【新】通达OA前台反序列化漏洞分析

0x01 前言

注:本文仅以安全研究为目的,分享对该漏洞的挖掘过程,文中涉及的所有漏洞均已报送给国家单位,请勿用做非法用途。

通达OA作为历史上出现漏洞较多的OA,在经过多轮的迭代之后已经很少前台的RCE漏洞了。一般来说通达OA是通过auth.inc.php文件来进行鉴权,如图1.1所示。整个通达全部的代码看下来很少有未鉴权的代码。

在通达中有一个模块

/general/appbuilder/web/index.php采用了yii框架实现,其鉴权逻辑与其他模块存在显著差异。

$url = $_SERVER["REQUEST_URI"];    $strurl = substr($url, 0, strpos($url, "?"));
    if (strpos($strurl, "/portal/") !== false) {      if (strpos($strurl, "/gateway/") === false) {        header("Location:/index.php");        sess_close();        exit();      }      else if (strpos($strurl, "/gateway/saveportal") !== false) {        header("Location:/index.php");        sess_close();        exit();      }      else if (strpos($url, "edit") !== false) {        header("Location:/index.php");        sess_close();        exit();      }    }    else if (strpos($url, "/appdata/doprint") !== false) {      $_GET["csrf"] = urldecode($_GET["csrf"]);      $b_check_csrf = false;      if (!empty($_GET["csrf"]) && preg_match("/^\{([0-9A-Z]|-){36}\}$/", $_GET["csrf"])) {        $s_tmp = __DIR__ . "/../../../../logs/appbuilder/logs";        $s_tmp .= "/" . $_GET["csrf"];
        if (file_exists($s_tmp)) {          $b_check_csrf = true;          $b_dir_priv = true;        }      }
      if (!$b_check_csrf) {        header("Location:/index.php");        sess_close();        exit();      }    }    else {      header("Location:/index.php");      sess_close();      exit();    }

从上面的代码可以看出,如果访问的目标地址是/general/appbuilder/web/portal/gateway/?,则不需要授权就能访问对应的接口。

再来看一下通达中使用的yii版本,如图1.2所示。Yii2 < 2.0.38是存在反序列化利用链的,网上已经有很多分析文章,感兴趣的小伙伴可以关注。

https://www.anquanke.com/post/id/254429

虽然有了利用链,但如何利用一直是个难题。本文的目的是寻找反序列化利用点并构造反序列化利用链达到RCE效果。

0x02 反序列化点

在appbuilder模块中多数情况下会加载视图views/layouts/main.php。视图中会调用csrfMetaTags方法。

<?php
$this->beginPage();echo "<!DOCTYPE html>\n<html lang=\"";echo Yii::$app->language;echo "\">\n<head>\n    <meta charset=\"";echo Yii::$app->charset;echo "\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    ";echo yii\helpers\Html::csrfMetaTags();echo "    <title>";echo yii\helpers\Html::encode($this->title);echo "</title>\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"";echo Yii::$app->params["MYOA_STATIC_SERVER"];echo "/static/theme/";echo Yii::$app->params["LOGIN_THEME"] == "" ? "1" : Yii::$app->params["LOGIN_THEME"];echo "/style.css\" />\n    <link href=\"/static/js/bootstrap/css/bootstrap.css\" rel=\"stylesheet\">\n<!--    <link href=\"/module/appbuilder/css/bootstrap.css\" rel=\"stylesheet\">-->\n    <link href=\"/module/appbuilder/css/site.css\" rel=\"stylesheet\"></head>\n\t<style>\n\ta.btn.btn-danger {\n\t\tcolor: #fff;\n\t}\n\t</style>\n    ";$this->head();echo "</head>\n<body class=\"bodycolor\">\n";$this->beginBody();echo "\n<div><!--class=\"wrap\"-->\n    ";

在yii框架中存在yii\helpers\Html::csrfMetaTags()方法,该方法的主要作用时用于生成csrf校验需要的meta标签。

 public static function csrfMetaTags(){        $request = Yii::$app->getRequest();        if ($request instanceof Request && $request->enableCsrfValidation) {            return static::tag('meta', '', ['name' => 'csrf-param', 'content' => $request->csrfParam]) . "\n"                . static::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]) . "\n";        }
        return '';    }

在方法中调用了$request->getCsrfToken(),跟踪该方法。

public function getCsrfToken($regenerate = false){        if ($this->_csrfToken === null || $regenerate) {            $token = $this->loadCsrfToken();            if ($regenerate || empty($token)) {                $token = $this->generateCsrfToken();            }            $this->_csrfToken = Yii::$app->security->maskToken($token);        }
        return $this->_csrfToken;    }   public function getCsrfToken($regenerate = false){        if ($this->_csrfToken === null || $regenerate) {            $token = $this->loadCsrfToken();            if ($regenerate || empty($token)) {                $token = $this->generateCsrfToken();            }            $this->_csrfToken = Yii::$app->security->maskToken($token);        }
        return $this->_csrfToken;    }

在该方法中继续调用了loadCsrfToken方法,跟踪该方法。

​​​​​​​

protected function loadCsrfToken(){        if ($this->enableCsrfCookie) {            return $this->getCookies()->getValue($this->csrfParam);        }
        return Yii::$app->getSession()->get($this->csrfParam);    }

继续跟踪getCookies方法。

public function getCookies(){    if ($this->_cookies === null) {        $this->_cookies = new CookieCollection($this->loadCookies(), [            'readOnly' => true,        ]);    }    return $this->_cookies;}

继续跟踪loadCookies方法,在这个方法中会调用unserialize方法对传入的Cookie的值进行反序列化。这也就会造成反序列化漏洞。

​​​​​​​​​​​​​​

protected function loadCookies(){        $cookies = [];        if ($this->enableCookieValidation) {            if ($this->cookieValidationKey == '') {                throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');            }            foreach ($_COOKIE as $name => $value) {                if (!is_string($value)) {                    continue;                }                $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);                if ($data === false) {                    continue;                }                if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {                    $data = @unserialize($data, ['allowed_classes' => false]);                } else {                    $data = @unserialize($data); //这里是反序列化点                }                if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) {                    $cookies[$name] = Yii::createObject([                        'class' => 'yii\web\Cookie',                        'name' => $name,                        'value' => $data[1],                        'expire' => null,                    ]);                }            }        } else {            foreach ($_COOKIE as $name => $value) {                $cookies[$name] = Yii::createObject([                    'class' => 'yii\web\Cookie',                    'name' => $name,                    'value' => $value,                    'expire' => null,                ]);            }        }
        return $cookies;    }

0x03 绕过限制

在上面的方法中会对传入的Cookie值进行签名校验,校验的方法是validateData,其中$this->cookieValidationKey是签名的key。

Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey)

在validateData方法中会通过hash_hmac对传入的key和value进行签名校验。

public function validateData($data, $key, $rawHash = false){    $test = @hash_hmac($this->macHash, '', '', $rawHash);    if (!$test) {        throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash);    }    $hashLength = StringHelper::byteLength($test);    if (StringHelper::byteLength($data) >= $hashLength) {        $hash = StringHelper::byteSubstr($data, 0, $hashLength);        $pureData = StringHelper::byteSubstr($data, $hashLength, null);        $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);        if ($this->compareString($hash, $calculatedHash)) {            return $pureData;        }    }    return false;}

由于通达OA中的$this->cookieValidationKey来自于配置文件general/appbuilder/config/web.php。其中值是固定的。

<?php
$params = require __DIR__ . "/params.php";$config = array(  "id"          => "appbuilder",  "basePath"    => dirname(__DIR__),  "bootstrap"   => array("log"),  "charset"     => "GB2312",  "language"    => "zh-CN",  "runtimePath" => "@app/../../../logs/appbuilder",  "components"  => array(    "request"      => array("cookieValidationKey" => "tdide2"), //这是固定的密钥tdide2    "cache"        => array("class" => "app\\td\base\TDRedisCache"),    "redis"        => array("class" => "yii\\redis\Connection", "hostname" => $MYOA_REDIS_SERVERS[0]["host"], "port" => $MYOA_REDIS_SERVERS[0]["port"], "database" => $MYOA_REDIS_DB_ID + 1, "password" => $MYOA_REDIS_PASS),    "errorHandler" => array("errorAction" => "site/error"),    "log"          => array(      "traceLevel" => 1,      "targets"    => array(        array(          "class"  => "yii\log\FileTarget",          "levels" => array("error")          )        )

另外通达OA有全局的addslashes过滤,包括Cookie中的值。由于PHP反序列化中有大量的双引号,如果直接通过Cookie传递则会因为双引号被转义而失败。但是令人开心的是通达在进行全局addslashes的时候对部分值进行了例外排查。

if (0 < count($_COOKIE)) {  foreach ($_COOKIE as $s_key => $s_value ) {    if ((substr($s_key, 0, 7) == "_SERVER") || (substr($s_key, 0, 8) == "_SESSION") || (substr($s_key, 0, 7) == "_COOKIE") || (substr($s_key, 0, 4) == "_GET") || (substr($s_key, 0, 5) == "_POST") || (substr($s_key, 0, 6) == "_FILES")) {      continue;    }
    if (!is_array($s_value)) {      $_COOKIE[$s_key] = addslashes(strip_tags($s_value));    }
    $s_key = $_COOKIE[$s_key];  }
  reset($_COOKIE);}

如果Cookie中字段名称的前面几位字符为_GET这种,则不进行addslashes操作。这也就给漏洞利用提供了便利。

0x04结论

反序列化利用链是直接采用yii中已经公开的反序列化链,本文作者调好的poc(实际是exp)如下所示,完整的poc可从ddpoc平台查看。

https://www.ddpoc.com/poc/DVB-2023-4705.html

使用该poc之后可以会在网站根目录生成哥斯拉的webshell。

在根目录生成文件/logon.php 111/111 哥斯拉

本漏洞已向相关单位报送并已推出补丁,可适用于通达11.X全版本。对应12系列未做过多测试,本文提到的所有漏洞在最新版的通达12.4中已修复,使用此漏洞造成的任何攻击影响均与本文作者无关

相关推荐
openinstall全渠道统计38 分钟前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫1 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫1 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫1 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫2 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫2 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫2 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标3 小时前
android 快速定位当前页面
android
Moyo2034 小时前
前端web安全
前端·安全·web安全
JSZNZZ5 小时前
汽车软件︱AUTO TECH China 2025 广州国际汽车软件与安全技术展览会:开启汽车科技新时代
科技·安全·汽车