1. 题目概览:先搞清楚 Exec1 到底在考什么
BUUCTF 上的 [ACTF2020 新生赛] Exec1 是一道很典型的 Web 方向入门题,核心考点就四个字:命令执行。更准确地说,是命令注入(Command Injection)。题面本身不复杂,一个在线 Ping 检测工具,输入一个 IP 地址,后端把输入拼进系统命令执行,然后回显 ping 的结果。很多第一次打 CTF 的人看到这个页面会犯迷糊:这不就是个普通的 ping 工具吗,怎么跟 flag 扯上关系?问题恰恰出在“拼接”这两个字上。
ACTF2020 是某高校战队办的新生赛,题目定位很明确,就是给刚接触 CTF 的新手铺路。Exec1 的 “Exec” 指的是命令执行类漏洞,“1” 说明这大概率是整个系列的第一题,后面可能还有 Exec2、Exec3 之类的进阶版本。BUUCTF 平台上这类题目的环境大多是 Docker 一键部署,出题人已经把靶场搭好了,我们只需要通过浏览器去访问一个 URL,就能进入题目页面。
这道题适合什么人做?只要你有一点 Linux 命令基础,哪怕完全没接触过 CTF,也可以拿它当 RCE 方向的第一个练手靶场。做完它,你基本上能理解“用户输入如何变成系统命令的一部分”这个 Web 安全里最核心的问题之一。接下来我会按我实际打题的顺序,把从信息收集、注入点判断、payload 构造到最终读取 flag 的完整过程拆开来讲,包括中间踩过的坑和一些绕过滤规则的技巧。
2. 核心考点:命令注入漏洞原理与 Payload 设计
2.1 命令注入和 SQL 注入本质上是同一个问题
先说原理。命令注入漏洞的产生原因特别直白:后端代码把用户输入直接拼到了系统命令里,然后交给 shell 去执行。比如题目的服务端逻辑大概长这样:
<?php $ip = $_GET['ip']; system("ping -c 4 " . $ip); ?>这段代码的本意是接收一个 IP 参数,然后 ping 这个地址。问题在于$ip完全没做过滤,用户输入什么,就拼接成什么。如果你传进去的是127.0.0.1;ls,那么最终 shell 执行的就是:
ping -c 4 127.0.0.1;ls分号左边的 ping 指令正常执行,分号右边的ls也会跟着执行。这种利用方式和 SQL 注入的思路是一模一样的:开发者的代码信任了用户输入,把输入内容当成了代码的一部分去解释执行。区别只是 SQL 注入拼接的是 SQL 语句,命令注入拼接的是系统命令。搞懂了这一点,你就明白为什么安全开发里总强调“永远不要信任用户输入”。
2.2 几个拼接符号的语义差异一定要分清
做命令注入题,Payload 里最常用的拼接符号有四个:分号;、管道符|、逻辑与&&、逻辑或||。很多入门教程会直接告诉你用;,但如果只背结论不看语义,遇到过滤规则就会很被动。
分号;表示顺序执行,不管前一条命令成功还是失败,后一条都会执行。管道符|表示前一条命令的 stdout 作为后一条命令的 stdin 输入。&&表示前一条命令必须执行成功,后一条才会执行。||恰好相反,前一条命令失败了,后一条才执行。它们之间的差异在 ping 场景里很明显:
127.0.0.1;ls # 先 ping,再 ls,两条都执行 127.0.0.1|ls # 把 ping 的输出丢给 ls 处理,ls 大概率会解析一堆没意义的内容 127.0.0.1&&ls # 如果 ping 成功,执行 ls 127.0.0.1||ls # 只有 ping 失败才执行 ls,比如输入一个不存在的主机名本题最稳妥的是分号。但实战里 WAF 可能拦分号,这时候你可以试管道符、换行符%0a,甚至是&&和||的变体。多了解几种符号,等于多几个绕过思路。
2.3 为什么前面一定要先拼一个合法 IP
新手最容易犯的一个错误是直接输入ls或者cat /flag,结果页面显示ping: unknown host,一脸懵。原因在于后端拼出来的命令是ping -c 4 ls,shell 把ls当成主机名去解析了,压根不会执行列目录的操作。
正确的姿势是:先给一个合法 IP 保证 ping 命令能顺利执行,再用拼接符号把我们的命令带出来。这个思路可以总结成一句话:“合法参数 + 拼接符号 + 恶意命令”。在本题里就是127.0.0.1;ls。先让 ping 正常工作,分号后面的命令才会被 shell 解释执行。这个套路不仅适用于这道题,后面做任何命令注入题目,第一反应都应该是先构造一个能正常执行的基底,再去叠加攻击命令。
3. 完整解题过程:一步步拿到 Flag
3.1 第一步:验证注入点是否存在
打开题目页面后,环境信息大概是这样:一个输入框,一个提交按钮,提示语通常是“输入 IP 地址进行 Ping 检测”。我先输入127.0.0.1提交,页面正常输出了 ping 的完整回显,说明后端确实执行了 ping 命令。
接下来验证注入点。我提交127.0.0.1;whoami,如果页面在 ping 的输出后面多了一行用户信息,比如www-data,那就说明分号后面的命令被成功执行。这里还可以顺手提交127.0.0.1;ls -la看当前目录结构,进一步确认是哪种系统环境。如果ls能跑而dir报错,基本可以判定后端是 Linux;反过来如果能用dir,那可能是 Windows 环境。这道题环境默认是 Linux,权限通常也给了www-data,但命令注入题里权限一般都不低,直接读文件通常没有问题。
我实操的时候,输入127.0.0.1;whoami返回了www-data,注入点确认无疑。到这一步,这道题最核心的部分已经拿下了:你可以在靶机上执行任意系统命令。
3.2 第二步:定位 Flag 文件在哪
注入点确认后,目标就变成找到 flag 文件并读取内容。不少入门者在这里会钻进一个误区:只在当前目录瞎转悠,不停ls、cat当前目录下的每个文件。其实 CTF 出题人经常把 flag 放在根目录,或者放在一个不起眼的路径下,全盘搜索往往比手动翻目录高效得多。
我当时的做法是先看根目录:
127.0.0.1;ls /回显里能看到flag文件直接在根目录下。但有时候 flag 文件名不叫flag,而是叫flag.txt、flag.php、flag_12345这类变体,所以更通用的搜索命令是这样:
127.0.0.1;find / -name "*flag*" 2>/dev/null这条命令会从根目录往下全盘搜索文件名带 flag 的文件,同时把权限不足的报错信息丢弃。执行结果可能有一堆,但真正有用的通常就是/flag或者/var/www/html/flag.txt这类路径。如果目标文件在/var/www/html下,说明它和 Web 服务在同一个目录,出题人可能想让你通过访问 URL 读取,但大多数情况下用命令直接读取就好。
3.3 第三步:读取 Flag 并提交
找到文件路径之后,读取内容就很简单了:
127.0.0.1;cat /flag回显里会出现一个标准的 CTF flag 字符串,格式通常是flag{...}。把这一串复制到 BUUCTF 的提交框里,题目就算打通了。
这道题到拿 flag 为止就结束了,没有二次绕过,没有特殊过滤,属于非常标准的入门体验。但我要强调一个细节:读取 flag 前,最好先确认一下文件类型。如果你用cat读一个二进制文件,终端会直接花屏,这不算大问题,但如果你读的是一个 PHP 文件,它可能被服务端解析后不返回源码,导致你什么都看不到。稳妥的做法是先file /flag看类型,再用cat读。
3.4 请求方式的小细节:能用 curl 就别手敲浏览器
实际操作时,很多人喜欢直接在浏览器地址栏里输入完整 Payload,比如:
http://target/?ip=127.0.0.1;cat /flag浏览器会帮你把空格、分号、斜杠做 URL 编码,然后在服务端解码。一般情况下没问题,但如果你要执行的命令很长,或者包含特殊字符,比如{}、$、#、&,地址栏写起来会很痛苦,还可能被某些浏览器插件或代理干扰。我的习惯是用 curl:
curl -X GET 'http://target/?ip=127.0.0.1;cat%20/flag'或者用更稳妥的 POST 方式配合--data-urlencode,让 curl 自动完成 URL 编码:
curl -X POST 'http://target/' --data-urlencode 'ip=127.0.0.1;cat /flag' --data-urlencode 'submit=ping'使用 Burp Suite 也很方便,抓包后直接在 Repeater 里改参数,可以反复尝试各种 Payload。这个习惯对于后面做更复杂的 RCE 题目非常重要,因为手工在浏览器里敲命令一旦出错,定位问题要花更多时间。
4. 变体与进阶:从 Exec1 延伸到更多命令执行场景
4.1 无回显怎么办:延时探测与写文件
Exec1 这个页面是有回显的,所以做起来很舒服。但真实比赛里很多命令注入是“盲注”状态,命令执行了,页面却一点输出都没有。这时候就要换思路,核心是找一个外部可观测的通道来验证命令是否执行。
最朴素的验证方式是用延时。提交127.0.0.1;sleep 5,然后掐表看响应时间。如果页面隔了 5 秒才返回,说明sleep命令成功执行。在此基础上还可以进一步做数据外带,比如把命令执行结果写入 Web 目录下,再用浏览器访问那个文件:
127.0.0.1;ls / > /var/www/html/out.txt然后访问http://target/out.txt,如果能看到目录列表,说明写入成功,命令也有回显通道了。再比如用 DNS 外带的方式,把 flag 内容拼到域名里发出去,再由自己的服务器接收,不过这道题用不上这么复杂的手段,知道思路即可。
4.2 空格被过滤时的绕过套路
题目如果简单,直接敲空格就行。但很多靶场和实战 WAF 会把空格视为高危字符直接拦掉。这时候最常见的替代品是${IFS},即国际字段分隔符(Internal Field Separator)环境变量,默认包含空格、Tab 和换行。在 shell 里把${IFS}放到原本空格的位置,效果等同:
127.0.0.1;cat${IFS}/flag有些更严格的环境会对${IFS}本身做过滤,这时候可以尝试${IFS}$9,或者直接用 Tab 字符(URL 编码为%09):
127.0.0.1;cat%09/flag再不行就试$IFS不带花括号。绕过方式没有一定之规,关键是要理解 shell 对特殊变量的解析规则,然后在不同过滤规则之间来回试。
4.3 关键字被过滤时的三个常用手段
过滤规则如果连cat、flag这种关键字都不放过,就需要动用 shell 的特性来拆解字符串了。第一种是单引号和反斜杠拆字,c''at、c\at在 shell 解析后都会变成cat,如果后端只是简单地把cat字符串替换为空,这种写法就能绕过。第二种是参数扩展:
a=c;b=at;$a$b /flag第三种是 base64 编码后管道执行:
echo Y2F0IC9mbGFn | base64 -d | shY2F0IC9mbGFn是cat /flag的 base64 编码,解码后由sh执行。这个方法的优点是整个命令字符串里没有出现cat和/flag关键字,适合对付关键词黑名单。但要注意base64 -d在某些精简镜像里可能不存在,目标环境如果是 Alpine Linux 或特殊容器,可能会报错,最好先确认基础命令是否可用。
4.4 防御视角:这道题在真实开发中怎么修
打完一道题,我会习惯性想一想开发侧应该怎么堵住这个洞。Exec1 的修复方法在面试里也常被问到。第一层是输入校验,比如用正则限定只能输入 IPv4 地址,这样;ls这种内容根本进不了命令。第二层是命令调用方式,PHP 里可以用escapeshellarg()对参数做转义,或者干脆不用system()、exec()这类函数,改成直接用网络库的 ICMP 功能,从根源上避免拼接 shell 命令。第三层是权限控制,Web 服务使用低权限用户运行,即使被注入,能读到的文件也有限。这道题里直接cat /flag能成功,说明容器权限给得比较宽松,真实环境一般不会这么大方。
4.5 做完之后还能练什么
Exec1 是一道很好的起点题,做完它你可以顺着命令执行这个方向继续刷题。BUUCTF 上搜“Exec”或者“RCE”能找到一批类似题目,难度从有回显到无回显、从无过滤到有过滤逐渐递进。还可以试试 ACTF2020 新生赛系列里的其他 Web 题,比如上传相关的题目,思路会从 RCE 转到文件上传和 Web Shell 方向。如果想把命令注入做得更扎实,建议多了解 Linux shell 的变量展开、管道、重定向、通配符这些基础能力,它们不仅是打 CTF 的工具,也是日常运维排错的底层功。
我个人做这道题最大的体会是:命令注入不靠背 Payload,靠理解“拼接”这件事。你只要知道服务端是把输入原样塞进命令里,就能顺着“合法参数 + 拼接符号 + 恶意命令”的框架自己推出各种玩法。Exec1 把这条链路完整演示了一遍,新手只需要照着我上面的步骤多操作几次,很快就能建立 RCE 方向的手感。