CTF入门:PHP字符串解析特性绕过WAF实现RCE
2026/9/16 21:36:14 网站建设 项目流程

做题做多了就会发现,很多CTF Web题其实都是“套娃”:表面是个平平无奇的计算器,背后往往藏着一个能让你getshell的洞。今天要复盘的是BUUCTF平台上的[RoarCTF 2019]Easy Calc,一道非常经典的PHP代码审计题。这道题的核心考点是“PHP字符串解析特性”,本质上就是在Nginx和PHP对查询字符串的解析差异上做文章,最终绕过WAF实现RCE并读取flag。网上关于这题的Writeup不少,但很多都只丢一个payload,原理只讲一句“加空格绕WAF”,对新手不太友好。这篇我会按自己的做题顺序完整过一遍,从信息收集、源码审计到绕过思路、构造payload,每一步都给可复现的请求和原理解释,希望能帮你把这个考点彻底吃透。

1. 信息收集:一个平平无奇的计算器页面

1.1 开局先看页面行为

打开题目给的地址,映入眼帘的是一个标准的计算器页面,输入1+1回车,页面正常返回2。输入2*3,返回6。看起来就是个普通的前端计算器,并没有什么特殊的地方。

但我平时做题有个习惯:不管页面多简单,先按F12看源码。这一看就发现了关键线索,页面引用了calc.php,并且功能是通过AJAX动态请求calc.php?num=...来完成的,也就是说真正执行计算逻辑的是一个PHP后端接口,而不是纯前端JS。

此时再回头看计算器页面,你会发现它有一个“伪限制”:输入框在JS层面被限制成只能输入数字和少量运算符号(比如+ - * / %),直接输入字母是没反应的。但这只是前端校验,传输到后端的数据完全可以通过Burp Suite抓包改包来绕过,所以这个限制约等于没有。

1.2 直接访问 calc.php 拿到源码

既然发现后端是calc.php,那就直接访问一下。请求GET /calc.php,页面居然直接把PHP源码打印了出来,这是很多PHP审计题的标准开局:当某个参数不存在时,用show_source(__FILE__)输出自己。

源码经过整理后大致是这个样子(不同平台部署的版本可能略有差异,但核心逻辑一致):

<?php error_reporting(0); if (!isset($_GET['num'])) { show_source(__FILE__); } else { $num = $_GET['num']; // 黑名单过滤 $blacklist = [ 'phpinfo', 'system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open', 'flag', 'cat', 'ls', 'dir' ]; foreach ($blacklist as $blackitem) { if (stripos($num, $blackitem) !== false) { die('what are you want to do?'); } } eval('echo ' . $num . ';'); } ?>

这里有一点需要提前说明:实际环境中WAF和参数取值可能不在同一层(后面会细讲),但过滤的关键词基本就是这些。源码的逻辑很简单:从$_GET里取num参数,经过黑名单过滤后,直接拼进eval执行。

看到eval的那一刻基本就可以确定了,这是一道RCE题。接下来要思考的就是怎么绕过黑名单,把我们要执行的代码喂给eval

2. 源码审计:WAF到底拦了什么、没拦什么

2.1 黑名单的分析

先梳理一下黑名单都拦了什么:

类别关键词
信息探测函数phpinfo
命令执行函数system、exec、passthru、shell_exec、popen、proc_open
文件操作关键字flag、cat、ls、dir

如果直接构造calc.php?num=phpinfo(),大概率会返回what are you want to do?,因为这个请求直接命中黑名单里的phpinfo。同理,num=system('cat /flag')也会因为出现systemcatflag等关键词而被拦。

这时候很多人会想到几种常规绕过思路:

  1. 大小写绕过:比如System('cat /flag')。PHP函数名本身不分大小写,但这里黑名单匹配用的是stripos,它也不区分大小写,所以直接被拦。
  2. URL编码绕过:比如?num=s%79stem('cat /flag')。如果WAF检查的是原始查询字符串,确实有一定概率绕过去,但这里的过滤发生在PHP代码里,也就是说$_GET['num']已经是URL解码后的值了,%79会被还原成y,最终还是会被stripos匹配到。
  3. 拼接绕过:比如?num=sys.(省略).tem('cat /flag')。对于动态函数调用,PHP确实允许$a = 'sys'.'tem'; $a('cat /flag');这种写法,但是注意黑名单里直接拦截了systemcatflag这些词,只要字符串拼接后完整包含某个关键词,stripos还是能匹配到。除非用更高级的异或、取反、自增来构造字符串,那又是另一个考点了。

所以这道题如果直接在参数值上想办法,会很痛苦。我们需要换个角度:既然WAF针对的是名为num的参数,那能不能让WAF根本认不出这个参数叫num,但PHP却仍然能把它解析成num这就是这道题最核心的绕过点。

2.2 WAF判断的是参数名,还是参数值?

这里就要讨论WAF的“薄弱点”了。很多WAF在做过滤时,是基于标准的参数名=参数值结构去匹配的。比如它会遍历所有查询参数,如果发现某个参数名等于num,就去检测这个参数值里有没有危险词。

问题在于:Web中间件(比如Nginx)对查询字符串的解析方式,和PHP的解析方式并不完全一致。如果两者存在认知差异,就很可能出现“WAF认为这不是num参数,但PHP实际却把它当成num参数”的情况。

拿到这道题目来说,利用的就是PHP对查询字符串参数名中“空格”和“点号”等字符的自动转换特性。

3. 关键绕过:PHP字符串解析特性

3.1 先在 BUUCTF 的题目环境里实测

直接说结论:在BUUCTF的题目环境中,访问下面的URL,可以成功执行phpinfo()

GET /calc.php?%20num=phpinfo() HTTP/1.1 Host: node4.buuoj.cn:xxxxx

注意num前面多了一个空格,并且这个空格被URL编码成了%20。请求发出后,可以看到页面没有报what are you want to do?,而是成功输出了PHP的配置信息,说明eval已经把phpinfo()当作代码执行了。

简单记录一下当时的前后对比:

请求结果
GET /calc.php?num=phpinfo()被WAF拦截,返回what are you want to do?
GET /calc.php?%20num=phpinfo()成功执行phpinfo()
GET /calc.php?%20num=system('cat /flag')成功输出flag

这就是这道题标准解法的核心payload。

3.2 为什么多一个空格就绕过了

要理解这个绕过,得先知道PHP解析查询字符串的一个特点:PHP在把查询字符串中的参数转换成$_GET$_REQUEST等超全局变量时,会对参数名做一些“清洗”操作,最典型的是把参数名中的空格和点号转换成下划线。

举个例子:

GET /test.php?a.b=1

在PHP里,$_GET['a_b']的值是1,而不是$_GET['a.b']

GET /test.php?a b=1

同理,空格也会被转换成下划线,$_GET['a_b']1

但是在Nginx层,它处理查询参数时并不会做这种“空格和点号转下划线”的转换。于是当我们提交%20num=phpinfo()时:

  • Nginx的$arg_num取不到值,因为它认为参数名是%20num或者_num,反正不是标准的num
  • 很多基于Nginx变量实现的WAF组件,自然也就不会对这个参数值做检测;
  • 而PHP解析时,参数名里的空格被自动转换成下划线,最终后端代码再通过某种方式取到了num对应的值,并拼进eval执行。

不同中间件和PHP版本的组合下,这个特性可能会表现出微妙的差异,有人会碰到直接取$_GET['num']取不到的情况,这也是为什么有些教程里会强调“以实际环境为准”。但本题环境里实测就是可以绕过的,这也是目前这题最通行的解法。

理论上,除了加空格,还有几种类似的绕过姿势可以尝试:

GET /calc.php?.num=phpinfo() GET /calc.php?%0anum=phpinfo() GET /calc.php?num%00=phpinfo()

核心思想都一样:利用中间件和PHP对参数名解析的差异,让WAF识别不到真正的参数名。

3.3 为什么不能选POST方式

可能有朋友会问:既然$_GET解析有问题,那我改成POST传参,比如num=phpinfo(),是不是也能绕过?

答案是:不能。

这道题后端代码写死了从GET查询字符串里取num参数,改成POST之后,$_GET['num']根本取不到值,eval自然也不会执行。而且POST参数的解析逻辑和查询字符串不同,一般也不会触发“空格转下划线”这类特性。所以老老实实走GET,在参数名上做文章就行。

4. 从RCE到读取Flag

4.1 先用 phpinfo 确认环境

执行phpinfo()不能直接拿flag,但它有一个很重要的作用:确认当前PHP环境里哪些函数被禁用了。

看到phpinfo页面后,重点看disable_functions这项配置。如果systemexecpassthru等函数都在禁用列表里,那命令执行这条路就断了,需要换文件读取函数。反之,如果disable_functions为空或者没有禁用这些函数,那RCE路径就很通畅。

BUUCTF这个题实例化出来的环境,大概率是没禁用system的,所以我们可以直接用命令执行来读flag。

4.2 读取根目录下的 /flag

默认情况下,这类题目容器会把flag放在Linux根目录,也就是/flag,而不是当前网站目录下的flag.txt。直接读取:

GET /calc.php?%20num=system('cat /flag') HTTP/1.1 Host: node4.buuoj.cn:xxxxx

请求返回的内容里会出现一长串flag,格式一般是flag{...}

如果你的有毒环境里system确实被禁了,也不要慌,PHP还有很多文件读取函数可以用,比如:

GET /calc.php?%20num=var_dump(file_get_contents('/flag')) GET /calc.php?%20num=readfile('/flag') GET /calc.php?%20num=highlight_file('/flag')

这些函数的作用是读取文件内容并输出,不需要依赖系统命令,所以即使WAF过滤了命令执行函数,只要没过滤这些函数名,一样能读flag。

如果想先看看根目录下都有什么文件,可以用:

GET /calc.php?%20num=var_dump(scandir('/'))

scandir会返回目录列表,如果flag不在根目录,可以再用scandir逐层找,比如/flag不在就去/var/www/html看源码,或者去/tmp/home找。

4.3 完整请求流程演示

用Burp Suite改包的话,完整的读取流程大概是:

GET /calc.php?%20num=system('cat /flag') HTTP/1.1 Host: node4.buuoj.cn:xxxxx User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Connection: close

这一步建议大家一定要自己动手实测一遍,尤其是新手,别只在浏览器地址栏里敲。地址栏会帮你做一部分编码处理,有时候会掩盖真实请求内容。用Burp你才能看到完整请求,也更容易理解到底绕过了什么。

5. 踩过的坑和一些经验总结

5.1 做题时最容易翻车的三个点

第一,前端限制不等于安全限制。计算器页面输入框只能输数字和运算符,这是前端JS做的校验,直接抓包改包或者用Burp重发就能绕过。很多新手会被这个限制卡住,以为题目必须在计算器里输入什么特殊内容,其实完全不需要碰那个输入框。

第二,空格编码别搞错。在URL里,空格可以编码成%20,但如果你写的是物理空格,有些服务端和HTTP解析器可能会直接报400错误。还有,这里的空格必须加在参数名num前面,而不是参数值里。参数值里的空格该编码还是要编码,但如果是 POST 请求或者用+号表示空格,要注意区分:在查询字符串里,+号常常会被解析成空格,这会导致表达式含义变化,需要特别注意。

第三,不要死磕命令执行。如果题目里的WAF或者disable_functions把命令执行函数全禁了,那就别想着catls这些了,直接换PHP文件函数读取,往往能更快解决战斗。做题的目的是拿flag,不是证明自己的RCE姿势有多花哨。

5.2 同类题目怎么举一反三

这道题的考点虽然叫“PHP字符串解析特性”,但本质上是中间件与PHP之间的解析差异问题。类似的绕过思路在很多CTF题里都能见到:

  • 有的题是用?num[]=phpinfo()传数组,让WAF的正则匹配失败,但PHP代码里如果用了$_GET['num'][0]或者函数参数要求字符串,就有机会利用数组转字符串或者类型错乱来绕过。
  • 有的题是利用?num=1%00phpinfo()这类空字节截断,让WAF读到的是1,PHP在特定老版本解析时把后面的内容也带进来。
  • 有的题更彻底,直接把字母和数字过滤掉,那就只能靠异或、取反、自增这类无字母数字RCE的技巧来构造代码。

不管形式怎么变,解题路径基本是固定的:拿到题目先找源码,再找到过滤点,然后分析过滤点是否有解析差异可以利用,最后构造payload执行。以后遇到“奇怪的计算器”“奇怪的输入框”“奇怪的查询参数”,一定要多留个心眼。

另外再多说一句:做这种题,一定要养成“抓包看原始请求”的习惯。地址栏访问虽然方便,但很多细节都会被浏览器自动处理掉,导致你根本看不出WAF到底拦了什么、绕过了什么。Burp Suite不仅是渗透测试工具,更是CTF Web题的学习利器,多用几次就会爱上它。

这道题整体难度不高,考点也很经典,作为PHP代码审计的入门题非常合适。如果你能独立走完“发现计算器、找到calc.php、审源码、加空格绕WAF、读flag”这条完整链路,那说明你已经初步理解了Web题里最常见的的WAF绕过思路。之后遇到再复杂的题目,核心分析思路大体也是这样的,剩下的就是经验和姿势的积累了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询