通达OA任意文件上传&文件包含


简述

最近爆出了通达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;

文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
  目录