简述
最近爆出了通达OA的两枚漏洞,文件上传漏洞为全版本通杀,文件包含漏洞只有V11.3版本存在。
通过绕过身份认证, 攻击者可上传任意文件,配合文件包含即可出发远程恶意代码执行。
复现环境
通达OA V11.3 密码:ordv
源码使用zend5.4加密,使用SeayDzend解密工具解密。
由于源码文件太多,可以只解密造成漏洞的关键文件。
/ispirit/im/upload.php
inc/utility_file.php
ispirit/interface/gateway.php
这里分析一下全部解密后的源码:链接提取码:p9av
漏洞分析
1、文件包含
问题代码在gateway.php
文件中,有一处文件包含,满足条件就会包含url。
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 == "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
进行了是否为空、正则校验以及当前用户是否登录。
$json
参数接受一个json格式的值,然后转化为数组。所以只要在传入的json数据中使url参数中包含ispirit/
、general/
、module/
就可以通过跨目录进行包含。
payload:payload:json={"url":"xxx"}
2、文件上传
文件上传的入口在/ispirit/im/upload.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";
}
先检查有没有POST参数P,P存在而且内容不为空则获取session,否则进行身份认证。
第20行开始又有两个POST传递的参数:
$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();
}
}
$TYPE
和$DEST_UID
都是通过POST的方式传递进来的,然后要求$DEST_UID
不为空,同时如果$DEST_UID
为0的话$UPLOAD_MODE
要是2。
然后继续进入文件上传的部分:
$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);
如果有文件上传进入upload函数,否则报错无文件上传。从上面代码知道文件上传的变量名为ATTACHMENT
,如果$UPLOAD_MODE
为1,会处理一下name参数的编码问题。然后跟进upload函数,进入inc/utility_file.php
文件,第1665行。
该函数经过一系列参数检查,文件检查等检查后返回一个数组ATTACHMENTS
。
#1691
if (!is_uploadable($ATTACH_NAME)) {
$ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
}
#1705
if (preg_match("/[\':<>?]|\/|\\\\|\"|\|/u", $ATTACH_NAME_UTF8)) {
$ERROR_DESC = sprintf(_("文件名[%s]包含[/\'\":*?<>|]等非法字符"), $ATTACH_NAME);
}
这里调用is_uploadable
函数进行上传检查。
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;
}
函数中寻找最后一个 .
的位置后三个字符,小写后看是否匹配字符’php’,匹配则返回false
,还通过find_id
函数进行黑名单的检查。
然后看一下保存路径问题,$UPLOAD_MODE
所需要的$ATTACHMENT_ID
等参数来自于$ATTACHMENTS
,而$ATTACHMENTS
则是调用upload函数的返回结果:upload.php
#61
$ATTACHMENT_ID = substr($ATTACHMENTS["ID"], 0, -1);
$ATTACHMENT_NAME = substr($ATTACHMENTS["NAME"], 0, -1);
#82
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);
}
inc/utility_file.php文件1713行
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 . "*";
}
}
ATTACHMENTS["ID"]
来源于add_attach函数,位于inc/utility_file.php文件1854行
$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);
}
$FILENAME = $PATH . "/" . $ATTACH_ID . "." . $ATTACH_FILE;
if (file_exists($FILENAME)) {
$ATTACH_ID = mt_rand();
$FILENAME = $PATH . "/" . $ATTACH_ID . "." . $ATTACH_FILE;
}
有$PATH
和$FILENAME
,然后看返回的内容
$ATTACH_ID_NEW = $AID . "@" . $YM . "_" . $ATTACH_ID;
if (is_office($ATTACH_NAME) && ($ATTACH_SIGN != 0)) {
$ATTACH_ID_NEW .= "." . $ATTACH_SIGN;
}
return $ATTACH_ID_NEW;