
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用来抓包、改包、重放请求。命令注入经常要反复构造 URLRepeater 比地址栏方便。curl如果你更喜欢命令行直接用 curl 发请求后续自动化脚本也更好写。工具不需要多关键是顺手。我个人习惯是在浏览器里先确认页面结构再把请求丢到 Burp 里改参数。因为这个题目只是一个 GET 请求所以用 curl 反而更轻量比如curl http://node4.buuoj.cn:xxxxx/?cmdid2.2 页面源码审计漏洞点直接摆在眼前我先把环境打开的页面完整看了一遍它就是 2.3 节里那个 PHP 源码。这里有个容易忽略的细节highlight_file(__FILE__)的作用是把你当前访问的 PHP 文件源码高亮显示出来。没有这个逻辑的话访问根路径可能是一个空白页或者 404我们就得多一步猜参数、扫目录的操作。既然源码直接可见那后面就不用再看目录结构和配置文件了。接下来的工作就是围绕cmd参数做探测。注意一点题目代码里用的是passthru而不是system这会直接影响回显方式。system()执行命令后会把最后一行输出返回给 PHP而passthru()是把整个原始输出全部写到浏览器上。正因为这样我们执行ls、cat这些命令时能看到非常完整的回显这比某些只回显最后一行的题目友好得多。2.3 参数探测先确认注入点源码审计得出结论后第一步就是测试cmd参数是否真实可用。在浏览器里访问/?cmdls如果页面返回了当前目录下的文件列表说明命令已经执行成功。比如可能看到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)并没有限制命令个数它只是把一个字符串交给 shellshell 会根据语法把它解析成多条命令。比如参数传成id; whoamishell 会先执行id再执行whoami。3.3 命令注入的本质Shell 元字符的作用说到命令注入就不得不提 shell 元字符。常见的命令分隔和组合符号有;按顺序执行多条命令。前一条命令成功后才执行后一条。||前一条命令失败后才执行后一条。|管道把前一条命令的输出作为后一条命令的输入。$()命令替换先执行括号里的命令并把结果放到当前位置。反引号和$()作用类似。这个题因为整体命令就是完全可控的所以甚至不需要用这些元字符去“拼接”什么我直接把cmd参数的值写成完整命令就好了。但在更复杂的 CTF 题目里如果代码把参数拼到了某个固定命令后面passthru(ping . $_GET[cmd]);这时候就必须利用;、|、等符号来截断原命令才能执行我们自己的命令。这也是命令注入这一类漏洞的通用核心。4. 利用过程从命令注入到读取 flag4.1 第一步验证命令执行点存在我开始正式打这个靶机时先执行了id命令GET /?cmdid HTTP/1.1 Host: node4.buuoj.cn:xxxxx响应内容是uid33(www-data) gid33(www-data) groups33(www-data)这里能拿到当前用户的 UID 和 GID说明服务器确实是以www-data身份在运行 Web 服务。验证命令执行点存在之后接下来的思路就很简单了先看看环境里有什么文件再找 flag。这一步看起来容易但新手经常在这时候犯一个错误验证完id之后就直接开始疯狂尝试各种 payload一会cat /flag一会cat flag.txt也不先看目录结构。我建议养成一个习惯把“信息收集”这一步做扎实命令执行点确认后先列目录、看路径、看身份再针对性地找 flag。4.2 第二步确定当前身份与目录结构我依次执行了下面两条命令/?cmdpwd /?cmdls -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命令去全盘找。常用写法/?cmdfind / -name flag* 2/dev/null解释一下这条命令find /从根目录开始找。-name flag*匹配所有以 flag 开头的文件。2/dev/null把错误输出丢弃避免因为某些目录没有权限而刷屏。find全盘搜索有时会有点慢因为容器里文件多、有些目录没有权限访问需要逐个跳转。更快的替代方案是限制搜索深度/?cmdfind / -maxdepth 3 -name flag* 2/dev/null-maxdepth 3表示只在三级目录以内搜索速度会快很多。对于大多数 CTF 容器来说flag 一般就在/、/root、/home、/var/www/html这几个常见位置三级深度基本够用。在我这台靶机上find的输出非常直接/flag说明根目录下就有一个名为 flag 的文件。4.4 第四步读取 flag 并提交找到/flag之后读取它/?cmdcat /flag页面返回了一串字符串这就是这道题的 flag。形如flag{...}或者INSA{...}。把它复制到 BUUCTF 题目页面里的提交框Done。如果题目里的 flag 不是/flag而是/flag.txt用同样的方法读取就行/?cmdcat /flag.txt如果cat某些环境下被禁用了也可以用tac、nl、more、less、head、tail等命令读取文本文件。例如/?cmdnl /flag /?cmdmore /flag /?cmdhead -n 100 /flag4.5 完整请求示例Burp 和 curl 双版本整个利用过程如果整理成一个最简单的请求在浏览器地址栏访问http://node4.buuoj.cn:xxxxx/?cmdcat%20/flag注意 URL 里的空格被编码成了%20。如果不编码直接在浏览器地址栏输入空格浏览器通常也会自动处理但在 Burp 里手动改包时一定要记得编码。用 Burp Repeater 发送的完整请求长这样GET /?cmdcat%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/?cmdcat%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。%09URL 编码后的 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 cat /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访问/?cmdid时如果页面直接白屏或者返回 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错误信息刷屏导致输出被截断。建议改成/?cmdfind / -name flag* 2/dev/null如果还是慢就加-maxdepth/?cmdfind / -maxdepth 3 -name flag* 2/dev/null也可以换个思路先看常见目录/?cmdls -la / /?cmdls -la /var/www/html /?cmdcat /flag6.4 过滤规则比预想更严格如果你在 BUUCTF 上拿到的环境和我描述的不完全一样说明题目可能被改过版本或者我当天遇到的是简单版本。遇到过滤更严的情况不要乱试先把“过滤了哪些字符”摸清楚。方法很简单逐步减少 payload观察哪部分触发拦截。比如先传?cmdcat /flag如果被拦改成?cmdcat ?cmd/flag ?cmdcat /fl*通过单变量对比就能知道是空格、命令名还是路径关键字被过滤了。然后回到第 5 章找对应的绕过方案。这道题做完以后我最大的感受是CTF Web 里很多题目并不是考“多炫的 0day”而是考基本功和判断力。你会不会从题目名里读出函数名会不会验证注入点会不会用find找文件这些动作单独看都不难但组合在一起就是完整解题链。建议新手拿到这道题以后不要满足于提交 flag把所有命令在本地 PHP 环境里复现一遍再试着手动实现一个简单的字符过滤用绕过技巧去打一遍这样才是把一道送分题转化成真本事。