CVE-2020-15504:Sophos XG RCE漏洞分析(Part 1)

阅读量    74435 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

2020年4月25日,Sophos发布了安全公告,其中提到了影响XG Firewall产品线的一个未授权SQL注入(SQLi)漏洞。根据官方描述,攻击者至少从4月22日以来就开始积极利用该漏洞。在安全公告发表不久后,Sophos发表了对Asnarök攻击活动的详细分析。之前的安全公告主要关注SQLi漏洞,后续的分析报告表明,攻击者通过某种方式扩展了攻击技术,最终实现了远程代码执行(RCE)。

基于这个漏洞的严重性,我们也开始研究其中技术细节。由于该漏洞影响的设备特殊,且具备RCE条件,因此可能帮助红队打破安全边界。然而根据下文分析,漏洞利用过程可能并不像我们最初设想的那么顺利。

在分析过程中,我们不仅复现了已公开漏洞(CVE-2020-12271)的RCE代码,也找到了另一个SQLi漏洞,该漏洞也能实现代码执行(CVE-2020-15504)。新漏洞的严重程度与Asnarök攻击活动中用过的漏洞相同:可以通过用户或管理员页面实现未授权利用。Sophos迅速响应了我们的报告,为目前支持的固件版本发布了补丁,也为v17.5及v18.0发布了新的固件版本(参考Sophos社区公告)。

 

0x01 环境配置

这里我们不详细介绍如何部署实验环境,XG防火墙虚拟环境搭建起来非常简单,只需要从官方下载页面获取匹配的固件ISO即可。需要注意的是,该固件允许管理员直接通过串口获取root shell,因此不需要考虑受限shell逃逸问题,可以直接开始分析。

图1. Device Management -> Advanced Shell -> root权限的/bin/sh

大概了解文件系统结构、开放的端口及运行的进程后,我们注意到XG控制中心中有一条消息,提示我们正在研究的历史漏洞已经有补丁程序。

图2. 自动安装补丁后的控制中心(来源

根据这种方式,我们分别在打补丁之前和之后创建了文件系统快照,diff这两个快照的web根目录后,我们发现官方只修改了一个文件,其中并没有关于修复SQL操作的直接改动。

 

0x02 架构分析

为了理解补丁逻辑,我们需要深入分析底层软件架构。根据官方公告,该漏洞可以通过web接口触发,因此我们比较关系服务器如何处理收到的HTTP请求。

用户和管理员web接口都基于同样的Java代码,由Apacher服务器后面的Jetty服务器提供服务。

图3. Jetty服务器在8009端口监听,服务接口为/usr/share/webconsole

与大多数接口的交互行为(如登录动作)都会向/webconsole/Controller端点发送HTTP POST请求,这类请求中至少包含2个参数:modejson。第一个参数指定了一个数值,在内部映射到被调用的某个函数。第二个参数指定了调用该函数时的参数。

图4. 通过XHR向/webconsole/Controller发送登录请求

对应的Servlet会检查所请求的参数是否需要身份认证,执行一些基本的参数校验操作(具体代码取决于被调用的函数),然后向另一个组件(CSC)发送消息。

if (151 == eventBean.getMode()) {
    try {
        final PrintWriter writer3 = httpServletResponse.getWriter();
        if (httpServletRequest.getParameter("json") != null) {
            final JSONObject jsonObject5 = new JSONObject();
            final CSCClient cscClient4 = new CSCClient();
            final JSONObject jsonObject6 = new JSONObject(httpServletRequest.getParameter("json"));
            final int int1 = jsonObject6.getInt("languageid");
            final int generateAndSendAjaxEvent = cscClient4.generateAndSendAjaxEvent(httpServletRequest, httpServletResponse, eventBean, sqlReader);

这个消息采用自定义格式,通过UDP或者TCP协议发送到本地主机的299端口(防火墙)。消息中包含一个JSON对象,与原始HTTP请求中的json参数类似,但不相同。

图5. 发送给299端口上CSC的JSON对象

CSC组件(/usr/bin/csc)似乎采用C编写,由多个子模块组成(类似于busybox二进制程序)。根据我们的理解,这个程序为防火墙的服务管理器,其中包含其他一些作业,也可以启动和控制这些作业。在针对Fortinet的研究过程中,我们也遇到过类似的架构。

图6. CSC程序生成的一些进程

CSC会解析收到的JSON对象,使用提供的参数来调用所请求的函数。然而这些函数采用Perl实现,通过Perl C语言接口来调用。为了完成该任务,该程序会加载并解密经过XOR加密的文件(cscconf.bin),该文件中包含各种配置文件及Perl包。

这个架构中还有一点比较重要:web接口、CSC及Perl代码逻辑同时使用了不同的PostgreSQL数据库实例。

图7. 服务端使用了3个PostgreSQL数据库

图8. 整体架构视图

 

0x03 定位Perl逻辑

前面提到过,Java组件会将修改版的JSON参数(原始版本位于HTTP请求中)发送给CSC程序,因此我们开始观察这个文件。反汇编工具能帮助我们检测分布在多个内部函数中的不同子模块,但并没有提供关于登录请求的任何逻辑。然而我们还是找到了与Perl C语言接口有关的大量import,因此我们猜测相关逻辑存放在外部Perl文件中。我们搜索文件系统后,并没有返回有用的信息。最终我们发现Perl代码以及各种配置文件存放在加密的tar.gz文件中(/_conf/cscconf.bin),该文件会在CSC初始化时解密并提取出来。之前我们之所以无法找到经过加密的文件,是因为这些文件位于独立的Linux命名空间中。

如下图所示,该程序会创建一个挂载点,使用0x20000标志来调用unshare syscall。这个值对应的是CLONE_NEWNS标志,用来解除进程与初始挂载命名空间的关联。

这里介绍一下Linux命名空间的背景信息:通常情况下,每个进程都关联一个命名空间,只能看到并使用与该命名空间关联的资源。将自身从初始命名空间分离后,程序可以确保在unshare syscall后创建的所有文件不会被其他进程获取。命名空间是Linux内核的一个功能,是docker之类的容器非常依赖的基础特性。

图9. 在提取配置文件前,调用unshare将自身与初始命名空间分离

因此,即使在root shell中,我们也无法访问提取出的文件。然而我们有多种办法可以绕过该限制,最常用的方法是简单patch目标程序。通过这种方式,我们可以将提取出的配置文件拷贝到全局可写的一个路径中。事后经我们分析,使用nsenter可能是更好的一种方法。

图10. 转入CSC程序命名空间后,可访问解密后释放出来的文件

 

0x04 CVE-2020-12271

官方补丁修改了已有的一个函数(_send),并在/usr/share/webconsole/WEB-INF/classes/cyberoam/corporate/CSCClient.class中引入了2个新函数(getPreAuthOperationList以及addEventAndEntityInPayload)。

getPreAuthOperationList函数定义未授权下可以调用的所有modeaddEventAndEntityInPayload会检查请求中指定的mode是否包含在preAuthOperationsList中,如果满足该条件,则删除JSON对象中的EntityEvent属性。

private static ArrayList<Integer> getPreAuthOperationList() {
    ArrayList<Integer> modeList = new ArrayList<Integer>();
    modeList.add(151);
    modeList.add(1503);
    [...]
    return modeList;
}

private void addEventAndEntityInPayload(HttpServletRequest req, JSONObject reqJson, int mode) {
    try {
        if (PRE_AUTH_OPERATIONS.contains(mode)) {
            [...]
            reqJson.remove("Event");
            reqJson.remove("Entity");
            CyberoamLogger.debug((String)"CSC", (String)("Request payload after sanitization: " + reqJson.toString()));
        } 
        [...]
    }
}

漏洞分析

根据补丁,我们猜测漏洞应该位于getPreAuthOperationList中指定的某个函数。我们浏览了相关的Perl代码,想寻找使用Entity或者Event属性的逻辑,却发现情况并非如此。

但在这个过程中我们注意到一点:不论我们指定哪种mode,每个请求都会由apiInterface函数处理。Sophos在内部通过操作码(opcode)方式将函数映射成mode参数。

opcode apiInterface{
    CALL validateRequestType
    CALL variableInitialization 
    [...]
    CALL isAPIVersionSupported
    CALL opcodePreProcess

    FOR ("$ind=0";"$ind<scalar(@reqEntitiesArr)";"$ind++") {
        $currRequest=$reqEntitiesArr[$ind];
        if($requestType eq $REQUEST_TYPE{MULTIREQUEST}){
            print "\n\n\t ============ Handling request -- Entity = $currRequest->{Entity}";
            $request=$currRequest->{reqJSON};
            $request->{Event}=uc($currRequest->{Event});
            $request->{Entity}=lc($currRequest->{Entity});
        }
        CALL checkUserPermission
        CALL preMigration
        CALL createModeJSON
        CALL migrateToCurrVersion
        CALL createJson
        CALL validateJson
        CALL handleDeleteRequest
        CALL replyIfErrorAtValidation
        CALL getOldObject       

        IF("$entityJson->{Event} eq 'DELETE' && defined $modeJson->{ORM} && $modeJson->{ORM} eq 'true'"){
            CALL executeDeleteQuery
        }
    }
        [...]

apiInterface函数也是我们最终发现SQLi漏洞的位置所在。如下部分源码所示,这个opcode会调用executeDeleteQuery函数(第27行),该函数接收来自query参数的SQL语句,在目标数据库中执行。

FUNCTION executeDeleteQuery{    
    @queryToExecute=@{$request->{query}};

    FOR("$q=0";"$q<scalar(@queryToExecute)";"$q++") {        
        QUERY "$queryToExecute[$q]"
    }

    ON_FAIL{
        QUERY "rollback"
        %responsej=("status"=>"500","statusmessage"=>"Records Deletion Failed.","deleteObjects"=>\@deletingObjects,"references"=>$references);
        REPLY  %responsej 500
    }
}

不幸的是,为了执行到漏洞代码,我们的payload需要通过前面的每个CALL语句,这些语句会在我们的JSON对象上设置各种条件及属性。

第一个调用(validateRequestType)要求Entity的值不为securitypolicy,并且在调用后请求类型为ORM

FUNCTION validateRequestType{
    IF("defined $request->{Entity} && defined $request->{Event}"){
        IF("$request->{Entity} eq '' || $request->{Event} eq ''"){
            Log applog "\n\n Error ----> Entity and Event is defined but NULL value is passed...! !\n"
            FAIL
        }
        [...]
        IF("$request->{Entity} eq 'securitypolicy' && $request->{Event} eq 'DELETE' && scalar(@{$request->{name}}) > 1"){
            [...]
        }
    }ELSE IF("defined $request->{reqEntities}"){
        IF("scalar(@{$request->{reqEntities}}) == 0"){
            FAIL
        }
    [...]

variableInitialization用来初始化Perl环境,始终能够执行成功。为了让我们的请求保持简洁性,不引入其他前提条件,我们payload中的Entity值不能等于这些值:

securityprofile
mtadataprotectionpolicy
dataprotectionpolicy
firewallgroup
securitypolicy
formtemplate
authprofile

这样我们就能跳过opcodePreProcess函数中的检查逻辑。

checkUserPermission函数的功能与函数名一致。函数体如下所示,只有当传递给Perl的JSON对象中包含__username参数时,函数体才会被执行。如果HTTP请求关联有效的用户会话,那么在请求被转发给CSC程序之前,Java组件会在其中添加这个参数。由于我们在payload中使用的是未经身份认证的mode,因此请求中不会设置__username参数,我们可以忽略相应的代码。

FUNCTION checkUserPermission{
    IF("defined $request->{___username} && '' ne $request->{___username} && 'LOCAL' ne $request->{___username}"){
        IF("(! defined  $request->{currentlyloggedinuserid}) || '0' eq  $request->{currentlyloggedinuserid} "){
            curLogOut = QUERY "select userid from tbluser where username= '$currentUser'"
            IF(" defined  $curLogOut->{output}->{userid} && $curLogOut->{output}->{userid}[0] ne ''"){
                 [...]

为了跳过preMigration调用,我们只需要选择不等于35cancel_firmware_upload)、36multicast_sroutes_disable)或者1101unknown)的mode即可。这3种mode都需要身份认证,因此对我们的攻击场景来说意义不大。

根据请求的类型,createModeJSON函数会使用不同的逻辑来加载连接到特定实体的Perl模块。尽管每个POST请求最初都为ORM请求,我们还是需要确保请求类型没有被改成其他类型。apiInterface函数内部调用存在漏洞的函数之前有个if语句,只有类型不变才能通过该语句。因此下面代码中第15行的条件肯定不满足,代码会检查已加载的Perl模块中指定的请求类型是否等于ORM。我们将识别这类Entity的工作留给大家来完成。

FUNCTION createModeJSON {
    IF("$requestType == $REQUEST_TYPE{NORMALREQUEST}"){
        $modeJson = getHashFromMode($request->{mode});
        $packName=$modeJson->{entityFilename};
        require $apiPath.$modeJson->{entityFilename};
    }ELSE IF("$requestType == $REQUEST_TYPE{ORMREQUEST} || $requestType == $REQUEST_TYPE{MULTIREQUEST}"){
        if(defined $request->{Entity}){
            $packName=$ENTITYMAP->{$request->{Entity}};
            print "\n\n package Name=$packName";
            eval "use $packName";
            $propertyObj="\$$packName"."::EventProperties";
            $objecto=eval $propertyObj;
            $modeJson = $objecto->{$request->{Event}};
            $modeJson->{entityFilename}=$packName;
            if((defined $modeJson->{ORM} && $modeJson->{ORM} ne 'true') || (!defined $modeJson->{ORM})){
                $requestType = $REQUEST_TYPE{NORMALREQUEST};
                $modeJson = getHashFromMode($request->{mode});
                require $apiPath.$modeJson->{entityFilename};
            }
        }            
    }
}

我们跳过对migrateToCurrVersion函数的分析,该函数对漏洞利用链来说不重要。代码随后会调用createJson,验证前面加载的Perl包能否被初始化。只要引用的是已有的Entity,肯定能通过这个检查。

FUNCTION createJson{
    IF("$modeJson->{entityFilename} eq \"\""){
        <code>
            $entityJson=$request;
            print "\n\n MODE:$request->{mode} FILE NOT FOUND\n";
        </code>
    }ELSE{
        <code>
            $Package=$modeJson->{entityFilename};
            print "\n PAckage ::::$Package";
            eval "require $Package";
            $entityJson=new $Package($request);         
            if(!$entityJson->can('new')){
                print "\n\n PACKAGE:$Package NOT Found OBJECT NOT CREA TED\n";
            }
        </code>
    }
}

handleDeleteRequest函数会再次校验请求类型是否为ORM。从我们的JSON中删除重复的属性后,该函数会确保我们的JSON payload中包含一个name属性。随后代码会循环遍历我们在name属性中指定的所有值,在其他数据库表中搜索外部引用以便删除对应表项。由于我们不想删除已有的任何数据,我们只需要将name设置为不存在的值即可。

我们可以直接跳过最后两个函数(replyIfErrorAtValidation以及getOldObject),这两个函数与我们的利用链无关,现在我们已经完成大多数Perl代码的分析工作。

目前我们了解到如下知识:

1、我们需要设置可以未授权调用的mode

2、我们不应当使用某些Entity;

3、我们的请求必须为$REQUEST_TYPE{ORMREQUEST}类型;

4、请求中必须包含name属性,其中保存某些垃圾值;

5、已加载的Entity的EventProperties(尤其是DELETE属性)必须将ORM值设置为true

6、我们的JSON对象必须包含一个query属性,其中保存我们想执行的SQL语句。

当我们满足以上所有条件时,我们就可以执行任意SQL语句。这里有一个限制条件:我们不能在SQL语句中使用任何引号,因为csc程序会正确转义这些符号。因此我们使用concat以及chr SQL函数来定义字符串。

从SQLi到RCE

一旦我们能够根据需求来修改数据库,就可以通过某些方式将SQLi拓展为RCE,这是因为我们在多个地方发现数据库中包含的参数会传递给exec调用,没有经过正确的过滤。为了实现RCE,我们根据自己的理解,借鉴Sophos对Asnarök攻击活动的分析报告,最终形成攻击路径。

根据官方提供的信息,攻击者将payload注入Sophos Firewall Manager(SFM)的hostname字段以实现代码执行。SFM是用来管理多个单元的独立单元。这样自然引出一个问题:如果我们启用中心管理后,后端服务会出现什么变化呢?

为了寻找与SFM功能有关的数据库值,我们dump出数据库,在前端启用SFM,然后再次执行dump操作。对比这两次dump的差别后,我们可以发现有改动的地方,从而找到了多处被修改的数据库行。我们发现攻击者使用表tblclientservices中的CCCAdminIP属性来注入payload。grep查找CCCAdminIP后,我们在Perl代码中找到了get_SOA函数。

opcode get_SOA;
opcode get_SOA with attributes no_wait {

    is_eula = NOFAIL EXECSH "/bin/nvram qget is_eula"
    IF ("$is_eula->{status} eq '0'") {
    TIMER get_SOA:add oncejob nosync "minutes 30" : opcode get_SOA
    RETURN 200
    }
    [...]
    #check if hotfixes are automatically installed
    accepthotfixes = QUERY "select value from tblconfiguration where key='accepthotfixes'"
    IF("$accepthotfixes->{output}->{value}[0] eq 'on'"){
        [...]
        # Our payload was inserted into CCCAdminIP and is here received from the database
        out = QUERY "select servicevalue from tblclientservices where servicekey in ('CCCEnabled','CCCAdminIP','CCC_signature_distribution','CCCFWVersion') order by servicekey='CCCEnabled' desc,servicekey='CCCAdminIP' desc,servicekey='CCC_signature_distribution' desc,servicekey='CCCFWVersion' desc"
            $CCCEnabled = $out->{output}->{servicevalue}[0];
            $CCCAdminIP = $out->{output}->{servicevalue}[1];
            $CCC_signature_distribution = $out->{output}->{servicevalue}[2];
            $CCCFWVersion =  $out->{output}->{servicevalue}[3];

        [...]
        out = EXECSH "echo $CCC_signature_port,$CCCAdminIP > /tmp/up2date_servers_srv.conf"

如上述代码15行所示,代码会从数据库中检索CCCAdminIP的值,然后未经过滤,将其传递给第22行的EXECSH调用。在某些cron作业任务的帮助下,get_SOA opcode会定期执行,因此会自动执行我们的payload。

这个利用链有个缺点,观察第11行的if语句,只有自动安装补丁设置处于激活状态时(这是默认设置),并且设备使用SFM用来实现中心管理时,我们才能访问到EXECSH调用。在这种情况下,攻击者很有可能只能在激活了自动更新的设备上才能实现代码执行,这将导致竞争条件:补丁安装时机与漏洞利用时机需要抢占先机。

未设置自动更新的设备或者未升级到最新版本的设备仍可能存在漏洞。

图11. CVE-2020-12271:通过SQLi实现代码执行

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多