作者:kw0ng
开始
通达OA上传到包含漏洞分析的文章已经有很多,本文重点分析,文件上传处决定路径信息是否回显的UPLOAD_MODE参数是怎么传递的。
代码分析
触发文件上传点位于/ispirit/im/upload.php中,服务端在接收文件信息的同时还需要几个参数,如不了解 multipart/form-data类型如何传递参数的可以先去学习下,通达OA的PHP代码采用了Zend54加密,但是我使用两个不同的工具解密却得到了不一致的代码,这也导致了这篇文章差点难产。首先是第一段代码:
$P = $_POST['P'];if (isset($P) || $P != '') { ob_start(); include_once 'inc/session.php'; //如果P不为空 session_id($P); session_start(); session_write_close();} else { include_once './auth.php';}
这段代码需要一个POST传参获取P值,且为空时将会包含auth.php进行重认证,因为我们的POST包需要一个P值且不为空。
$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; }}
这段代码中我们需要提供DEST_UID参数,且如果DEST_UID为0,则触发判断UPLOAD_MODE是否等于2,不等于2则报error并退出,因此将DEST_UID不等于0可绕过。
if (1 <= count($_FILES)) { //返回数组中元素的数目: if ($UPLOAD_MODE == '1') { //如果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;}
判断1 <= count($FILES) 就是$FILES是否取到上传数据。
判断UPLOAD_MODE是否等于1,如果等于1且文件名url解码后的长度不等于原长度则将文件名url解码。
调用upload函数$ATTACHMENTS = upload('ATTACHMENT', $MODULE, false);传入3个参数,$MODULE = 'im';。
使用PHP的$FILES函数来获取我们上传的文件信息$FILES['ATTACHMENT']['name'] $_FILES的第一个下标必须是我们的input name值,因此我们的POST包的Content-Disposition: form-data; name="ATTACHMENT"; filename="jpg"中的name必须是'ATTACHMENT'。
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;}
本段代码说明了UPLOAD_MODE等于2或3的时候会返回$CONTENT或$ATTACHMENT_ID变量的值(1也可以),且返回值与最终文件上传所在的路径、文件名是相关的。
综上所述我们得到了3个信息
存在P值且不为空 ($_POST["P"]获取)
DEST_UID不为0 ($POST["DEST_UID"]获取)
UPLOAD_MODE = 1或2或3 (非$POST["UPLOAD_MODE"]获取)
Content-Disposition中name值为"ATTACHMENT"
流程图:
变量覆盖
由于在upload.php中未找到取UPLOAD_MODE值的方法,但在实际测试中POST的UPLOAD_MODE值可以被正常带入且影响文件上传走向,因此判断接收UPLOAD_MODE值的方法存在于被包含的文件中,在解密后使用搜索工具也仅发现UPLOAD_MODE这个参数名仅存在于upload.php中,开始追溯之旅最后在common.inc.php文件中发现了UPLOAD_MODE的源头.
具体调用为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实际是一个数组,存储了客户端传递的参数与值,接着使用foreach函数对数组进行遍历,foreach函数也是CTF变量覆盖漏洞经常考察的知识点,在这里我们假设$_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];
关键代码:
${$s_key}=$_POST[$s_key];
方便大家理解我使用IDE将这段代码运行下.
可以看到最终数组键名UPLOAD_MODE成了了变量名,而他的对应键值成为了变量值.
这也就是upload.php未直接接收UPLOAD_MODE值,而我们仍可以传递此参数影响函数走向的原因.
RCE
在明白了上传流程后,我们还需要配合本次的包含漏洞来包含我们上传的代码实现RCE,文件包含利用点在/ispirit/interface/gateway.php文件中. 首先
if ($P != "") { if (preg_match("/[^a-z0-9;]+/i", $P)) { echo _("非法参数"); exit(); }
参数P要为空值.接着
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();}
由于存在json_decode($json);因此我们需要构造一个json,以及一个key为url且含有general/或ispirit/或module/的值,让函数走到include_once $url;处就完成了文件包含,至于$json从哪来,我想看到这你就明白了.
include_once "inc/session.php";include_once "inc/conn.php"
关于我们
蛇獴攻防实验室成立于2019年11月,专注网络攻防技术。
地点:吉林·长春