当前位置: 首页 > news >正文

从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?param=value时,REQUEST_URI的值就是/index.php/test?param=value

这道题中,代码用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,使得:

  1. 正则检查时,路径看起来不像utils.php
  2. 但经过basename()处理后,又变成了utils.php

3.3 实际利用方式

在本题中,我们可以构造这样的URL:

/index.php/utils.php/%88?show[source=1

这里%88是一个关键。当这个URL被处理时:

  1. $_SERVER['PHP_SELF']会变成/index.php/utils.php/加上解码后的%88(一个非ASCII字符)
  2. 正则表达式/utils\.php\/*$/i在匹配时,因为末尾有非ASCII字符,匹配失败
  3. 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 攻击步骤拆解

现在我们把所有知识点串起来,看看完整的攻击是如何进行的:

  1. 构造特殊URL:/index.php/utils.php/%88?show[source=1
  2. 服务器接收到请求后:
    • $_SERVER['PHP_SELF']值为/index.php/utils.php/加解码后的%88
    • $_SERVER['REQUEST_URI']值为/index.php/utils.php/%88?show[source=1
  3. 正则检查:
    • 第一个正则检查PHP_SELF,因为末尾有非ASCII字符,匹配失败
    • 第二个正则检查REQUEST_URI,因为使用的是show[source而不是show_source,匹配失败
  4. 由于GET参数中有show_source,执行highlight_file(basename($_SERVER['PHP_SELF']))
    • basename()处理/index.php/utils.php/加特殊字符,返回utils.php
  5. 最终highlight_file('utils.php')被执行,读取到flag

5.2 防御措施建议

从这道题中,我们可以学到几个重要的PHP安全实践:

  1. 不要依赖basename()来做安全校验,因为它容易被特殊字符绕过
  2. 使用正则表达式检查路径时,要考虑非ASCII字符的影响
  3. $_SERVER的不同变量有不同特性,安全检查时要选择正确的变量
  4. 对于关键安全检查,最好使用多重验证机制

我在实际开发中就遇到过类似问题。有一次我们的系统检查文件上传路径时只用了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?param=1/index.php/test/index.php/test?param=1
/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()还有一些其他需要注意的行为:

  1. 它会去掉末尾的斜杠:
    echo basename('/path/to/file/'); // 输出file
  2. 它对非ASCII字符的处理可能因系统locale设置而异
  3. 它不能识别真实的文件系统路径,只是简单的字符串处理

我曾经遇到过一个案例:攻击者上传了一个名为evil.php\x00.jpg的文件,然后利用basename()的特性,最终执行了PHP代码。这说明在安全敏感的上下文中,不能单纯依赖basename()

6.3 正则表达式的强化写法

为了防止类似的绕过,我们可以强化正则表达式的写法:

  1. 使用\A\z代替^$,它们是真正的字符串开头和结尾锚点
  2. 明确指定字符集:
    if (preg_match('/utils\.php[\/]*\z/i', $_SERVER['PHP_SELF'])) { // 更严格的匹配 }
  3. 对输入先进行规范化处理(如URL解码、去除非法字符等)

在实际项目中,我通常会先对输入进行标准化处理,然后再进行安全检查,这样可以避免很多因解析差异导致的安全问题。

7. 从CTF到真实世界的安全思考

这道CTF题目虽然设计精巧,但它反映出的安全问题在真实Web应用中也很常见。很多PHP应用都会用basename()来处理上传文件名,用$_SERVER变量来做路由或安全检查,但却没有充分考虑到这些函数的边缘情况。

我在审计真实项目时,就发现过几个类似的漏洞:

  1. 一个CMS系统用basename()来检查上传文件的后缀名,结果被特殊字符绕过
  2. 一个框架用PHP_SELF来做路由分发,导致可以注入任意路径
  3. 一个API系统用简单的正则检查访问权限,被精心构造的URL绕过

这些案例都说明,理解PHP的这些底层特性对于编写安全的代码至关重要。作为开发者,我们不仅要让代码能工作,还要考虑各种可能的边界情况和恶意输入。

http://www.jsqmd.com/news/661884/

相关文章:

  • 2026天津协议离婚vs诉讼离婚律所测评!快速办结+权益保障指南 - 速递信息
  • 别再手动敲AT指令了!用正点原子官方软件搞定以太网转串口模块配置(附静态IP设置避坑点)
  • 如何在Chrome浏览器中实现一键画中画视频播放:终极免费扩展指南
  • Python中的常用函数使用及说明
  • 神经网络遗传算法函数极值寻优(非线性函数极值)
  • Attention U-Net:让模型学会“看”哪里
  • 从零开始构建SaaS多租户架构:SpringBoot + MyBatis-Plus动态数据源实战
  • 用Java Stream一行代码搞定彩票随机选号(双色球/大乐透)
  • Mysql--基础知识点--102--redo log内容
  • Kubernetes资源配额实战:LimitRange配置指南
  • PINN实战:从零构建一个偏微分方程求解器
  • 海洋CMS资源接口实战:XML数据格式与API调用详解
  • STM32 FOC电机库PID调参避坑指南:为什么你的定点参数调不好?
  • 邢台脱发白发理疗养发馆哪家好?黑奥秘参与行业标准制定,专业有据可依 - 美业信息观察
  • AMD平台ESXI 7.0实战:避坑部署Win11与TrueNAS虚拟化存储方案
  • Flask-Admin进阶指南:从基础增删改查到自定义视图和权限控制的完整配置流程
  • 从入门到实战:在UniApp中高效集成uCharts图表(组件与原生双模式详解)
  • 大模型应用开发实战(19)——Andrej Karpathy Skills 为什么突然火了?一份 CLAUDE.md,把 Claude Code 从“会写”拉回“会做事”
  • 2026年团鱼脚鱼甲鱼养殖基地推荐:中华鳖老鳖水鱼专业供应与回收服务选型指南 - 品牌推荐官
  • ContextMenuManager:Windows右键菜单终极解决方案,3个核心功能重塑你的操作效率
  • 别再傻傻地直接扫了!手把手教你用wafw00f在Windows和Kali上优雅地“试探”网站防火墙
  • Intel RealSense D435i数据采集避坑指南:Python脚本获取相机内参、外参并同步保存多传感器图像
  • TMSpeech:Windows本地实时语音识别工具终极配置指南
  • 2026年台式净饮机推荐:碧云泉G7S万相凭实力问鼎年度榜首 - 品牌企业推荐师(官方)
  • 设计模式系列目录
  • 如何用Open-Lyrics实现AI字幕生成与语音翻译:3步完成多语言转换
  • Mysql--基础知识点--101--在线扩容
  • 给企业开发者的MFI指南:当你的App需要连接Honeywell扫描枪时,info.plist和PPID该怎么填?
  • Infinity图像合成实战:如何用比特级建模提升你的AI画质(附GitHub代码)
  • 【技术解析】SwAV:用在线聚类与最优运输破解无监督视觉特征学习难题