每次分享不一样,我还是那个菜菜的Master先生,一位向往于任意门的白帽少年,我的奇技淫巧,让你尽可能的getshell.今天的主角通达OA,前几天黑产界的杀手,开始吧

0x00 漏洞简介

CNVD:CNVD-2020-26562

通达OA是由北京通达信科科技有限公司开发的一款办公系统,前几天通达官方在其官网发布了安全提醒与更新程序,并披露有用户遭到攻击。
攻击者可在未授权的情况下可上传图片木马文件,之后通过精心构造的请求进行文件包含,实现远程命令执行,且攻击者无须登陆认证即可完成攻击。
图片1.png
本文主要以通达OA文件上传和文件包含导致的RCE进行复现和分析

通过fofa的搜索可以看到通达OA系统应用非常广泛,这就给同学们提供了大量的实战环境.当然大家一定要做一个正直的白帽子.
图片2.png

0x01 影响范围

  1. 通达OA V11版 <= 11.3 20200103
  2. 通达OA 2017版 <= 10.19 20190522
  3. 通达OA 2016版 <= 9.13 20170710
  4. 通达OA 2015版 <= 8.15 20160722
  5. 通达OA 2013增强版 <= 7.25 20141211
  6. 通达OA 2013版 <= 6.20 20141017

0x02 环境搭建

通达OA系统采用了一键式的傻瓜操作,正常的软件安装,这里我本地搭建的.安装过程省略.安装完成后访问本地地址,截图如下:
图片3.png

0x03 未授权上传文件

文件在 webroot\ispirit\im\upload.php

<?php

set_time_limit(0);
$P = $_POST['P'];
if (isset($P) || $P != '') {
 ob_start();
 include_once 'inc/session.php';
 session_id($P);
 session_start();
 session_write_close();
} else {
 include_once './auth.php';
}
include_once 'inc/utility_file.php';
include_once 'inc/utility_msg.php';
include_once 'mobile/inc/funcs.php';
ob_end_clean();
$TYPE = $_POST['TYPE'];
$DEST_UID = $_POST['DEST_UID'];
$dataBack = array();
if ($DEST_UID != '' && !td_verify_ids($ids)) {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 _('接收
 方ID无效'));
 echo json_encode(data2utf8($dataBack));
 exit;
}
if (strpos($DEST_UID, ',') !== false) {
} else {
 $DEST_UID = intval($DEST_UID);
}
if ($DEST_UID == 0) {
 if ($UPLOAD_MODE != 2) {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 _('接收方ID无效'));
 echo json_encode(data2utf8($dataBack));
 exit;
 }
}
$MODULE = 'im';
if (1 <= count($_FILES)) {
 if ($UPLOAD_MODE == '1') {
 if (strlen(urldecode($_FILES['ATTACHMENT']['name'])) != 
 strlen($_FILES['ATTACHMENT']['name'])) {
 $_FILES['ATTACHMENT']['name'] = 
 urldecode($_FILES['ATTACHMENT']['name']);
 }
 }
 $ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);
 if (!is_array($ATTACHMENTS)) {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 $ATTACHMENTS);
 echo json_encode(data2utf8($dataBack));
 exit;
 }
 ob_end_clean();
 $ATTACHMENT_ID = substr($ATTACHMENTS['ID'], 0, -1);
 $ATTACHMENT_NAME = substr($ATTACHMENTS['NAME'], 0, -1);
 if ($TYPE == 'mobile') {
 $ATTACHMENT_NAME = 
 td_iconv(urldecode($ATTACHMENT_NAME), 
 'utf-8', MYOA_CHARSET);
 }
} else {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 _('无文
 件上传'));
 echo json_encode(data2utf8($dataBack));
 exit;
}
$FILE_SIZE = attach_size($ATTACHMENT_ID, $ATTACHMENT_NAME, 
$MODULE);
if (!$FILE_SIZE) {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 _('文件
 上传失败'));
 echo json_encode(data2utf8($dataBack));
 exit;
}
if ($UPLOAD_MODE == '1') {
 if (is_thumbable($ATTACHMENT_NAME)) {
 $FILE_PATH = attach_real_path($ATTACHMENT_ID, 
 $ATTACHMENT_NAME, $MODULE);
 $THUMB_FILE_PATH = substr($FILE_PATH, 0, 
 strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) . 
 'thumb_' 
 . $ATTACHMENT_NAME;
 CreateThumb($FILE_PATH, 320, 240, $THUMB_FILE_PATH);
 }
 $P_VER = is_numeric($P_VER) ? intval($P_VER) : 0;
 $MSG_CATE = $_POST['MSG_CATE'];
 if ($MSG_CATE == 'file') {
 $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . 
 $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
 } else {
 if ($MSG_CATE == 'image') {
 $CONTENT = '[im]' . $ATTACHMENT_ID . '|' . 
 $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/im]';
 } else {
 $DURATION = intval($DURATION);
 $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . 
 $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
 }
 }
 $AID = 0;
 $POS = strpos($ATTACHMENT_ID, '@');
 if ($POS !== false) {
 $AID = intval(substr($ATTACHMENT_ID, 0, $POS));
 }
 $query = 'INSERT INTO im_offline_file 
 (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG,AID) values 
 (\'' . date('Y-m-d H:i:s') . '\',\'' . 
 $_SESSION['LOGIN_UID'] 
 . '\',\'' . $DEST_UID . '\',\'*' . $ATTACHMENT_ID . '.' . 
 $ATTACHMENT_NAME . '\',\'' . $FILE_SIZE . '\',\'0\',\'' . 
 $AID 
 . '\')';
 $cursor = exequery(TD::conn(), $query);
 $FILE_ID = mysql_insert_id();
 if ($cursor === false) {
 $dataBack = array('status' => 0, 'content' => '-ERR ' . 
 _('数据库操作失败'));
 echo json_encode(data2utf8($dataBack));
 exit;
 }
 $dataBack = array('status' => 1, 'content' => $CONTENT, 
 'file_id' => $FILE_ID);
 echo json_encode(data2utf8($dataBack));
 exit;
} else {
 if ($UPLOAD_MODE == '2') {
 $DURATION = intval($_POST['DURATION']);
 $CONTENT = '[vm]' . $ATTACHMENT_ID . '|' . 
 $ATTACHMENT_NAME . '|' . $DURATION . '[/vm]';
 $query = 'INSERT INTO WEIXUN_SHARE (UID, CONTENT, 
 ADDTIME) 
 VALUES (\'' . $_SESSION['LOGIN_UID'] . '\', \'' . 
 $CONTENT 
 . '\', \'' . time() . '\')';
 $cursor = exequery(TD::conn(), $query);
 echo '+OK ' . $CONTENT;
 } else {
 if ($UPLOAD_MODE == '3') {
 if (is_thumbable($ATTACHMENT_NAME)) {
 $FILE_PATH = attach_real_path($ATTACHMENT_ID, 
 $ATTACHMENT_NAME, $MODULE);
 $THUMB_FILE_PATH = substr($FILE_PATH, 0, 
 strlen($FILE_PATH) - strlen($ATTACHMENT_NAME)) 
 . 'thumb_' . $ATTACHMENT_NAME;
 CreateThumb($FILE_PATH, 320, 240, 
 $THUMB_FILE_PATH);
 }
 echo '+OK ' . $ATTACHMENT_ID;
 } else {
 $CONTENT = '[fm]' . $ATTACHMENT_ID . '|' . 
 $ATTACHMENT_NAME . '|' . $FILE_SIZE . '[/fm]';
 $msg_id = send_msg($_SESSION['LOGIN_UID'], 
 $DEST_UID, 
 1, $CONTENT, '', 2);
 $query = 'insert into IM_OFFLINE_FILE 
 (TIME,SRC_UID,DEST_UID,FILE_NAME,FILE_SIZE,FLAG) 
 values (\'' . date('Y-m-d H:i:s') . '\',\'' . 
 $_SESSION['LOGIN_UID'] . '\',\'' . $DEST_UID . 
 '\',\'*' . $ATTACHMENT_ID . '.' . $ATTACHMENT_NAME 
 . '\',\'' . $FILE_SIZE . '\',\'0\')';
 $cursor = exequery(TD::conn(), $query);
 $FILE_ID = mysql_insert_id();
 if ($cursor === false) {
 echo '-ERR ' . _('数据库操作失败');
 exit;
 }
 if ($FILE_ID == 0) {
 echo '-ERR ' . _('数据库操作失败2');
 exit;
 }
 echo '+OK ,' . $FILE_ID . ',' . $msg_id;
 exit;
 }
 }
}

源码采用了zend加密,解密后才能正常阅读代码,上面的代码是解密后的,如果有想去探索更多的可以用解密工具解密自行研究。
通过上边的源码可以看到,第一个if(第5行)对P进行了判断,只要传递了参数P或者不为空,就可以进入下面的语句,如果判断失败,就进入else,也就是身份认证功能,所以这里只需要传递一个P并且值不为空,就可以绕过登录认证,在未授权的情况下进行上传文件。
图片4.png
这里测试的包中传递了P参数
接着往下看
图片5.png
判断DEST_UID,只要不为空也不为0即可, 在之后的文件上传处理逻辑代码中,会对$_FILES‘ATTACHMENT’)进行一次url解码,之后判断解码前后文件名长度是否有变化,如果有变化,则将url解码后的文件名作为最后的文件名。之后追踪upload函数,在 inc\utility_file.php 的1321行

function upload($PREFIX = 'ATTACHMENT', $MODULE = '', $OUTPUT = 
true)
{
 if (strstr($MODULE, '/') || strstr($MODULE, '\\')) {
 if (!$OUTPUT) {
 return _('参数含有非法字符。');
 }
 Message(_('错误'), _('参数含有非法字符。'));
 exit;
 }
 $ATTACHMENTS = array('ID' => '', 'NAME' => '');
 reset($_FILES);
 foreach ($_FILES as $KEY => $ATTACHMENT) {
 if ($ATTACHMENT['error'] == 4 || $KEY != $PREFIX && 
 substr($KEY, 0, strlen($PREFIX) + 1) != $PREFIX . '_') 
 {
 continue;
 }
 $data_charset = isset($_GET['data_charset']) ?
 $_GET['data_charset'] : (isset($_POST['data_charset'])?
 $_POST['data_charset'] : '');
 $ATTACH_NAME = $data_charset != ''? 
 td_iconv($ATTACHMENT['name'], $data_charset, 
 MYOA_CHARSET) : $ATTACHMENT['name'];
 $ATTACH_SIZE = $ATTACHMENT['size'];
 $ATTACH_ERROR = $ATTACHMENT['error'];
 $ATTACH_FILE = $ATTACHMENT['tmp_name'];
 $ERROR_DESC = '';
 if ($ATTACH_ERROR == UPLOAD_ERR_OK) {
 if (!is_uploadable($ATTACH_NAME)) {
 $ERROR_DESC = sprintf(_('禁止上传后缀名为[%s]的文
 件'), substr($ATTACH_NAME, 
 strrpos($ATTACH_NAME, '.') + 1));
 }
 $encode = mb_detect_encoding($ATTACH_NAME, 
 array('ASCII', 'UTF-8', 'GB2312', 'GBK', 'BIG5'));
 if ($encode != 'UTF-8') {
 $ATTACH_NAME_UTF8 = 
 mb_convert_encoding($ATTACH_NAME, 'utf-8', 
 MYOA_CHARSET);
 } else {
 $ATTACH_NAME_UTF8 = $ATTACH_NAME;
 }
 if (preg_match('/[\\\':<>?]|\\/|\\\\|"|\\|/u', 
 $ATTACH_NAME_UTF8)) {
 $ERROR_DESC = sprintf(_('文件名[%s]包含
 [/\\\'":*?<>|]等非法字符'), $ATTACH_NAME);
 }
 if ($ATTACH_SIZE == 0) {
 $ERROR_DESC = sprintf(_('文件[%s]大小为0字节'), 
 $ATTACH_NAME);
 }
 if ($ERROR_DESC == '') {
 $ATTACH_NAME = str_replace('\'', '', 
 $ATTACH_NAME);
 $ATTACH_ID = add_attach($ATTACH_FILE, 
 $ATTACH_NAME, $MODULE);
 if ($ATTACH_ID === false) {
 $ERROR_DESC = sprintf(_('文件[%s]上传失败'), 
 $ATTACH_NAME);
 } else {
 $ATTACHMENTS['ID'] .= $ATTACH_ID . ',';
 $ATTACHMENTS['NAME'] .= $ATTACH_NAME . '*';
 }
 }
 @unlink($ATTACH_FILE);
 } else {
 if ($ATTACH_ERROR == UPLOAD_ERR_INI_SIZE) {
 $ERROR_DESC = sprintf(_('文件[%s]的大小超过了系统
 限制
(%s)'), $ATTACH_NAME, ini_get('upload_max_filesize'));
 } else {
 if ($ATTACH_ERROR == UPLOAD_ERR_FORM_SIZE) {
 $ERROR_DESC = sprintf(_('文件[%s]的大小超过
 了表
单限制'), $ATTACH_NAME);
 } else {
 if ($ATTACH_ERROR == UPLOAD_ERR_PARTIAL) {
 $ERROR_DESC = sprintf(_('文件[%s]上传不
 完整'), $ATTACH_NAME);
 } else {
 if ($ATTACH_ERROR == 
 UPLOAD_ERR_NO_TMP_DIR) {
 $ERROR_DESC = sprintf(_('文件[%s]上
 传失败:找不到临时文件夹'), 
 $ATTACH_NAME);
 } else {
 if ($ATTACH_ERROR == U
 PLOAD_ERR_CANT_WRITE) {
 $ERROR_DESC = sprintf(_('文件
 [%s]写入失败'), $ATTACH_NAME);
 } else {
 $ERROR_DESC = sprintf(_('未知错
 误[代码:%s]'), $ATTACH_ERROR);
 }
 }
 }
 }
 }
 }
 if ($ERROR_DESC != '') {
 if (!$OUTPUT) {
 delete_attach($ATTACHMENTS['ID'], 
 $ATTACHMENTS['NAME'], $MODULE);
 return $ERROR_DESC;
 } else {
 Message(_('错误'), $ERROR_DESC);
 }
 }
 }
 return $ATTACHMENTS;
}

图片6.png
这里调用了is_uploadable对文件名字进行判断,这个函数在1833行

function is_uploadable($FILE_NAME)
{
 $POS = strrpos($FILE_NAME, '.');
 if ($POS === false) {
 $EXT_NAME = $FILE_NAME;
 } else {
 if (strtolower(substr($FILE_NAME, $POS + 1, 3)) == 
 'php') {
 return false;
 }
 $EXT_NAME = strtolower(substr($FILE_NAME, $POS + 1));
 }
 if (find_id(MYOA_UPLOAD_FORBIDDEN_TYPE, $EXT_NAME)) {
 return false;
 }
 if (MYOA_UPLOAD_LIMIT == 0) {
 return true;
 } else {
 if (MYOA_UPLOAD_LIMIT == 1) {
 return !find_id(MYOA_UPLOAD_LIMIT_TYPE, $EXT_NAME);
 } else {
 if (MYOA_UPLOAD_LIMIT == 2) {
 return find_id(MYOA_UPLOAD_LIMIT_TYPE, 
 $EXT_NAME);
 } else {
 return false;
 }
 }
 }
}

首先使用了strrpos来定位 . 最后出现的位置
图片7.png
当文件名中不存在”.”时会直接以现有的文件名来作为EXT_NAME,如果存在则从.开始匹配3位,判断后缀是否为php,如果为php则返回false,否则将”.”之前的作为EXT_NAME。
因为通达OA搭建在windows环境下,所以可以上传一个.php.后缀的文件,来绕过文件检测(这里跟文件上传的绕过原理相同),但是这里问题是上传的文件不在web工作目录下,所以即使上传了也访问不到,所以无法利用
下边我们就要用到文件包含的漏洞执行我们上传的文件!

0x04 文件包含

这个关键文件的位置在webroot\ispirit\interface\gateway.php(这里仅参考我用的版本,不同的版本好像路径不同,还有待研究),话不多说,看源码:

<?php
ob_start();
include_once 'inc/session.php';
include_once 'inc/conn.php';
include_once 'inc/utility_org.php';
if ($P != '') {
 if (preg_match('/[^a-z0-9;]+/i', $P)) {
 echo _('非法参数');
 exit;
 }
 session_id($P);
 session_start();
 session_write_close();
 if ($_SESSION['LOGIN_USER_ID'] == '' || 
 $_SESSION['LOGIN_UID'] == '') {
 echo _('RELOGIN');
 exit;
 }
}
if ($json) {
 $json = stripcslashes($json);
 $json = (array) json_decode($json);
 foreach ($json as $key => $val) {
 if ($key == 'data') {
 $val = (array) $val;
 foreach ($val as $keys => $value) {
 ${$keys} = $value;
 }
 }
 if ($key == 'url') {
 $url = $val;
 }
 }
 if ($url != '') {
 if (substr($url, 0, 1) == '/') {
 $url = substr($url, 1);
 }
 if (strpos($url, 'general/') !== false || strpos($url, 
 'ispirit/') !== false || strpos($url, 'module/') !== 
 false) {
 include_once $url;
 }
 }
 exit;
}

这里首先是不传入参数P就可以进入下面判断语句,之后用到了stripcslashes函数
图片8.png
看一下实例就明白了,只是这里的源码接收了一个形参
之后从json中获取url参数的值,之后判断general/、ispirit/、module/是否在url内,如果不在直接跳过下面的include_once $url,如果存在则包含指定URL的文件, 这个是后期进行文件包含的重点

0x05 综合思路

通过第一个漏洞,绕过认证上传木马,然后通过文件包含来包含文件,其中需要注意的是 DEST_UID 不能未空,文件包含中的url请求数据中需要包含 general/、ispirit/、module/三者中的一个.

Last modification:June 7th, 2020 at 09:32 pm