PHP反序列化漏洞(上)

阅读需知:本文从作者主观意见出发,如有不合适的地方,如:文章内容错误、文章排版混乱、逻辑思维混乱,请第一时间在文章评论区留言。

今天👴来写PHP反序列化漏洞,PHP作为👴认为的(世界上最好的语言),必定存在着世界上最好的漏洞

请注意

这是一篇没有(下)的文章,Q:为什么没有? A:因为我懒

什么是序列化

PHP序列化是将PHP数据结构转换为字符串的过程,以便在不同系统之间传输或存储数据。序列化后的字符串包含了变量的值和类型信息,可以通过网络传输或保存到文件中,之后可以通过反序列化将字符串还原成原始的PHP数据结构。

这么说可能有点抽象,让👴来举个例子。

你所需要知道的函数

在PHP中,privatepublicprotected是类的成员(属性和方法)的可见性关键字。

  • public(公有):公有成员可以在任何地方被访问,包括类的外部。
  • protected(受保护):受保护成员只能在类本身和子类中被访问。
  • private(私有):私有成员只能在类本身中被访问。

在序列化后,这三种关键字会序列化后会加上%00和一些关键字,具体加法分别对应如下

1
2
3
4
5
6
7
8
<?php
class Example{
public $a;
private $b;
protected $c;
}

echo urlencode(serialize(new Example()));
  • public:"a"
  • private:"%00Example%00b"
  • protected:"%00*%00c"

(自行理解)

在PHP反序列化漏洞时,如果没注意到类型的可见性的话,那很有可能造成不必要的失误

1
2
3
4
5
6
7
<?php
class DingZhen{
public $oneFive;
}

$a = new DingZhen();
echo serialize($a);

什么是反序列化

正如上面所说的,PHP序列化后会返回一串数据,那反序列化就是将序列化的数据反转回PHP对象或数组的过程

上面那个小程序,运行后会返回

1
O:8:"DingZhen":1:{s:7:"oneFive";N;}

我们可以使用 unserialize(); 这个函数来进行读取

img官方给出的警告

我们重写上面那个程序,在里边添加一个 smoke();__construct() 方法(__construct待会会说干什么用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class DingZhen{
public $oneFive;
public function __construct($oneFive)
{
$this->oneFive = $oneFive;
}
public function smoke(){
return $this->oneFive . ": I got smoke.";
}
}

$a = new DingZhen("丁真");
echo serialize($a);

这时候重新序列化

1
O:8:"DingZhen":1:{s:7:"oneFive";s:6:"丁真";}

然后我们把这串数据这样子封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class DingZhen
{
public $oneFive;

public function __construct($oneFive)
{
$this->oneFive = $oneFive;
}

public function smoke()
{
return $this->oneFive . ": I got smoke.";
}
}

$a = new DingZhen("丁真");
$b = serialize($a);
$c = unserialize($b);
echo $c->smoke();

这样就完成了一个反序列化过程

img

常见的序列化形式

  • serialize()
  • unserialize()
  • json_encode()
  • json_decode()
  • 数组形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
// serialize
$data = array('a', 'b', 'c');
$serialized_data = serialize($data);
echo $serialized_data . "\n"; // 输出: a:3:{i:0;s:1:"a";i:1;s:1:"b";i:2;s:1:"c";}

// unserialize
$unserialized_data = unserialize($serialized_data);
print_r($unserialized_data) . "\n"; // 输出: Array ( [0] => a [1] => b [2] => c )

// json_encode
$data = array('a', 'b', 'c');
$json_encoded_data = json_encode($data);
echo $json_encoded_data . "\n"; // 输出: ["a","b","c"]

// json_decode
$json_decoded_data = json_decode($json_encoded_data);
print_r($json_decoded_data) . "\n"; // 输出: Array ( [0] => a [1] => b [2] => c )

什么是魔术方法

建议看官方的,官方写的很细

PHP: 魔术方法 - Manual

https://www.php.net/manual/zh/language.oop5.magic.php

  • __call 调用不可访问或不存在的方法时被调用
  • __callStatic 调用不可访问或不存在的静态方法时被调用
  • __clone 进行对象clone时被调用,用来调整对象的克隆行为
  • __constuct 构建对象的时被调用;
  • __debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
  • __destruct 明确销毁对象或脚本结束时被调用;
  • __get 读取不可访问或不存在属性时被调用
  • __invoke 当以函数方式调用对象时被调用
  • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用
  • __set 当给不可访问或不存在属性赋值时被调用
  • __set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
  • __sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
  • __toString 当一个类被转换成字符串时被调用
  • __unset 对不可访问或不存在的属性进行unset时被调用
  • __wakeup 当使用unserialize时被调用,可用于做些对象的初始化操作

那么问题来了,为什么会产生反序列化漏洞?

说好听点,方法套娃,说难听点,就是攻击者将恶意的序列化数据发送到应用程序,然后应用程序在反序列化过程中解析并执行了攻击者预期的恶意代码。具体来说,PHP反序列化漏洞常常出现在应用程序接收用户输入数据的地方,如请求参数、Cookie、HTTP头等。攻击者可以构造恶意的序列化数据,并将其伪装成正常的请求数据发送给应用程序。当应用程序使用反序列化函数(如unserialize())解析接收到的数据时,攻击者构造的恶意代码就会被执行,从而导致应用程序的漏洞。例如,攻击者可以通过构造恶意的序列化数据来执行任意的系统命令、读取或写入任意文件、修改数据库记录等操作,从而导致应用程序的安全性受到威胁。

一般反序列化漏洞的入口点都是从 __wakeup()__destruct()__toString()__invoke(),开始看起,举几个例子。

__wakeup()

当使用unserialize时被调用,可用于做些对象的初始化操作(unserialize触发)

继续修改上面的代码,我们添加一个 __wakeup() 方法

1
2
3
4
public function __wakeup(){
// 实际开发别这样写
exec($this->oneFive);
}

如果我们没有对 __construct 中的 $oneFive 变量做过滤的话,unserialize在执行完后时会自动调用__wakeup()的,所以__wakeup()一般在赛场上做过滤(可以绕过),实际开发应该用于对象反序列化后对其状态进行恢复

img

接下来我们在__wakeup()里加入一些过滤方法,来看看怎么利用__wakeup()函数失效(CVE-2016-7124)来绕过这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DingZhen
{
public $oneFive;

public function __construct($oneFive)
{
$this->oneFive = $oneFive;
}

public function __destruct()
{
echo exec($this->oneFive) . ": I got smoke.";
}

public function __wakeup(){
// 实际开发别这样写
if (preg_match("/\b(exec|system)\b/i", "", $this->oneFive)){
echo $this->oneFive;
}
}
}

将序列化后的数据的参数数量+1即可

img

加了一后,正常运行

img

__sleep()

serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。(serialize)

注意:__sleep()只能返回数组

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

class Example {
public function __sleep() {
return ['data'];
}
}

$a = new Example();
$a->data = 'flag';
$b = serialize($a);
echo $b;

很好理解

__destruct()

__destruct 函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class DingZhen
{
public $oneFive;

public function __construct($oneFive)
{
$this->oneFive = $oneFive;
}
public function session1(){
echo "1\n";
}
public function __destruct()
{
echo "Done!";
}
}

$a = new DingZhen("ls");
$a->session1();

很好理解

img

__toString()

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

很好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Example {
public $data;
public function __construct($data){
$this->data = data;
}
public function __toString() {
return eval($this->data);
}
}

$a = 'O:7:"Example":1:{s:4:"data";s:10:"phpinfo();";}';
$b = unserialize($a);
$c = $b; // 注意这里
echo $c; // 注意这里

img

__invoke()

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

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

class Example {
public $data;
public function __construct($data){
$this->data = data;
}
public function __invoke(){
eval($this->data);
}
}
$a = 'O:7:"Example":1:{s:4:"data";s:10:"phpinfo();";}';
$b = unserialize($a);
$b();

非常好理解👍

img

__construct()

PHP 允许开发者在一个类中定义一个方法作为构造函数(__construct)。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

class Example{
private $a;
private $b;
private $c;
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function getAll(){
return "A: " . $this->a . "\n" .
"B: " . $this->b . "\n" .
"C: " . $this->c . "\n";
}
}
$a = new Example("我是", "理塘", "丁真");
echo $a->getAll();

很好理解,他将输出

1
2
3
A: 我是
B: 理塘
C: 丁真

__destruct()

PHP 有析构函数(__destruct)的概念,这类似于其它面向对象的语言,如 C++。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。

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

class Example{
private $a;
private $b;
private $c;
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function getAll(){
return "A: " . $this->a . "\n" .
"B: " . $this->b . "\n" .
"C: " . $this->c . "\n";
}
public function __destruct(){
$this->a = "一";
$this->b = "五";
$this->c = "!";
echo "A: " . $this->a . "\n" .
"B: " . $this->b . "\n" .
"C: " . $this->c . "\n";
}
}
$a = new Example("我是", "理塘", "丁真");
echo $a->getAll();

很好理解,这将输出

1
2
3
4
5
6
A: 我是
B: 理塘
C: 丁真
A: 一
B: 五
C: !

__call()和__callStatic()

在对象中调用一个不可访问方法时,__call() 会被调用。

在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

很好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Example{
public function __call($name, $arguments)
{
// 注意: $name 的值区分大小写
echo "Calling object method '$name' "
. implode(', ', $arguments). "\n";
}

public static function __callStatic($name, $arguments)
{
// 注意: $name 的值区分大小写
echo "Calling static method '$name' "
. implode(', ', $arguments). "\n";
}
}
$obj = new Example();
$obj->fuck('me');
Example::mother('fuck');

非常好理解,爱来自我❤

属性重载

  • 在给不可访问(protected 或 private)或不存在的属性赋值时,__set() 会被调用。
  • 读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。
  • 当对不可访问(protected 或 private)或不存在的属性调用 isset()empty() 时,__isset() 会被调用。
  • 当对不可访问(protected 或 private)或不存在的属性调用 unset() 时,__unset() 会被调用。

这一块没什么好说的,但在POP链反序列化里会比较常见,建议自己到官网看看。

PHP: 重载 - Manual

作者

IceCliffs

发布于

2021-11-27

更新于

2023-10-28

许可协议

评论