今天讲解的是PHP的Web105-108题目解析讲解
文章目录
Web105(新题型)
好样子,先看代码:
<?php
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);
?>
你还想要flag嘛?
从代码我们可以看到,与上一关的题型又不一样了:
分析代码
- 初始化:定义了两个字符串变量 $error 和 $suces
$_GET
循环:- 首先,它检查 GET 参数的键($key)是否等于字符串 ‘error’。如果是,程序会立即终止。
- 然后,它使用了一个非常关键的技巧:可变变量 (
$$key = $$value
)。这意味着,以 $key 的值为名字的那个变量,它的值将被设置为以 $value 的值为名字的那个变量的值。例如,如果你传入 GET 参数a=b
,代码就会执行$$a = $$b
,等同于 $a = $b。
$_POST
循环:- 它检查 POST 参数的值($value)是否等于字符串 ‘flag’。如果是,程序会立即终止。
- 和 GET 循环一样,它也使用了可变变量:
$$key = $$value
- 最终检查:
- 代码执行
if (!($_POST['flag'] == $flag))
: - 这个条件是核心:只有当 POST 参数中名为
flag
的值 等于$flag
变量的值时,条件才为假,程序才能继续执行,最终打印出 flag。
- 代码执行
构造Payload思路:
- 假设 flag.php 内有:
$flag = "FLAG{123}";
$error = "你还想要flag嘛?"; // 初始值
我们发送:
- GET:
?a=flag
- POST body:
error=a
脚本按顺序执行:
2. 处理 $_GET循环:GET 有 a => 'flag'
,所以执行 $$key = $$value
就是:
$a = $flag; // 把真实 flag 的值复制到 $a
现在:
$a = "FLAG{123}"
$flag = "FLAG{123}"
$error = "你还想要flag嘛?"
$_POST = array('error' => 'a') // 由 PHP 填充的 POST 初始数组
- 处理
$_POST
循环:POST 有一项key='error'
,value='a'
。先检查 if($value===‘flag’) —— 值是a
,不是flag
,不会 die。然后 $$key = $$value 即:
$error = $a;
也就是把 $a
的值(flag)赋给 $error
:
$error = "FLAG{123}" // 注意:$error 已经被覆盖成 flag
- 到比较 if(!($_POST[‘flag’] == f l a g ) ) d i e ( flag)) die( flag))die(error);
$_POST['flag']
—— 在我们的 POST 中并不存在 flag 这个字段,所以它是 NULL / 未定义(与 $flag 字符串不同)。- 因此条件为真(比较不等),脚本执行
die($error)
- 但是
$error 已在上一步被赋值成了 flag
,因此 die($error) 会把 真实 flag 输出到响应,并退出。
最终payload:
Web106
<?php
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2) && $v1!=$v2){
echo $flag;
}
}
?>
嗯?看了一下代码,怎么有种似曾相识的感觉?
这不是Web104的题型吗,不过对比了一下,还是有点小区别的:
多了对$v1!=$v2
的比较;
老样子,直接用数组绕过,payload甚至都是一样的:
# GET参数
?v2[]=1
# POST参数
v1[]=2
没什么好说的,直接下一关;
Web107
代码还是有点变化的:
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){
echo $flag;
}
}
?>
代码分析
这段 PHP 代码的目的是让用户通过一个特定的条件来获取 flag。它的逻辑步骤如下:
错误报告与文件引入: 脚本首先禁用所有错误报告 (
error_reporting(0)
),然后通过include("flag.php")
引入包含$flag
变量的文件。变量赋值:
$v1
被赋值为POST
变量v1
的值。$v3
被赋值为GET
变量v3
的值。
parse_str()
函数:parse_str()
函数会将$v1
(一个字符串) 解析成 URL 查询字符串的格式,并将解析出的变量存储到一个名为$v2
的新数组中。
- 例如,如果
$v1
的值是 “name=John&age=30”,那么 parse_str($v1, $v2) 会创建一个 $v2数组,其内容为 [‘name’ => ‘John’, ‘age’ => ‘30’]
脚本执行
if($v2['flag']==md5($v3))
。
*$v2['flag']
是从 POST变量 v1 提供的字符串中解析出来的flag
键对应的值。
* 脚本将我们提供的flag
值与 GET参数$v3 字符串的 MD5 哈希值进行比较。
方法一:PHP弱类型比较
要获取 flag,只需要需要找到一个
$v3
的值,使得md5($v3)
的结果与v2['flag']
相等。- 直接让
md5($v3)
等于字符串'flag'
是不可能的。
- 直接让
这里的漏洞在于 PHP 的 松散比较 (
==
) 和 类型转换。当 PHP 使用==
运算符比较一个 MD5 哈希(一个十六进制字符串)和一个普通字符串时,它会尝试将它们转换为同一类型。
思路:如果一个 MD5 哈希字符串以 '0e'
开头,后面跟着纯数字,PHP 在进行比较时会将其视为科学计数法表示的数字,其值为 0
。
利用这个漏洞,我们可以构造出 payload:
# GET参数
?v3=s878926199a
# POST参数
v1=flag=0
目的:为了通过PHP弱类型比较,使v3解析为0,从而与v1相等;
方法二:直接使v3的md5值等于v1
大家看到标题应该就知道我们应该怎么做了:
具体操作:
- 先选
v3 = abc
,md5(“abc”) = 900150983cd24fb0d6963f7d28e17f72。 - POST 的 v1 写成 flag=900150983cd24fb0d6963f7d28e17f72
嗯,对的就是这么朴实无华;
Web108
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');
}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}
?>
error
话不多说,直接分析代码:
好 — 我把这段 PHP 代码逐行分析并给出可直接尝试的 payload(含 curl 示例)和备选绕过方法。
代码要点(精简)
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) { die('error'); }
if (intval(strrev($_GET['c']))==0x36d) {
echo $flag;
}
ereg("^[a-zA-Z]+$", $_GET['c'])
:字符串必须从头到尾 (^ 和 $),由一个或多个(+)
大写或小写的英文字母([a-zA-Z])
组成。strrev($_GET['c'])
:把c
字符串反转。intval(...)==0x36d
:把反转后的字符串转换为整数,并与十六进制0x36d
(十进制是 877)比较。- 如果相等就输出 flag。
利用思路
0x36d
的字符串表示是 "0x36d"
—— 如果 strrev(c) === "0x36d"
,那么 intval("0x36d")
在 PHP 中会把它当作 16 进制数解析(常见 PHP 行为),结果就是 0x36d
(等于 877),比较成立,输出 flag。
因此需要:
- 让
strrev(c) == "0x36d"
,即c == strrev("0x36d") == "d63x0"
。
于是最直接的 payload 是把 c
设为 d63x0
,请求如下:
c=d63x0
解释:d63x0
反转后是 0x36d
,intval("0x36d")
→ 0x36d (877),等于 0x36d
,通过判断,输出 flag。
最终payload
尝试了一下?c=d63x0,失败了。。。
之后我又去网上查询资料:
老版本的 ereg() 函数存在 空字节注入 漏洞。
- 如果你在输入字符串中包含一个空字节(\0),ereg() 会在遇到空字节时停止处理,导致它无法匹配到字符串的结尾,因此返回 FALSE,条件成立。
- 也就是说:ereg()在处理字符串时,如果遇到 %00会提前终止匹配,导致验证绕过
那么我们是不是可以利用%00截断
来绕过?
?c=778%00
没成功。。看了一下网上WP:
# 最终payload
?c=a%00778
解释:
- 开头写个a是为了匹配正则表达式
- 然后%00截断后面的匹配。然后经过strrev以后变成了877%00a,
- 再经过intval就变成了877。
- 而877就是0x36d的十进制,所以相等。
总结
无总结。