详解PHP反序列化漏洞

Posted H3rmesk1t

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解PHP反序列化漏洞相关的知识,希望对你有一定的参考价值。

序列化与反序列化

定义

序列化(串行化):是将变量转换为可保存或传输的字符串的过程;
反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用;
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性;
常见的php序列化和反序列化方式主要有:serialize,unserialize

在这里插入图片描述
在这里插入图片描述

常见使用情况

serialize和unserialize函数

<?php
class Dino{
	public $name = 'H3rmesk1t';
	public $way = 'Web_Misc_Crypto';
}
$a = new Dino();
$a = serialize($a);
print_r($a);

$H3rmesk1t = array('a' => 'Apple', 'b' => 'Banana', 'c' => 'Cocount');
$m = serialize($H3rmesk1t);
echo $m;
$n = unserialize($m);
print_r($n);
?>
[1]输出:
O:4:"Dino":2:{s:4:"name";s:9:"H3rmesk1t";s:3:"way";s:15:"Web_Misc_Crypto";}
a:3:{s:1:"a";s:5:"Apple";s:1:"b";s:6:"Banana";s:1:"c";s:7:"Cocount";}
Array
(
    [a] => Apple
    [b] => Banana
    [c] => Cocount
)

[2]解释:
O:对象
4:对象长度
Dino:对象名
2:属性个数
s:字符串
9:该属性名称长度
name:该属性名
H3rmesk1t:该属性的值
...

a:数组
3:三个属性
s:字符串
1:长度
...

[3]补充:
注意点:当访问控制修饰符(publicprotectedprivate)不同时,序列化后的结果也不同
public          被序列化的时候属性名 不会更改
protected       被序列化的时候属性名 会变成  %00*%00属性名
private         被序列化的时候属性名 会变成  %00类名%00属性名

常见的序列化格式

  1. 二进制格式
  2. 字节数组
  3. json字符串
  4. xml字符串

反序列化中常见的魔术方法

  1. __construct(),类的构造函数
  2. __destruct(),类的析构函数
  3. __call(),在对象中调用一个不可访问方法时调用
  4. __callStatic(),用静态方式中调用一个不可访问方法时调用
  5. __get(),获得一个类的成员变量时调用
  6. __set(),设置一个类的成员变量时调用
  7. __isset(),当对不可访问属性调用isset()或empty()时调用
  8. __unset(),当对不可访问属性调用unset()时被调用
  9. __sleep(),执行serialize()时,先会调用这个函数
  10. __wakeup(),执行unserialize()时,先会调用这个函数
  11. __toString(),类被当成字符串时的回应方法
  12. __invoke(),调用函数的方式调用一个对象时的回应方法
  13. __set_state(),调用var_export()导出类时,此静态方法会被调用
  14. __clone(),当对象复制完成时调用
  15. __autoload(),尝试加载未定义的类
  16. __debugInfo(),打印所需调试信息

魔术方法详解

反序列化绕过

protected和private绕过

在这里插入图片描述

如果变量前是protected,则是\\x00*\\x00类名的形式
如果变量前是private,则是\\x00类名\\x00的形式

绕过:
①:php7.1+反序列化对类属性不敏感,将protected改成public
②:手动将序列化后的形式改为protected或者private的标准形式,结合urlencode和base64编码进行操作

__wakeup绕过(CVE-2016-7124)

利用版本:
PHP5 < 5.6.25、​ PHP7 < 7.0.10
原理:
当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行
示例:
O:4:"Dino":1:{s:1:"a";s:4:"misc";}改为O:4:"Dino":2:{s:1:"a";s:4:"misc";}

引用

在这里插入图片描述

通过值的引用可以使$a的值与$b的值相等

利用16进制绕过字符过滤

在这里插入图片描述

序列化结果:O:4:"Dino":1:{s:3:"way";s:3:"web";}中含有字符web,但将s改成S后,O:4:"Dino":1:{S:3:"\\\\77ay";s:3:"web";}利用十六进制绕过了字符的过滤检测

同名方法的利用

示例源码

在这里插入图片描述

POP链

在这里插入图片描述

POP链利用

在这里插入图片描述

绕过部分正则

preg_match(’/^O:\\d+/’)匹配序列化字符串是否是对象字符串开头

  • 利用加号绕过(注意在url里传参时+要编码为%2B)
  • serialize(array(a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
preg_match('/[oc]:\\d+:/i', $var)

O:4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}
O:+4:"Demo":1:{s:10:"Demofile";s:16:"f15g_1s_here.php";}

unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

字符逃逸

PHP在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,当序列化的长度不对应的时候会出现报错
字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少

字符增多

  • 正常情况
<?php
function up($str){
    return str_replace("x","xx",$str);
}
class D1no{
    public $name = 'H3rmesk1t';
    public $way = 'Web_Crypto_Misc';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(up(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:9:"H3rmesk1t";s:3:"way";s:15:"Web_Crypto_Misc";}
过滤前
D1no Object
(
    [name] => H3rmesk1t
    [way] => Web_Crypto_Misc
)
过滤后
D1no Object
(
    [name] => H3rmesk1t
    [way] => Web_Crypto_Misc
)
  • 参数name多传入一个x导致溢出导致反序列化失败
<?php
function up($str){
    return str_replace("x","xx",$str);
}
class D1no{
    public $name = 'H3rmesk1tx';
    public $way = 'Web_Crypto_Misc';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(up(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:10:"H3rmesk1tx";s:3:"way";s:15:"Web_Crypto_Misc";}
过滤前
D1no Object
(
    [name] => H3rmesk1tx
    [way] => Web_Crypto_Misc
)
过滤后
  • 字符串逃逸实现
    将name的值设置为H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:4:"door";s:7:"Hacker!";}";s:4:"door";s:7:"Hacker!";}部分一共28个字符,由于我们定义的up函数将一个x替换成两个xx,所以name参数中的28个x将被替换成56个x,多出来的28个x取代了name参数中的";s:4:"door";s:7:"Hacker!";},从而";s:4:"door";s:7:"Hacker!";}可以溢出,"闭合了前串,使得我们填写的而已字符串成功逃逸并执行反序列化操作,参数way被替换成Hacker!
<?php
function up($str){
    return str_replace("x","xx",$str);
}
class D1no{
    public $name = 'H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"way";s:7:"Hacker!";}';
    public $way = 'Web_Crypto_Misc';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(up(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:63:"H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"way";s:7:"Hacker!";}";s:3:"way";s:15:"Web_Crypto_Misc";}
过滤前
D1no Object
(
    [name] => H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"way";s:7:"Hacker!";}
    [way] => Web_Crypto_Misc
)
过滤后
D1no Object
(
    [name] => H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    [way] => Hacker!
)

字符减少

  • 正常情况
<?php
function down($str){
    return str_replace("xx","x",$str);
}
class D1no{
    public $name = 'H3rmesk1t';
    public $way = 'Web_Crypto_Misc';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(down(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:9:"H3rmesk1t";s:3:"way";s:15:"Web_Crypto_Misc";}
过滤前
D1no Object
(
    [name] => H3rmesk1t
    [way] => Web_Crypto_Misc
)
过滤后
D1no Object
(
    [name] => H3rmesk1t
    [way] => Web_Crypto_Misc
)
  • 参数name少传入一个x导致溢出导致反序列化失败
<?php
function down($str){
    return str_replace("xx","x",$str);
}
class D1no{
    public $name = 'H3rmesk1txx';
    public $way = 'Web_Crypto_Misc';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(down(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:11:"H3rmesk1txx";s:3:"way";s:15:"Web_Crypto_Misc";}
过滤前
D1no Object
(
    [name] => H3rmesk1txx
    [way] => Web_Crypto_Misc
)
过滤后
  • 字符串逃逸实现
    由于xx会被替换成x,所以我们输出的66个x会变成33个x,由于";s:3:"way";s:15:"Web_Crypto_Misc部分一共33个字符,所以它会被参数name吃进去当成它的属性值,而我们写入的恶意字符串";s:3:"way";s:7:"Hacker!";}就能够正常的解析并执行反序列化操作
<?php
function up($str){
    return str_replace("xx","x",$str);
}
class D1no{
    public $name = 'H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
    public $way = 'Web_Crypto_Misc";s:3:"way";s:7:"Hacker!";}';
}

echo serialize(new D1no())."\\n";
echo "过滤前"."\\n";
$c = unserialize((serialize(new D1no())));
print_r($c)."\\n";
echo "过滤后"."\\n";
$c = unserialize(up(serialize(new D1no())));
print_r($c);
?>
=>
O:4:"D1no":2:{s:4:"name";s:75:"H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"way";s:42:"Web_Crypto_Misc";s:3:"way";s:7:"Hacker!";}";}
过滤前
D1no Object
(
    [name] => H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    [way] => Web_Crypto_Misc";s:3:"way";s:7:"Hacker!";}
)
过滤后
D1no Object
(
    [name] => H3rmesk1txxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:3:"way";s:42:"Web_Crypto_Misc
    [way] => Hacker!
)

以上是关于详解PHP反序列化漏洞的主要内容,如果未能解决你的问题,请参考以下文章

反序列化漏洞-02PHP反序列化漏洞实验详解

反序列化漏洞详解

反序列化漏洞详解

php代码审计9审计反序列化漏洞

15-PHP代码审计——yii 2.0.37反序列化漏洞

15-PHP代码审计——yii 2.0.37反序列化漏洞