900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > 通达OA任意文件上传/文件包含RCE漏洞分析

通达OA任意文件上传/文件包含RCE漏洞分析

时间:2020-01-16 22:02:55

相关推荐

通达OA任意文件上传/文件包含RCE漏洞分析

通达OA任意文件上传/文件包含RCE漏洞分析

0x01 前提0x01 漏洞介绍0x02 漏洞分析首先下载安装绕过身份验证文件上传部分变量传递问题文件包含部分

0x01 前提

关于这个漏洞的利用方式:

利用方式大致有两种:

包含日志文件。绕过身份验证文件上传然后在文件包含。

下面主要分析第二种

0x01 漏洞介绍

通达OA系统代表了协同OA的先进理念,研发铸就成熟OA产品,协同OA软件行业唯一央企团队研发,多次摘取国内OA软件金奖,拥有2万多家正式用户,8万多家免费版用户,超过…

主要危害:

攻击者可以在为登陆或者说,无任何条件触发漏洞,上传图片木马文件,请求进行文件包含最终可达成远程命令执行

影响版本:

V11版版版版版增强版

0x02 漏洞分析

我用的官网下载的V11.3

利用方式大致有两种:

包含日志文件。绕过身份验证文件上传然后在文件包含。

下面我主要分析饶过权限上传,然后文件包含的方式:

首先下载安装

打开源码一看,都加密了,使用zend进行了加密。

所以先要进行解密,百度即可。

绕过身份验证文件上传部分

存在漏洞的上传功能文件为 webroot\ispirit\im\upload.php

解密后的源码

<?phpset_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;}}}

看下开头这一块,就是绕过的核心部分

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';}

这里获取了一个P,如果P存在或者不为空,就要包含上面的auth.php,看名字就知道是一个主要实现身份认证功能,所以通过这里的参数"P"绕过登录认证,就可以去下面的上传了在往后就是两个IF条件句,只要进去了都要exit退出,所以要绕过才能进入上传的逻辑里面

$DEST_UID = $_POST['DEST_UID'];还好这个参数可控,要求不能为 0 也不能为空就可以了

进入循环后使用PHP的 $_FILES 函数来获取我们上传的文件信息

$_FILES['ATTACHMENT']['name']

第一个下标必须是我们的input name值,因此我们的POST包的Content-Disposition: form-data; name=“ATTACHMENT”; filename="xxx.php.png"中的name必须是’ATTACHMENT’。也就是有文件上传就会调用upload函数后续对获取的文件名处理了一下,对获取的文件名行一次url解码,对比文件名长度是否有变化,如果有变化,则将url解码后的文件名作为最后的文件名在45行有upload函数,要跟进看一下干了什么,inc/utility_file.php的1321行

$ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);

函数具体代码如下:

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 == UPLOAD_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;}

看下is_uploadable()函数对文件名进行检查,跟进到该函数,同样位于inc/utility_file.php 这个仔细看一下,代码意思是查找 “.” 在文件名中最后一次出现的位置然后

strtolower(substr($FILE_NAME, $POS + 1, 3)) == 'php'

这是 substr( 文件名,最后一次点的位置+1,3个位置)从存在 ”.“ 开始匹配3位,判断后缀是否为php,,如果为php则返回false,否则将"."之前的作为EXT_NAME。 这么判断 .php肯定是不行了,只能是 shell.php. 或者 shell.php.png那么只能是配合文件包含漏洞了

变量传递问题

由于在upload.php中UPLOAD_MODE值的是一个重要的流程走向的判断

但是并没有发现是从哪来的,所以一直很疑惑,

但根据payload中POST的UPLOAD_MODE值可以被正常带入且影响文件上传走向

预测 UPLOAD_MODE值的方法存在于被包含的文件中,

但是UPLOAD_MODE这个参数名仅存在于upload.php中

开始追溯,发现下面的路径

具体调用为upload.php -> session.php -> coon.php -> td_config.php -> common.inc.php

关键部分

if (0 < count($_POST)) {$arr_html_fields = array();foreach ($_POST as $s_key => $s_value) {if (substr($s_key, 0, 7) == '_SERVER') {continue;}if (substr($s_key, 0, 15) != 'TD_HTML_EDITOR_') {if (!is_array($s_value)) {$_POST[$s_key] = addslashes(strip_tags($s_value));}${$s_key} = $_POST[$s_key];} else {if ($s_key == 'TD_HTML_EDITOR_FORM_HTML_DATA' || $s_key == 'TD_HTML_EDITOR_PRCS_IN' || $s_key == 'TD_HTML_EDITOR_PRCS_OUT' || $s_key == 'TD_HTML_EDITOR_QTPL_PRCS_SET' || isset($_POST['ACTION_TYPE']) && ($_POST['ACTION_TYPE'] == 'approve_center' || $_POST['ACTION_TYPE'] == 'workflow' || $_POST['ACTION_TYPE'] == 'sms' || $_POST['ACTION_TYPE'] == 'wiki') && ($s_key == 'CONTENT' || $s_key == 'TD_HTML_EDITOR_CONTENT' || $s_key == 'TD_HTML_EDITOR_TPT_CONTENT')) {unset($_POST[$s_key]);$s_key = $s_key == 'CONTENT' ? $s_key : substr($s_key, 15);${$s_key} = addslashes($s_value);$arr_html_fields[$s_key] = ${$s_key};} else {$encoding = mb_detect_encoding($s_value, 'GBK,UTF-8');unset($_POST[$s_key]);$s_key = substr($s_key, 15);${$s_key} = addslashes(rich_text_clean($s_value, $encoding));$arr_html_fields[$s_key] = ${$s_key};}}}reset($_POST);$_POST = array_merge($_POST, $arr_html_fields);}

首先一开始对POST长度进行了判断,这里_POST长度进行了判断,这里P​OST长度进行了判断,这里_POST实际是一个数组,接着使用foreach函数对数组进行遍历,在这里$_POST数组中key为"UPLOAD_MODE",value为"2",那么根据配会到

if (substr($s_key, 0, 15) != 'TD_HTML_EDITOR_') {if (!is_array($s_value)) {$_POST[$s_key] = addslashes(strip_tags($s_value));}//直接来这${$s_key} = $_POST[$s_key];

最终数组键名UPLOAD_MODE成了了变量名,而他的对应键值成为了变量值

所以 upload.php 未直接接收UPLOAD_MODE值,而我们仍可以传递到这里

upload函数的中 调用 add_attach函数,设置$ATTACHMENTS[‘ID’]

再往后 继续跟进函数add_attach,函数同样位于inc/utility_file.php文件下找到了保存路径的方式

function add_attach($SOURCE_FILE, $ATTACH_NAME, $MODULE, $YM, $ATTACH_SIGN, $ATTACH_ID){$ATTACH_PARA_ARRAY = TD::get_cache("SYS_ATTACH_PARA");$ATTACH_POS_ACTIVE = $ATTACH_PARA_ARRAY["SYS_ATTACH_POS_ACTIVE"];$ATTACH_PATH_ACTIVE = $ATTACH_PARA_ARRAY["SYS_ATTACH_PATH_ACTIVE"];if (!file_exists($SOURCE_FILE)) {return false;}if ($MODULE == "") {$MODULE = attach_sub_dir();}if ($YM == "") {$YM = date("ym");}$PATH = $ATTACH_PATH_ACTIVE . $MODULE;if (!file_exists($PATH) || !is_dir($PATH)) {@mkdir($PATH, 448);}$PATH = $PATH . "/" . $YM;if (!file_exists($PATH) || !is_dir($PATH)) {@mkdir($PATH, 448);}$ATTACH_NAME = (is_default_charset($ATTACH_NAME) ? $ATTACH_NAME : iconv("utf-8", MYOA_CHARSET, $ATTACH_NAME));$EXT_NAME = substr($ATTACH_NAME, strrpos($ATTACH_NAME, "."));$ATTACH_NAME = str_replace($EXT_NAME, strtolower($EXT_NAME), $ATTACH_NAME);$ATTACH_FILE = (MYOA_ATTACH_NAME_FORMAT ? md5($ATTACH_NAME) . ".td" : $ATTACH_NAME);$ATTACH_ID = mt_rand();$FILENAME = $PATH . "/" . $ATTACH_ID . "." . $ATTACH_FILE;if (file_exists($FILENAME)) {$ATTACH_ID = mt_rand();$FILENAME = $PATH . "/" . $ATTACH_ID . "." . $ATTACH_FILE;}$AID = mysql_insert_id();$ATTACH_ID_NEW = $AID . "@" . $YM . "_" . $ATTACH_ID;return $ATTACH_ID_NEW;}

可以看到返回值ATTACHIDNEW有三部分组成ATTACH_ID_NEW有三部分组成ATTACHI​DN​EW有三部分组成AID,YM,YM,YM,ATTACH_ID

其实UPLOAD_MODE值随便为1,2,3中的任意一个数字,都可以返回文件名字和部分路径,不看也行

文件包含部分

这个比较简单文件包含功能的文件位于webroot\ispirit\interface\gateway.php具体代码如下:

<?php//decode by QQ 2859470ob_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;}

这里的参数也是,POST直接传入就可以了,分析在上面也有主要是有这两个个就可以

include_once 'inc/session.php';include_once 'inc/conn.php';

逻辑较为简单,

如果这里不传递参数P为空,就以绕过前面一系列的检测直

随后从json中获取url参数的值

只有 general/、ispirit/、module/ 在url内,在直接包含 $url,

文件包含结束

构造一个就好了

/ispirit/interface/gateway.php?json={"url":"/general/../../attach/im//1153189608.jpg"}

0x03 修复方案

更新官方发布的补丁 /news/673.php

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。