尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CTF入门:PHP字符串解析特性绕过WAF实现RCE

CTF入门:PHP字符串解析特性绕过WAF实现RCE 做题做多了就会发现很多CTF Web题其实都是“套娃”表面是个平平无奇的计算器背后往往藏着一个能让你getshell的洞。今天要复盘的是BUUCTF平台上的[RoarCTF 2019]Easy Calc一道非常经典的PHP代码审计题。这道题的核心考点是“PHP字符串解析特性”本质上就是在Nginx和PHP对查询字符串的解析差异上做文章最终绕过WAF实现RCE并读取flag。网上关于这题的Writeup不少但很多都只丢一个payload原理只讲一句“加空格绕WAF”对新手不太友好。这篇我会按自己的做题顺序完整过一遍从信息收集、源码审计到绕过思路、构造payload每一步都给可复现的请求和原理解释希望能帮你把这个考点彻底吃透。1. 信息收集一个平平无奇的计算器页面1.1 开局先看页面行为打开题目给的地址映入眼帘的是一个标准的计算器页面输入11回车页面正常返回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?numphpinfo()大概率会返回what are you want to do?因为这个请求直接命中黑名单里的phpinfo。同理numsystem(cat /flag)也会因为出现system、cat、flag等关键词而被拦。这时候很多人会想到几种常规绕过思路大小写绕过比如System(cat /flag)。PHP函数名本身不分大小写但这里黑名单匹配用的是stripos它也不区分大小写所以直接被拦。URL编码绕过比如?nums%79stem(cat /flag)。如果WAF检查的是原始查询字符串确实有一定概率绕过去但这里的过滤发生在PHP代码里也就是说$_GET[num]已经是URL解码后的值了%79会被还原成y最终还是会被stripos匹配到。拼接绕过比如?numsys.(省略).tem(cat /flag)。对于动态函数调用PHP确实允许$a sys.tem; $a(cat /flag);这种写法但是注意黑名单里直接拦截了system和cat、flag这些词只要字符串拼接后完整包含某个关键词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?%20numphpinfo() HTTP/1.1 Host: node4.buuoj.cn:xxxxx注意num前面多了一个空格并且这个空格被URL编码成了%20。请求发出后可以看到页面没有报what are you want to do?而是成功输出了PHP的配置信息说明eval已经把phpinfo()当作代码执行了。简单记录一下当时的前后对比请求结果GET /calc.php?numphpinfo()被WAF拦截返回what are you want to do?GET /calc.php?%20numphpinfo()成功执行phpinfo()GET /calc.php?%20numsystem(cat /flag)成功输出flag这就是这道题标准解法的核心payload。3.2 为什么多一个空格就绕过了要理解这个绕过得先知道PHP解析查询字符串的一个特点PHP在把查询字符串中的参数转换成$_GET、$_REQUEST等超全局变量时会对参数名做一些“清洗”操作最典型的是把参数名中的空格和点号转换成下划线。举个例子GET /test.php?a.b1在PHP里$_GET[a_b]的值是1而不是$_GET[a.b]。GET /test.php?a b1同理空格也会被转换成下划线$_GET[a_b]为1。但是在Nginx层它处理查询参数时并不会做这种“空格和点号转下划线”的转换。于是当我们提交%20numphpinfo()时Nginx的$arg_num取不到值因为它认为参数名是%20num或者_num反正不是标准的num很多基于Nginx变量实现的WAF组件自然也就不会对这个参数值做检测而PHP解析时参数名里的空格被自动转换成下划线最终后端代码再通过某种方式取到了num对应的值并拼进eval执行。不同中间件和PHP版本的组合下这个特性可能会表现出微妙的差异有人会碰到直接取$_GET[num]取不到的情况这也是为什么有些教程里会强调“以实际环境为准”。但本题环境里实测就是可以绕过的这也是目前这题最通行的解法。理论上除了加空格还有几种类似的绕过姿势可以尝试GET /calc.php?.numphpinfo() GET /calc.php?%0anumphpinfo() GET /calc.php?num%00phpinfo()核心思想都一样利用中间件和PHP对参数名解析的差异让WAF识别不到真正的参数名。3.3 为什么不能选POST方式可能有朋友会问既然$_GET解析有问题那我改成POST传参比如numphpinfo()是不是也能绕过答案是不能。这道题后端代码写死了从GET查询字符串里取num参数改成POST之后$_GET[num]根本取不到值eval自然也不会执行。而且POST参数的解析逻辑和查询字符串不同一般也不会触发“空格转下划线”这类特性。所以老老实实走GET在参数名上做文章就行。4. 从RCE到读取Flag4.1 先用 phpinfo 确认环境执行phpinfo()不能直接拿flag但它有一个很重要的作用确认当前PHP环境里哪些函数被禁用了。看到phpinfo页面后重点看disable_functions这项配置。如果system、exec、passthru等函数都在禁用列表里那命令执行这条路就断了需要换文件读取函数。反之如果disable_functions为空或者没有禁用这些函数那RCE路径就很通畅。BUUCTF这个题实例化出来的环境大概率是没禁用system的所以我们可以直接用命令执行来读flag。4.2 读取根目录下的 /flag默认情况下这类题目容器会把flag放在Linux根目录也就是/flag而不是当前网站目录下的flag.txt。直接读取GET /calc.php?%20numsystem(cat /flag) HTTP/1.1 Host: node4.buuoj.cn:xxxxx请求返回的内容里会出现一长串flag格式一般是flag{...}。如果你的有毒环境里system确实被禁了也不要慌PHP还有很多文件读取函数可以用比如GET /calc.php?%20numvar_dump(file_get_contents(/flag)) GET /calc.php?%20numreadfile(/flag) GET /calc.php?%20numhighlight_file(/flag)这些函数的作用是读取文件内容并输出不需要依赖系统命令所以即使WAF过滤了命令执行函数只要没过滤这些函数名一样能读flag。如果想先看看根目录下都有什么文件可以用GET /calc.php?%20numvar_dump(scandir(/))scandir会返回目录列表如果flag不在根目录可以再用scandir逐层找比如/flag不在就去/var/www/html看源码或者去/tmp、/home找。4.3 完整请求流程演示用Burp Suite改包的话完整的读取流程大概是GET /calc.php?%20numsystem(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把命令执行函数全禁了那就别想着cat、ls这些了直接换PHP文件函数读取往往能更快解决战斗。做题的目的是拿flag不是证明自己的RCE姿势有多花哨。5.2 同类题目怎么举一反三这道题的考点虽然叫“PHP字符串解析特性”但本质上是中间件与PHP之间的解析差异问题。类似的绕过思路在很多CTF题里都能见到有的题是用?num[]phpinfo()传数组让WAF的正则匹配失败但PHP代码里如果用了$_GET[num][0]或者函数参数要求字符串就有机会利用数组转字符串或者类型错乱来绕过。有的题是利用?num1%00phpinfo()这类空字节截断让WAF读到的是1PHP在特定老版本解析时把后面的内容也带进来。有的题更彻底直接把字母和数字过滤掉那就只能靠异或、取反、自增这类无字母数字RCE的技巧来构造代码。不管形式怎么变解题路径基本是固定的拿到题目先找源码再找到过滤点然后分析过滤点是否有解析差异可以利用最后构造payload执行。以后遇到“奇怪的计算器”“奇怪的输入框”“奇怪的查询参数”一定要多留个心眼。另外再多说一句做这种题一定要养成“抓包看原始请求”的习惯。地址栏访问虽然方便但很多细节都会被浏览器自动处理掉导致你根本看不出WAF到底拦了什么、绕过了什么。Burp Suite不仅是渗透测试工具更是CTF Web题的学习利器多用几次就会爱上它。这道题整体难度不高考点也很经典作为PHP代码审计的入门题非常合适。如果你能独立走完“发现计算器、找到calc.php、审源码、加空格绕WAF、读flag”这条完整链路那说明你已经初步理解了Web题里最常见的的WAF绕过思路。之后遇到再复杂的题目核心分析思路大体也是这样的剩下的就是经验和姿势的积累了。
返回列表