从CTF题[鹤城杯 2021]EasyP剖析PHP安全:$_SERVER变量、正则绕过与basename的攻防实战

张开发
2026/5/11 11:16:07 15 分钟阅读
从CTF题[鹤城杯 2021]EasyP剖析PHP安全:$_SERVER变量、正则绕过与basename的攻防实战
1. 从一道CTF题看PHP安全攻防第一次看到这道题的时候我也被它绕晕了。表面上就是一段简单的PHP代码但仔细一看全是坑。这道题来自2021年鹤城杯CTF比赛名字叫EasyP但实际上一点都不easy。它巧妙利用了PHP中几个容易被忽视的安全特性包括$_SERVER变量的差异、basename()函数的特性以及正则表达式的绕过技巧。这道题的核心在于如何通过精心构造的URL绕过代码中的安全防护最终读取到utils.php文件中的flag。整个过程涉及到三个关键技术点$_SERVER[PHP_SELF]和$_SERVER[REQUEST_URI]的区别、basename()函数在处理路径时的特性以及如何利用特殊字符绕过正则表达式检查。下面我们就来逐一拆解这些技术点看看这道题到底是怎么被攻破的。2. 理解$_SERVER变量的关键差异2.1 $_SERVER[PHP_SELF]的玄机$_SERVER[PHP_SELF]这个变量看起来简单但实际上藏着不少坑。它返回的是当前执行脚本相对于网站根目录的路径。听起来很直接对吧但关键在于它如何处理URL中的额外路径信息。举个例子如果你的网站结构是这样的/var/www/html/index.php当访问http://example.com/index.php时$_SERVER[PHP_SELF]的值就是/index.php。但如果访问http://example.com/index.php/extra/path它的值就会变成/index.php/extra/path。这个特性在本题中被巧妙地利用了。我在实际测试中发现即使extra/path这部分根本不存在PHP也会原封不动地把它包含在PHP_SELF中。这就为路径操纵提供了可能。比如我们可以构造一个URL让PHP_SELF看起来像是访问了utils.php但实际上执行的是index.php。2.2 $_SERVER[REQUEST_URI]的不同之处相比之下$_SERVER[REQUEST_URI]的行为就有所不同了。它返回的是请求的完整URI包括查询字符串。比如访问http://example.com/index.php/test?paramvalue时REQUEST_URI的值就是/index.php/test?paramvalue。这道题中代码用REQUEST_URI来检查是否包含show_source字符串。这里有个关键点REQUEST_URI不会像PHP_SELF那样被URL解码。也就是说如果你用URL编码的特殊字符它们在REQUEST_URI中会保持原样而在PHP_SELF中可能会被解码。我做过一个测试访问http://example.com/index.php/%61%62%63时PHP_SELF会显示/index.php/abc解码后REQUEST_URI则保持/index.php/%61%62%63这个差异在后面绕过正则检查时非常重要。3. basename()函数的安全隐患3.1 basename的基本行为basename()函数本意是用来获取路径中的文件名部分。比如echo basename(/var/www/html/index.php); // 输出index.php看起来很简单对吧但它的实际行为在某些情况下会很危险。特别是当路径中包含特殊字符时basename()的输出可能出乎意料。我在本地测试时发现如果你传入/var/www/html/utils.php/../index.phpbasename()会返回index.php因为它只关心最后一个斜杠后的内容。但更关键的是它对非ASCII字符的处理方式。3.2 利用非ASCII字符绕过检查这道题的关键突破点就在于basename()对非ASCII字符的处理。当路径中包含某些特殊字符特别是非ASCII字符时basename()会把这些字符原样保留而正则表达式可能无法正确匹配这些字符。举个例子$path /index.php/utils.php/我; echo basename($path); // 输出我更妙的是即使这些特殊字符是URL编码形式的basename()也能正确处理。这就为我们绕过正则检查提供了可能。因为我们可以构造一个URL使得正则检查时路径看起来不像utils.php但经过basename()处理后又变成了utils.php3.3 实际利用方式在本题中我们可以构造这样的URL/index.php/utils.php/%88?show[source1这里%88是一个关键。当这个URL被处理时$_SERVER[PHP_SELF]会变成/index.php/utils.php/加上解码后的%88一个非ASCII字符正则表达式/utils\.php\/*$/i在匹配时因为末尾有非ASCII字符匹配失败但basename()处理时会忽略index.php/部分返回utils.php这样就完美绕过了所有检查成功让highlight_file()读取到utils.php。4. 正则表达式的绕过技巧4.1 分析题目中的正则防护这道题设置了两道正则防线if (preg_match(/utils\.php\/*$/i, $_SERVER[PHP_SELF])) { exit(hacker :)); } if (preg_match(/show_source/, $_SERVER[REQUEST_URI])){ exit(hacker :)); }第一个正则检查PHP_SELF是否以utils.php结尾第二个检查REQUEST_URI是否包含show_source字符串。我们的目标是要同时绕过这两个检查。4.2 非ASCII字符绕过第一个正则对于第一个正则/utils\.php\/*$/i关键点在于$这个元字符。它表示匹配字符串的结尾。但是当字符串末尾有非ASCII字符时这个匹配可能会失败。我测试发现PHP的正规表达式引擎在处理包含非ASCII字符的字符串时$的行为有时会不一致。特别是当这些字符是URL编码形式时。因此在utils.php后面加上一个非ASCII字符如%88就能让正则匹配失败。4.3 特殊字符绕过第二个正则第二个正则/show_source/看起来简单但也有绕过的方法。在正则表达式中点号(.)默认匹配任何字符除了换行符。所以我们可以用show.source或者show[source来绕过。更妙的是由于这个检查是针对REQUEST_URI的而REQUEST_URI不会自动URL解码所以我们可以直接在URL中使用这些特殊字符不需要编码。这就是为什么我们的payload中使用show[source能成功绕过检查。5. 完整攻击链的构建5.1 攻击步骤拆解现在我们把所有知识点串起来看看完整的攻击是如何进行的构造特殊URL/index.php/utils.php/%88?show[source1服务器接收到请求后$_SERVER[PHP_SELF]值为/index.php/utils.php/加解码后的%88$_SERVER[REQUEST_URI]值为/index.php/utils.php/%88?show[source1正则检查第一个正则检查PHP_SELF因为末尾有非ASCII字符匹配失败第二个正则检查REQUEST_URI因为使用的是show[source而不是show_source匹配失败由于GET参数中有show_source执行highlight_file(basename($_SERVER[PHP_SELF]))basename()处理/index.php/utils.php/加特殊字符返回utils.php最终highlight_file(utils.php)被执行读取到flag5.2 防御措施建议从这道题中我们可以学到几个重要的PHP安全实践不要依赖basename()来做安全校验因为它容易被特殊字符绕过使用正则表达式检查路径时要考虑非ASCII字符的影响$_SERVER的不同变量有不同特性安全检查时要选择正确的变量对于关键安全检查最好使用多重验证机制我在实际开发中就遇到过类似问题。有一次我们的系统检查文件上传路径时只用了basename()结果被攻击者用包含特殊字符的路径绕过了检查。从那以后我们都会对用户输入进行多重验证和过滤。6. 深入理解PHP特性与安全6.1 PHP_SELF与REQUEST_URI的深入比较为了更清楚地理解这两个变量的区别我做了一系列测试URLPHP_SELFREQUEST_URI/index.php/index.php/index.php/index.php/test/index.php/test/index.php/test/index.php/test?param1/index.php/test/index.php/test?param1/index.php/%74%65%73%74/index.php/test/index.php/%74%65%73%74/index.php/我/index.php/我/index.php/%E6%88%91从表中可以看出PHP_SELF会自动URL解码而REQUEST_URI保持原样。此外PHP_SELF不包含查询字符串而REQUEST_URI包含。6.2 basename()的更多陷阱basename()还有一些其他需要注意的行为它会去掉末尾的斜杠echo basename(/path/to/file/); // 输出file它对非ASCII字符的处理可能因系统locale设置而异它不能识别真实的文件系统路径只是简单的字符串处理我曾经遇到过一个案例攻击者上传了一个名为evil.php\x00.jpg的文件然后利用basename()的特性最终执行了PHP代码。这说明在安全敏感的上下文中不能单纯依赖basename()。6.3 正则表达式的强化写法为了防止类似的绕过我们可以强化正则表达式的写法使用\A和\z代替^和$它们是真正的字符串开头和结尾锚点明确指定字符集if (preg_match(/utils\.php[\/]*\z/i, $_SERVER[PHP_SELF])) { // 更严格的匹配 }对输入先进行规范化处理如URL解码、去除非法字符等在实际项目中我通常会先对输入进行标准化处理然后再进行安全检查这样可以避免很多因解析差异导致的安全问题。7. 从CTF到真实世界的安全思考这道CTF题目虽然设计精巧但它反映出的安全问题在真实Web应用中也很常见。很多PHP应用都会用basename()来处理上传文件名用$_SERVER变量来做路由或安全检查但却没有充分考虑到这些函数的边缘情况。我在审计真实项目时就发现过几个类似的漏洞一个CMS系统用basename()来检查上传文件的后缀名结果被特殊字符绕过一个框架用PHP_SELF来做路由分发导致可以注入任意路径一个API系统用简单的正则检查访问权限被精心构造的URL绕过这些案例都说明理解PHP的这些底层特性对于编写安全的代码至关重要。作为开发者我们不仅要让代码能工作还要考虑各种可能的边界情况和恶意输入。

更多文章