引子
2023-02-23,某站發布了一個關于泛微e-cology9 SQL注入的漏洞通告。

如上圖所示,根據其說明,受影響的版本范圍是<=10.55版本。
另外,他們還提到該漏洞無權限要求,并不是后臺洞。
補丁包對比
E-COLOGY安全補丁下載網址如下:
https://www.weaver.com.cn/cs/securityDownload.html?src=cn
通過如下兩個鏈接,下載該次漏洞的以及上一個版本的補丁包:
v10.55:
https://www.weaver.com.cn/cs/package/Ecology_security_20221014_v10.55.zip
v10.56:
https://www.weaver.com.cn/cs/package/Ecology_security_20230213_v10.56.zip
將兩個補丁壓縮包分別解壓,然后使用IDEA工具對比差異。

這里對比看了很久,但是卻沒有看出有價值的內容。
嗯?先了解下web.xml文件中的內容。

在開頭存在一個SecurityFilter的過濾器,SecurityFilter在初始化時會調用weaver.security.filter.SecurityMain中的initFilterBean方法初始化安全規則。而在weaver.security.rules.ruleImp包中的每個類差不多就是每次打的補丁,此包中的類將被重點關注。
所以,縮小范圍去補丁包的WEB-INF/myclasses/weaver/security/rules/ruleImp目錄尋找。并且我們還可以根據文件時間戳,將2022年10月前的補丁文件都排除在外,繼續過濾一遍。
security/rules/ruleImp? stat -f %SB----%N *.class | grep -v "2021----" | grep -v -E '^[Jan|Mar|Apr|May|Jul|Aug|Sep].*2022' | wc -l
125
這樣還剩下125個補丁文件,然后通過jadx這款反編譯工具一次性打開這些補丁文件,然后搜索Xss(Validate failed關鍵詞,不斷尋找,最終找到了一段SQL注入漏洞的補丁代碼,下圖所框疑似就是本次漏洞的位置。

上圖所示的補丁代碼,所處如下位置。
$ Ecology_security_20230213_v10.56 ? fd SecurityRuleForMobileBrowser WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class
然而在v10.55補丁包中也同樣發現該文件所在,并且是一摸一樣的。
$ Ecology_security_20221014_v10.55 ? fd SecurityRuleForMobileBrowser
WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class
$ patch ? md5 Ecology_security_20230213_v10.56/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class Ecology_security_20221014_v10.55/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class | awk -F "=" '{print $2}'
391d53bb28cffa1bf6974e21eac16b7d
391d53bb28cffa1bf6974e21eac16b7d
查看這兩個文件的時間戳,發現最后修改時間也是一致,都是2022年12月8日。
$ patch ? stat -f %SB Ecology_security_20230213_v10.56/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class Ecology_security_20221014_v10.55/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleForMobileBrowser.class Dec 8 19:55:26 2022 Dec 8 19:55:26 2022
但是在下載v10.55補丁包的時候,通過其下載鏈接,可以得知此版本補丁包的發布日期是2022年10月14日。是早于其中的SecurityRuleForMobileBrowser.class文件的最后修改時間的。
https://www.weaver.com.cn/cs/package/Ecology_security_20221014_v10.55.zip
在通過關于這個漏洞的通告中的時間線中,大致可以猜測出來,在2022年9月,正值演練期間收到該漏洞,在2022月12月上報給監管單位,監管單位應該是收到了該漏洞就立馬通知給廠商,在2022年12月8日廠商就已經開發出本次SQL注入漏洞的補丁代碼,也就是SecurityRuleForMobileBrowser.class文件中的內容。但廠商在開發出該漏洞的補丁代碼后并未立即發布v10.56補丁包,而是先將其更新至v10.55補丁包中了。

這也就是為什么用IDEA對比v10.55和v10.56補丁包,卻一無所獲的原因。
那么既然如此,拿v10.54補丁包與v10.56補丁包對比呢?v10.54補丁包下載地址如下:
https://www.weaver.com.cn/cs/package/Ecology_security_20220805_v10.54.zip
如下圖所示,確實對比出了該文件存在于V10.56中,而不存在于V10.54中。

確定漏洞位置
雖然通過如上的補丁包對比分析找到了一個疑似的漏洞路徑,但是未必就能肯定這是真正的漏洞位置。
我們先來簡單看看/mobile/plugin/browser.jsp的內容。

參數很多,繼續往下看,發現一個isDis參數及其判斷語句。
boolean isDis = "1".equals(Util.null2String(request.getParameter("isDis"))) ? true : false;
if (!isDis) {
request.getRequestDispatcher("/mobile/plugin/dialog.jsp").forward(request, response);
return;
}
如果isDis參數值不為1的話,則進入if條件語句之中,RequestDispatcher的作用是將請求分配給另一個資源,后面使用的是forward方法,此處就是將請求轉發到/mobile/plugin/dialog.jsp處理。那么就看看/mobile/plugin/dialog.jsp的內容。

發現開頭的HrmUserVarify.getUser,該方法部分代碼如下:

可以看出此處有個登錄判斷,根據我們的情況肯定會返回null到dialog.jsp就直接返回空了。死路一條,棄之。
回到上面,現在可以確定的是該參數是必須需要存在的,且參數值還得必須為1。不妨構造一個請求發送看看。
POST /mobile/plugin/browser.jsp HTTP/1.1 Host: Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en-US;q=0.9,en;q=0.8 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 7 isDis=1

與網站放出的測試圖相比,是不是就對上了。那么也就能夠確定漏洞的路徑就是:/mobile/plugin/browser.jsp。
在使用BurpSuite Intruder多個不同的目標時,發現除了200的狀態碼,還有很多404的,這種情況我們最后再說。

補丁代碼分析
通過前面兩段內容的相互印證,可以確定本次SQL注入的漏洞補丁代碼就是SecurityRuleForMobileBrowser.class文件的內容。
完整補丁代碼內容如下。

對該段補丁代碼作簡單分析。從第5行的第一個if條件語句開始,判斷如果../、\、十六進制的00都不存在于URI中,則進入第6行下一個if條件語句判斷,如果/mobile/、/plugin/、/browser.jsp都存在于URI中,那么獲取keyword參數值;接著進入到第9行try語句,首先對keyword參數值進行了一次URL解碼,并判斷其中是否有惡意SQL注入payload ',如果有的話,則進行攔截、拉黑IP,返回false;緊接著對keyword參數值進行二次URL解碼,并將二次URL解碼后的值與第一次URL解碼的值將比較,如果不一致也會進行攔截、拉黑IP,返回false,此處判斷是為了防止多層URL編碼Bypass的,即第一次URL解碼的結果必須是最終的的解碼結果。
通過分析可以得知存在注入的參數就是/mobile/plugin/browser.jsp中的keyword參數。
SQL注入分析
現在繼續關注/mobile/plugin/browser.jsp中的內容。剛剛說到對isDis的判斷,繼續往下看。
String f_weaver_belongto_userid=Util.null2String(request.getParameter("f_weaver_belongto_userid"));//需要增加的代碼
String f_weaver_belongto_usertype=Util.null2String(request.getParameter("f_weaver_belongto_usertype"));//需要增加的代碼
User user = HrmUserVarify.getUser(request, response, f_weaver_belongto_userid, f_weaver_belongto_usertype) ;//需要增加的代碼
BrowserAction braction = new BrowserAction(user, browserTypeId, pageNo, pageSize);
跟進HrmUserVarify.getUser,代碼如下,這里肯定會返回null。

起初這個地方讓我感到很迷惑,誤以為這個鑒權會被用到,實際并不會用到,這里返回的null作為browser.jsp中的user變量的值,但是在browser.jsp中并未對user做檢查。如果需要達到鑒權的效果,那么正確的寫法應該是增加如下代碼片段:
if(user == null) return ;
如下圖所示,SearchSubDept.jsp正是采用的這種寫法。

繼續往下,設置了很多參數值,但不過大部分參數都不是必須的。

最后到braction.getBrowserData()方法,這個方法位于classbean/weaver/mobile/webservices/common/BrowserAction.class文件。

最開始有對browserTypeId參數值進行判斷,以及很多list開頭的方法,接著還對method參數值進行判斷,根據不同的值執行不同的list開頭的方法,那么注入很大可能就存在某個list開頭的方法之中。
先簡單嘗試注入一下,請求如下:
POST /mobile/plugin/browser.jsp HTTP/1.1 Host: Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 54 isDis=1&browserTypeId=160&keyword=a%' union select 1,'

可以發現一些與SQL注入相關的敏感關鍵詞('、select)均被全角化了。那嘗試URL編碼一下,一層URL編碼的請求如下,服務器直接返回500了。

雙層URL編碼試試,如下圖所示,服務器端依舊做了兩次URL解碼,然后發現',將其轉換成全角字符了,但不過select關鍵詞卻沒有被全角化。

繼續三層URL編碼,通過下圖可以發現,經過三層編碼后的字符串被URL解碼兩次后順利到達BrowserAction.getBrowserData(),而在每一個list開頭的方法中都有一次URL解碼操作,那么就能確保我們的SQL注入payload順利傳遞到SQL查詢語句中。

下面依次對各個list開頭的方法進行審計,最后發現當browserTypeId等于269時,執行的listRemindType()方法中存在一個有回顯的SQL注入漏洞。
public void listRemindType() {
try {
new MeetingBrowser();
this.keyword = URLDecoder.decode(this.keyword, "UTF-8");
if (this.pageInfo.getPageNo() > 0) {
this.pageInfo.setPageNo(this.pageInfo.getPageNo());
}
RecordSet var1 = new RecordSet();
String var2 = " select count(0) as count from meeting_remind_type t1 where isuse=1 ";
if (StringUtils.isNotEmpty(this.keyword)) {
var2 = var2 + " and name like '%" + this.keyword + "%'";
}
var1.executeSql(var2);
int var3 = 0;
if (var1.next()) {
var3 = Util.getIntValue(var1.getString("count"), 0);
}
this.pageInfo.setTotalCount(var3);
String var4 = " from meeting_remind_type t1 ";
String var5 = "t1.id as id,t1.name as name";
String var6 = " where isuse=1 ";
if (StringUtils.isNotEmpty(this.keyword)) {
var6 = var6 + " and t1.name like '%" + this.keyword + "%' ";
}
String var7 = "t1.id";
String var8 = "t1.id";
log.info("select " + var5 + var4 + " where " + var6);
this.pageInfo.setResult(this.getLimitPageData(var5, var4, var6, var7, var8, 1, this.pageInfo.getPageNo(), this.pageInfo.getPageSize(), 3));
} catch (Exception var9) {
var9.printStackTrace();
}
}
看到這里就可以直接構造注入payload了,最終注入的效果如下所示。


認證繞過分析
在上面有埋下一個坑,就是在使用BurpSuite Intruder請求多個不同的目標的/mobile/plugin/browser.jsp時,發現有很多返回404狀態碼的站。

我們需要再次對比v10.54和v10.56的補丁包。

找到SecurityRuleMobile29.class這么一個補丁文件,再次對比,首先可以發現在v10.54補丁包中的 SecurityRuleMobile29.class文件的最后修改時間是2020年9月10日,那么如果ecology沒有打過這個補丁則無需考慮繞過的情況,/mobile/plugin/browser.jsp路徑可以被直接訪問,這也就是為什么有一些站直接訪問該路徑不會出現404的原因。
# stat -f %SB Ecology_security_20220805_v10.54/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleMobile29.class Ecology_security_20230213_v10.56/WEB-INF/myclasses/weaver/security/rules/ruleImp/SecurityRuleMobile29.class Sep 10 09:51:04 2020 Dec 7 20:26:20 2022

未更新該補丁之前的validate方法內容如下圖。

我們先從153行的else語句開始看起。毫無疑問,當請求路徑中存在/mobilemode/或/mobile/或/cpt/其中一個,并且請求路徑的結尾是.jsp時,順利進入到154行if分支。
else {
if (path.indexOf("/mobilemode/") != -1 || path.indexOf("/mobile/") != -1 || path.indexOf("/cpt/") != -1 && path.endsWith(".jsp")) {
List mobileNoLoginUrlList = (List)sc.getRule().get("mobile-no-login-urls");
if (mobileNoLoginUrlList != null && !mobileNoLoginUrlList.isEmpty()) {
Iterator var7 = mobileNoLoginUrlList.iterator();
while(var7.hasNext()) {
String url = (String)var7.next();
if (path.indexOf(url) != -1) {
return true;
}
}
}
List mobileNeedLoginUrlList = (List)sc.getRule().get("mobile-need-login-urls");
if (mobileNeedLoginUrlList != null && !mobileNeedLoginUrlList.isEmpty()) {
Iterator var8 = mobileNeedLoginUrlList.iterator();
while(var8.hasNext()) {
String url = (String)var8.next();
if (path.indexOf(url) != -1) {
boolean isLogin = this.isLogin(req, res);
if (!isLogin) {
sc.writeLog(">>>>Xss(Validate failed[Not Login]) validateClass=weaver.security.rules.SecurityRuleMobile29 path=" + req.getRequestURI() + " security validate failed! source ip:" + ThreadVarManager.getIp());
return false;
}
}
}
}
}
return true;
}
接下來,mobileNoLoginUrlList這個列表中的路徑意味著無需登錄即可直接訪問,如果請求的路徑在該列表中,則會返回true。

mobileNeedLoginUrlList列表顧名思義,當請求的路徑在該列表中,則是需要登錄才能訪問的,否則就會返回false。而/mobile/plugin/browser.jsp恰巧在其之中。

當請求的路徑既不屬于mobileNoLoginUrlList,也不屬于mobileNeedLoginUrlList,也是會返回true的。
那么我們繼續看validate方法中新增的補丁代碼片段:

else if (StringUtil.matches(path, "\\s") && StringUtil.matches(path, "/\\s+/")) {
sc.writeLog(">>>>Xss(Validate failed[invalidate url]) validateClass=weaver.security.rules.SecurityRuleMobile29 path=" + req.getRequestURL() + " security validate failed! source ip:" + ThreadVarManager.getIp());
return false;
}
這里做了一個正則匹配,如果請求路徑中存在空白字符,就會觸發安全補丁的警告,并返回false。
在此之前,需要留意super.path方法,這個是父類ParentRule中的方法。

path方法會對請求路徑中出現的一些特殊字符如;、//,那么則會做正則replace。并且最后還會去除路徑中出現的空白字符。最后返回path.toLowerCase()。
但是這里過濾的并不全,不然就不會出現補丁代碼中的又一遍的檢查了。
StringUtil.matches(path, "\\s") && StringUtil.matches(path, "/\\s+/")
所以這里必定是存在繞過的,別忘了path方法中開始會使用uriDecode方法對請求路徑做URL解碼的哦。
最后梳理下,原始的請求路徑需要先經過URL解碼,解碼后其中不要存在有;、//字符,然后還要經過一遍去空白字符操作,再然后就會返回最終的路徑,最后的路徑能到達/mobile/plugin/browser.jsp。

成功繞過后,就能對未打這次最新補丁的ecology進行SQL注入了。

HACK學習呀
骨哥說事
雁行安全團隊
合天網安實驗室
黑白之道
LemonSec
HACK之道
安全圈
HACK學習呀
HACK學習呀
聚銘網絡
系統安全運維