ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC
ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC
文章目录
- ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC
- 动态调试环境配置
- Thinkphp反序列化链5.1.X原理分析
- 一.实现任意文件删除
- 二.实现任意命令执行
- 真正的难点
- Thinkphp反序列化链5.1.x 编写 Poc
- 汇总POC
动态调试环境配置
比较简洁的环境配置教程:
https://sn1per-ssd.github.io/2021/02/09/phpstudy-phpstorm-xdebug%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83/
Thinkphp反序列化链5.1.X原理分析
原理分析仅仅是遵循前辈的已有的道路,而不是完全探究每一种链子所带来的情况和可能性
前提:存在反序列化的入口
- unserialize()
- phar反序列化
- session反序列化
__destruct/__wakeup可以作为PHP反序列链的入口
这里简单介绍一下__destruct垃圾回收机制与生命周期的含义
__destruct可以理解为PHP的垃圾回收机制,是每次对象执行结束后必须执行的内容,但是执行的先后顺序往往和反序列化的生命周期有关
例如:
这里$test = new test("test",18, 'Test String');
对象被赋值给了$test变量,而不是直接的new test("test",18, 'Test String'); 传递给对象延长了对象的生命周期
所以是在echo '第二种执行完毕'.'
';执行后才执行了__destruct内容类似的比如快速销毁(Fast-destruct)
这里直接__construct后执行__destruct
因为unset — 清除指定变量直接销毁储存对象的变量,达到快速垃圾回收的目的
现在开始分析链子 Windows 类中__destruct执行了自身的removeFiles()方法
跟进removeFiles
private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; }
发现遍历$this->files,而且$this->files可控,作为数组传递
一.实现任意文件删除
@unlink($filename);删除了传递的filename
简单编写poc
可以实现任意文件的的删除
二.实现任意命令执行
除了任意文件删除,危害还可以更大吗?
通过POP链可以实现任意命令执行
全局逻辑图
private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; }
file_exists函数用于判断文件是否存在
预期传入 String $filename但是如果我们控制$filename作为一个对象,就可以隐形的调用类的__toString()方法
在thinkphp/library/think/model/concern/Conversion.php中
public function __toString() { return $this->toJson(); }
public function toJson($options = JSON_UNESCAPED_UNICODE) { return json_encode($this->toArray(), $options); }
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]) && is_array($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)) {//在poc中定义了append:["peanut"=>["whoami"] foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"] if (is_array($name)) {//$name=["whoami"]所以进入 // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); if ($relation) { $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法 } } $item[$key] = $relation ? $relation->append($name)->toArray() : []; } elseif (strpos($name, '.')) { list($key, $attr) = explode('.', $name); // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); if ($relation) { $relation->visible([$attr]); } } $item[$key] = $relation ? $relation->append([$attr])->toArray() : []; } else { $item[$name] = $this->getAttr($name, $item); } } } return $item; }
关键的几个判断和赋值
public function getRelation($name = null) { if (is_null($name)) { return $this->relation; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } return; }
if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"] foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"] if (is_array($name)) {//$name=["whoami"]所以进入 // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); if ($relation) { $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法 } }
public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut { try { $notFound = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $notFound = true; $value = null; }
public function getData($name = null)//$name = $key =peanut { if (is_null($name)) { return $this->data; } elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()] return $this->data[$name]; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); }
$relation->visible($name);中$relation可控,可以实现任意类的visible方法,如果visible方法不存在,就会调用这个类的__call方法
如何达到$relation->visible($name); 触发点 访问
if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]] foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"] if (is_array($name)) {//$name=["whoami"]所以进入
- 保证$this->append不为空
- $this->append 数组的值$name为数组 也就是二维数组
比如传入append:["peanut"=>["whoami"]]
接着向下走
$relation = $this->getRelation($key); if (!$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; }
不会进入if/elseif中 直接return;回来 为null
if (!$relation)为空进入判断
$relation = $this->getAttr($key); if ($relation) { $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法 }
public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut { try { $notFound = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $notFound = true; $value = null; }
进入$this->getData
public function getData($name = null)//$name = $key =peanut { if (is_null($name)) { return $this->data; } elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()] return $this->data[$name]; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); }
判断了$this->data传递的键存在,如果存在,返回其数组对应的键值
比如可以控制$this->data = ['peanut'=>new request()]
$relation = $this->getAttr($key); if ($relation) { $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法 }
$relation->visible($name);中$relation可控为任意类
现在寻找调用__call的类
在thinkphp/library/think/Request.php中
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); }
这里存在敏感关键函数call_user_func_array
__call($method, $args)接受的参数`
$method固定是visible
$args是传递过来的$name
if (array_key_exists($method, $this->hook)) { array_unshift($args, $this); return call_user_func_array($this->hook[$method], $args);
可以控制$this->hook['visible']为任意值,可以控制函数名
call_user_func()的利用方式无非两种
__call_user_func($method, $args) __
call_user_func_array([ o b j , obj, obj,method], $args)
如果执行第一种方式call_user_func($method, $args)
但是这里array_unshift($args, $this); 参数插入$this作为第一个值
参数是不能被正常命令识别的,不能直接RCE
那我们最终的利用点可以肯定并不是这里
如果选择第二种方式
call_user_func_array([$obj,$method], $args)
**通过调用 任意类 的 任意方法 **,可供选择的可能性更多
call_user_func_array([ o b j , " 任 意 方 法 " ] , [ obj,"任意方法"],[ obj,"任意方法"],[this,任意参数])
也就是 o b j − > obj-> obj−>func( t h i s , this, this,argv)
真正的难点
曲线救国的策略
难点理解:
__call魔术方法受到array_unshift无法可控触发call_user_func_array
利用_call调用isAjax类找可控变量再触发到filterValue里的call_user_func
为什么这里选Request类isAjax方法 接着POP链的调用了?
为什么当时的链子发现的作者会想到通过isAjax接着执行命令?
网上文章千篇一律,无非就是拿个poc动态调试,粘贴个poc就完了
Thinkphp反序列化漏洞 核心在于 逆向的思考 倒推
开发者不会傻乎乎写个system,shell_exec,exec等系统函数给你利用的可能
而我们又希望最终实现RCE的效果
我们最终应该更多关注于 不明显的回调函数或者匿名函数执行命令
比如call_user_func,call_user_func_array,array_map,array_filter...
在thinkphp/library/think/Request.php中
private function filterValue(&$value, $key, $filters) { $default = array_pop($filters); foreach ($filters as $filter) { if (is_callable($filter)) { // 调用函数或者方法过滤 $value = call_user_func($filter, $value);
$filter , $value 可控
通过传递 $filter , $value实现任意命令执行
那么什么地方调用了filterValue?回溯调用filterValue的地方
在thinkphp/library/think/Request.php中input调用
$this->filterValue($data, $name, $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', '