RCE Laravel5.7 deserialization code audit analysis of recurring 2019-9081 CVE-

This article first appeared to the prophet community: https: //xz.aliyun.com/t/5510

surroundings:

php7.2+apache+laravel5.7

Vulnerability Description:

Laravel Framework is Taylor Otwell software developers to develop a Web-based application development framework for PHP. Illuminate is one component. Deserialization vulnerability exists Laravel Framework 5.7.x versions Illuminate components, a remote attacker could exploit this vulnerability to execute code.

The following secondary development point of vulnerability hypothesis:

    public function index()

    {
        if(isset($_GET['code']))
        {
            $code = $_GET['code'];

            unserialize($code);
            return "2333";
        }
    }

exp.php:

Folder on the public execution

<?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('id'),$genericuser,$application);
    echo urlencode(serialize($pendingcommand));
}

?>

Attack effect:

 Analysis process:

First, because in laravel5.7 core package inside, then take a look at github 5.7 relative to 5.6 which adds things:

Which can be seen in more than 5.7 of a

For newly added files, you can explain to understand the role of the document by api function official document, which defines the class PendingCommand

Wherein deserializing method has __destruct (), this calls the destructor run () method:

 

 This method can be found and used to execute commands in its annotation, then the idea to run execution command to invoke the method by way of example of such an object is deserialized achieve the rce. When a class of research, we must first look at the member variables associated with:

 

 In its constructor, there are four key variables:

$this->app;         //一个实例化的类 Illuminate\Foundation\Application
$this->test;        //一个实例化的类 Illuminate\Auth\GenericUser
$this->command;     //要执行的php函数 system
$this->parameters;  //要执行的php函数的参数  array('id')

 断点跟踪分析:

首先在反序列化方法unserialize()处下断点,执行exp生成的payload后将停在此处,此时F7进入unserialize函数进行分析:

按道理说反序列化的下一步接下来就是触发__destruct函数,那么我们继续F7,可以在左下方的函数调用栈中发现出现了两处调用,

首先调用spl_autoload_call()方法

因为我们在payload中使用的类在Task控制器中并没有加载进来,因此便触发了PHP的自动加载的功能,也就是实现了 lazy loading,以加载类PendingCommand为例进行分析(其它所用到的类加载方式相同):关于PHP自动加载的相关描述可以参考

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

   首先是类AliasLoadder中load方法的调用,其中涉及到使用Laravel框架所带有的Facade功能去尝试加载我们payload中所需要的类,这里的判断的逻辑主要是有2条:

1.用户提供所要加载的类是不是其中包含"Facades",如果是则通过loadFacade()函数进行加载
2.在Illuminate\Support\Facades命名空间中寻找是否含有用户所要加载的类

 

如果通过load()方法没有加载成功,则会调用loadclass()函数进行加载,而loadclass()函数中通过调用findfile()函数去尝试通过Laravel中的composer的自动加载功能含有的classmap去尝试寻找要加载的类所对应的类文件位置,此时将会加载vendor目录中所有组件, 并生成namespace + classname的一个 key => value 的 php 数组来对所包含的文件来进行一个匹配:

 

找到类PendingCommand所对应的文件后,将通过includeFile()函数进行包含,从而完成类PendingCommand的整个加载流程

 加载完所需要的类后,将进入__destruct方法,此时hasExecuted属性默认为false,即还没有执行命令,所以此时才能调用run方法:

继续使用F7进入用于执行命令的run()函数进行分析:

在run方法中,首先要调用mockConsoleOutput()方法,该方法主要用于模拟应用程序的控制台输出,此时因为要加载类Mockery和类Arrayinput,所以又要通过spl_autoload_call->load->loadclass加载所需要的类,并且此时又会调用createABufferedOutputMock()函数

按F7进入createABufferedOutputMock观察一下其内部的实现,其中又调用了Mockery的mock()函数,此时继续F7进入mock函数,进入以后直接F8单步执行即可,我们的目的只需要此段代码能够往下执行,在调试的时候我们并不一定要搞清每个变量每个函数的作用,调用链实在是太长太复杂,并且只要它不出错就行

Mockery是一个简单而灵活的PHP模拟对象框架,在 Laravel 应用程序测试中,我们可能希望「模拟」应用程序的某些功能的行为,从而避免该部分在测试中真正执行

接下来是exp构造的第一个亮点:

此时在createABufferedOutputMock()方法中要进入for循环,并且在其中要调用tes对象的expectedOutput属性,然而在可以实例化的类中不存在expectedOutput属性(通过ctrl+shift+F即可进行全局搜索),只在一些测试类中存在

所以这里要用到php的一个小trick,也是经常在ctf题中可能遇到的,当访问不存在的属性时会触发__get()方法,通过去触发__get()方法去进一步构造pop链,而在Illuminate\Auth\GenericUser的__get方法中存在:

而此时$this->test是Illuminate\Auth\GenericUser的实例化对象,其是我们传入的,那么其是可以控制的,即attributes属性也是我们可以控制的,那当发生$this->test->expectedOutput的调用时,我们只需要让attributes中存在键名为expectedOutput的数组,即数组中有内容就能够通过循环流程进行返回,继续F8单步执行即可跳出createABufferedOutputMock()方法

此时回到mockConsoleOutput()函数中,又进行了一个循环遍历,调用了test对象的的expectedQuestions属性,里面的循环体与createABufferedOutputMock()函数的循环体相同,因此绕过方法也是通过调用__get()方法,设置一个键名为expectedQuestions的数组即可,此时将继续往下走,继续F8单步调试就可以return $mock,从而走出mockConsoleOutput()函数。接下来回到run函数中,此时到了触发rce的关键点,也就是exp构造的第二个关键点:

 

 

其中出现了$this->app[Kernel::class]->call方法的调用,其中Kernel::class在这里是一个固定值Illuminate\Contracts\Console\Kernel,并且call的参数为我们所要执行的命令和命令参数($this->command, $this->parameters),那我们此时需要弄清$this->app[Kernal::class]返回的是哪个类的对象,使用F7步入程序内部进行分析

 

直到得到以下的调用栈,此时继续F8单步执行到利用payload的语句,此时因为$this为Illuminate\Foundation\Application,bindings属性是Container类的,而这里也是pauload中选择Applocation作为app参数值的原因,那么通过反序列化我们可以控制bindings属性,而此时$abstract为固定值,即只需要让$bindings为一个二维数组,其中键$abstract作为数组,其中存在键名为concrete,键值为我们想要实例化的类Application即可

此时继续F8往下走,到了实例化Application类的时刻, 此时要满足isBuildable函数才可以进行build,因此F7步入查看

此时$concrete为Application,而$abstract为kernal,显然不满足,并且||右边$concrete明显不是闭包类的实例化,所以此时不满足Application实例化条件,此时继续F7,此时将会调用make函数,并且此时将$abstract赋值为了Application,并且make函数又调用了resolve函数,即实现了第二次调用isBuildable()函数判断是否可以进行实例化,即此时已经可以成功实例化类Application,即完成了$this->app[Kernel::class]为Application对象的转化

接下来将调用类Application中的call方法,即其父类Container中的call方法

 

 

其中第一个分支isCallableWithAtSign()判断回调函数是否为字符串并且其中含有"@“,并且$defaultMethod默认为null,显然此时不满足if条件,即进入第二个分支,callBoundMethod()函数的调用

 在callBoundMethod()函数中将调用call_user_func_array()函数来执行最终的命令,首先$callback为”system“,参数为静态方法getMethodDependencies()函数的返回值,F7步入看看

在return处可以看到此时调用array_merge函数将$dependencies数组和$parameters数组进行合并,但是$dependencies数组为空,因此对我们要执行命令的参数不产生影响,即在此步返回将执行命令,即完成

call_user_func_array('system',array('id'))

 此时run函数中$exitcode值即为命令的执行结果

 

 参考:

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

https://learnku.com/docs/laravel/5.7/facades/2251

https://learnku.com/articles/12575/deep-analysis-of-the-laravel-service-container

https://laravel.com/docs/5.7/structure#the-tests-directory

Guess you like

Origin www.cnblogs.com/wfzWebSecuity/p/11079354.html