环境
centos7
php7.1.30
laravel5.7.28
复现过程
本次漏洞点在PendingCommand
文件中,这个文件定义了PendingCommand类
,该类存在__destruct()
,在__destruct()
中调用了该类的run()
。那么就是通过反序列化触发PendingCommand类
的__destruct析构函数
,进而调用其run()
实现代码执行
根据已有的exp分析,在PendingCommand类
中需要用到的几个属性如下1
2
3
4$this->app; //一个实例化的类 Illuminate\Foundation\Application
$this->test; //一个实例化的类 Illuminate\Auth\GenericUser
$this->command; //要执行的php函数 system
$this->parameters; //要执行的php函数的参数 array('id')
调试过程
该漏洞存在`laravel组件中,因此要基于Laravel进行二次开发后可能存在此反序列化漏洞,通过qwb题目分析1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<?php
namespace App\Http\Controllers;
highlight_file(__FILE__);
class TaskController
{
public function index()
{
if(isset($_GET['code']))
{
$code = $_GET['code'];
unserialize($code);
return "Welcome to qiangwangbei!";
}
}
}
?>
- 首先在
unserialize()
处下断点,F7步入unserialize()
进行分析,在左下方的函数调用栈中发现出现了两处调用,首先调用spl_autoload_call()
(尝试所有已注册的函数来加载类),因为在payload中使用的类在Task控制器
中并没有加载进来,因此便触发了PHP的自动加载的功能(autoload 机制可以使得 PHP 程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件 include 进来,这种机制也称为 lazy loading)
加载过程
首先是类AliasLoadder
中load()
的调用,使用Laravel
框架所带有的Facade
功能去尝试加载我们payload中所需要的类。
首先提供所要加载的类是不是其中包含Facades
,如果是则通过loadFacade()
进行加载
通过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()
,这里又进行一次对象模拟。 - 继续
createABufferedOutputMock()
往下看,此时createABufferedOutputMock()
进入for循环,并且在其中要调用test
的expectedOutput
属性,然而在可以实例化的类中不存在expectedOutput
属性(ctrl+shift+F
全局搜索),只在一些测试类中存在。 - 这里要用到php魔术方法中的一个小trick,当访问一个类中不存在的属性时会触发
get()
,通过去触发get()方法去进一步构造pop链,在Illuminate\Auth\GenericUser
的get()
中 - 此时
$this->test
是Illuminate\Auth\GenericUser
的实例化对象,是我们传入的,那么是可控的,则$this->attributes
通过反序列化是可控的,因此我们可以构造$this->attributes
键名为expectedOutput
的数组。这样一来$this->test->expectedOutput
就会返回$this->attributes
中键名为expectedOutput
的数组。 - 此时回到
mockConsoleOutput()
中,又进行了一个循环遍历,调用了test
对象的的expectedQuestions
属性,里面的循环体与createABufferedOutputMock()
的循环体相同,因此绕过方法也是通过调用get()
,设置一个键名为expectedQuestions
的数组即可 - 继续F8单步调试就可以
return $mock
,从而走出mockConsoleOutput()
,接下来回到run()
中 - 其中
Kernel::class
在这里是一个固定值Illuminate\Contracts\Console\Kernel
,并且call的参数为我们所要执行的命令和命令参数($this->command, $this->parameters)
,需要弄清$this->app[Kernal::class]
返回的是哪个类的对象,使用F7步入程序 - 直到得到以下的
getConcrete()
,在704行判断$this->bindings[$abstract])
是否存在,若存在则返回$this->bindings[$abstract]['concrete']
。$bindings
是Container.php
文件中Container类
中的属性。只要寻找一个继承自Container的类
,即可通过反序列化控制$this->bindings属性
。Illuminate\Foundation\Application
恰好继承自Container
类。$abstract变量
为Illuminate\Contracts\Console\Kernel
,只需通过反序列化定义Illuminate\Foundation\Application
的$bindings
属性存在键名为Illuminate\Contracts\Console\Kernel
的二维数组就能进入该分支语句,返回我们要实例化的类名。 - 到了实例化
Application类
的时候, 此时要满足isBuildable()
才可以进行build
- 此时
$concrete
为Application
,而$abstract
为kernal
,此时不满足Application实例化
条件,此时继续F7,将会调用make()
- 此时将
$abstract
赋值为了Application
,并且make()
又调用了resolve()
,即实现了第二次调用isBuildable()
判断是否可以进行实例化,即此时已经可以成功实例化类Application
,完成了$this->app[Kernel::class]
为Application对象的转化 - 接下来将调用类
Application
中的call()
,即其父类Container
中的call()
- 其中第一个分支
isCallableWithAtSign()
判断回调函数是否为字符串并且其中含有@
,并且$defaultMethod
默认为null,显然此时不满足if条件,即进入第二个分支,callBoundMethod()
的调用. - 前面的
static::callBoundMethod
只是判断我们的$callback
是否为数组。后面的匿名函数直接调用call_user_func_array()
,并且第一个参数我们可控,参数值为system
,第二个参数由static::getMethodDependencies
方法返回。跟进static::getMethodDependencies
。 static::getCallReflector($callback)
用于利用反射获取$callback
的对象,继续往下执行static::addDependencyForCallParameter
,会对$callback
的对象添加一些参数,最后将我们传入的$parameters参数数组
和$dependencies数组
合并,$dependencies
数组为空。最后在BoundMethod对象
的call()
中我们相当于执行了以下代码:call_user_func_array('system',array('id'))
exp
1 | <?php |
参考文献:
https://laravel.com/api/5.7/Illuminate/Foundation/Testing/PendingCommand.html
https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce
https://xz.aliyun.com/t/5510