Reference:
https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA
反序列化起点
ThinkPHP/Library/Think/Image/Driver/Imagick.class.php
line 636 - line 642
1 | public function __destruct() |
在反序列化中,所有成员属性均可控。即 $this->img
可控。
如此一来,即可 无参数式 调用 TP 下任意类的 destroy 方法
destroy() 跳板
搜索 destroy 方法,找到三个存在 destroy 的位置:

其中两个类 Think\Session\Driver\Mysqli
类 和 Think\Session\Driver\Db
类 是直接调用 mysqli_query 进行数据库操作的,如下:
1 | public function destroy($sessID) |
由于 mysqli_query 中 $hander
的值取决与 $this->hander
,但是就算我们在序列化 POC 中进行 mysqli_connect ,句柄移植之后是不可用的,所以这个点只能放弃。
这里注意下, php7 调用有参数函数时必须传参,不然会报错。但 php5 则可不传参数调用有参数函数。
转头看第三个类 Think\Session\Driver\Memcache
1 | public function destroy($sessID) |
由于 $this->handle
可控,我们可以调用任意类的 delete 方法。
并且由于跳到 destroy 方法时是 无参数调用。这里的 $sessID 是个无效的形参。用这个无效的形参去调用别的函数时,传入的参数会无效。所以这里调用 delete 的形参还是不可控的
Model delete() 跳板
全局搜索 function delete
。找到 Think\Model
类
1 | public function delete($options = array()) |
$this->getPK()
函数仅仅只是 return $this->pk;
所以 $pk 的值为 $this->pk
可以发现在 if
判断中,如果传入的 $options 为空,则重新调用 $this->delete()
方法 ,并且传入的参数为 $this->data[$pk]
。这样子 *delete()*方法的形参 $options 就是可控的了。
在分析表达式 流程中,重新为 $options 赋值,调用了 $this->_parseOptions()
。该方法将会返回 $this->options
1 | protected function _parseOptions($options = array()) |
分析表达式 流程后还判断了 $options[‘where’] 。需要确保 $options 中有该值
程序接着调用了 $this->db->delete($options)
,其中 $this->db 可控,并且 $options 可控,全局搜索 function delete
。找到 Think\Db\Driver 类。
至于选择这个类的原因,因为它在后面构造 SQL payload 的时候比较方便。
Driver delete() 跳板
Think\Db\Driver 类 delete() 方法主要代码如下:
1 | $table = $this->parseTable($options['table']); |
可是该类为抽象类,无法直接实例化。需要找一个继承它的子类,并且该类没有 *delete()*,这样程序调用的时候才能调用到父类的 delete() 方法。
最终找到 Think\Db\Driver\Mysql类 作为反序列化的实例化类。
delete()
方法 最终调用了 $this->execute()
,由于此类是专门用作数据库操作的,execute()
中并没有发现能 RCE 的点,也没有发现能当跳板的点。
不过注意到 execute()
的第一行
1 | $this->initConnect(true); |
跟进,发现当不存在 $this->_linkID 时,将会调用 $this->connect()
1 | ...... |
跟进,发现该函数使用 PDO 进行数据库连接,并返回了句柄
1 | if (!isset($this->linkID[$linkNum])) { |
返回句柄后,execute
方法 将会根据 传入的 sql语句 执行sql。这里 传入的sql语句 就是 delete()
方法 中的 $sql
基本Payload
至此,以上整个从反序列化到执行SQL注入的流程。根据以上流程,得出基本Payload:
Payload demo:
1 |
|
开启 Debug ,追踪程序执行流程,发现 SQL 语句成功被控制

但是,由于我们无法知道目标服务器的 Mysql 配置,所以 SQL注入自然是跑不动的。
仔细查看数据库连接函数 connect
后,发现其 PDO 配置是我们可控的
1 | if (!isset($this->linkID[$linkNum])) { |
所以我们可以控制 tp3 连接的数据库,自然想到了 Rogue mysql server
原理简单来说是这样的:
mysql中有一个 SQL语句,为 LOAD DATA LOCAL INFILE。作用是将客户端本地的文件加载到数据库中。而 Rogue mysql server 可以任意读取 mysql 客户端的本地文件。
具体详情可见本博客的另一篇文章 :)
Rogue Mysql Server 简单分析尝试让 tp3 连接 Rogue mysql server。修改 payload,增加数据库配置
1 |
|
成功读取客户端文件

自此文章就写到这了,本来二月十多号开始写的,中间停停写写,最后拖到现在才全部写完。。。
该漏洞还可继续利用,通过 Rogue Mysql Server 读取 tp3 的 数据库配置,再利用该配置进行 SQL注入。具体的 Reference 中写了,膜拜下奶权师傅