本文针对Struts2一些经典漏洞进行分析和梳理。为什么要分析struts2这些略久远的漏洞呢?最近热议的Spring jdk9 漏洞,其中涉及到Struts2+类加载器漏洞的利用方式,由此有了重新梳理Struts2历史漏洞的想法。漏洞虽然是过去的,但知识点永远是知识点。希望通过对Struts2的一些经典漏洞的分析,获取新的认识和知识。
0x00 S2-001
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-001)
该漏洞是由于WebWork 2.1+ 和 Struts 2 的"altSyntax"特性引起的。“altSyntax"特性允许将OGNL表达式插入文本字符串并进行递归处理。这允许恶意用户通过HTML文本字段提交一个包含OGNL表达式的字符串,如果表单验证失败,服务器将执行该表达式。
影响版本
WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
测试demo
漏洞调用链
经分析,漏洞出现在doEndTag()
处理过程中。当处理到表单中的\>
时会通过org.apache.struts2.views.jsp.ComponentTagSupport
的doEndTag
方法解析相应标签的属性。跟踪到org.apache.struts2.components.UIBean#end
中的evaluateParams
方法,在处理表单中name
为username
的textfield
字段时,将该name包装为%{}
的形式交给findValue
处理。具体代码如下:
分析struts2-core
jar包中的findvalue
方法,该漏洞版本支持altSyntax
特性,并进入TextParseUtil.translateVariables('%', expr, this.stack);
方法处理表达式expr
。
在xwor-2.0-beat-1.jar
(本次漏洞测试环境)中的com.opensymphony.xwork2.util.TextParseUtil#translateVariables()
方法处理表达式中进行递归处理,从而导致用户提交的value进行二次解析,导致S2-001漏洞的触发。
漏洞修复
在com.opensymphony.xwork2.util.TextParseUtil#translateVariables()
中不再对表达式进行递归解析,如此用户输入的value值不会再被解析执行。
说明:本次测试S2-001的demo中并不是因为表单验证失败而触发漏洞,根据调试分析过程可见,触发漏洞根本原因在于,可以递归解析表单中的属性值从而导致用户输入的ognl表达式被执行。
0x01 S2-003 and S2-005
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-003)
漏洞触发源于ParameterInterceptor
拦截器中,可通过构造参数绕过正则限制从而执行ognl表达式。
影响版本
Struts 2.0.0 - Struts 2.1.8.1
漏洞分析
com.opensymphony.xwork2.interceptor.ParametersInterceptor
拦截器通过doIntercept
方法处理参数
跟进setParameters
方法看到熟悉的stack.setValue(name, value);
代码,但中间要经过this.acceptableName(name
进行验证。
可见对传入参数进行正则表达式匹配[\p{Graph}&&[^,#:=]]*
因此,绕过该正则即可实现ognl表达式。在S2-003中,通过unicode编码或八进制对"#","="等字符进行绕过。#
编码为\u0023
或者八进制\43
,=
编码为\u003d
。其中,空格会被转义,因此将空格也用unicode编码为\u0020
。最终poc如下,其中xwork.MethodAccessor.denyMethodExecution
需设置为true
才能执行命令。
测试环境:tomcat8 jdk8
payload:简单构造如下:
('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(t)(c)&('\u0023test\u003d@java.lang.Runtime@getRuntime().exec(\'open\u0020/System/Applications/Calculator.app\')')(a)(b)
S2-005和S2-003的漏洞原理相同,S2-005的出现时因为官方对S2-003的修补的不完全而导致。官方通过增加安全配置禁止静态方法调用(allowStaticMethodAcces
)和类方法执行(MethodAccessor.denyMethodExecution
)等安全配置来修补,但是该方式可以通过OGNL表达式控制开关。
0x02 S2-007
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-007)
当配置了验证规则<ActionName>-validation.xml
时,若类型验证转换出错,后端默认会将用户提交的表单值通过字符串拼接,然后执行一次 OGNL 表达式解析,从而造成远程代码执行。
影响版本
Struts 2.0.0 - Struts 2.2.3
漏洞分析
本次漏洞环境:S2-007
UserAction-validation.xml
文件中设置age类型为int
当输入age为String类型时会经过com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor
拦截器进行拦截处理。
分析com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor
拦截器拦截流程:
1、将验证错误的age
和其value值存在conversionErrors
中
2、通过Object value = entry.getValue();
获取value值,然后通过getOverrideExpr()
处理。
经getOverrideExpr()
处理后在value前后分别拼接'
,这也是为什么payload前后要加'
闭合的原因。
3、关键:com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor
的intercept
方法中设置addPreResultListener
,在beforeResult
方法中通过 invocation.getStack().setExprOverrides(fakie);
将fakie
中对应的ognl
表达式添加到this.overrides
中,后续会用到。
然后与S2-001同样的流程,在处理提交表单的闭合标签时经过
doEndTag --> end --> evaluateParams -->this.findValue(expr, valueClazz) -->TextParseUtil.translateVariables('%', expr, this.stack);
流程处理。
重点分析TextParseUtil.translateVariables('%', expr, this.stack);
方法。由于S2-001的补丁,不会再对expr表达式进行递归处理,因此name、email的value值不会导致ognl表达式执行。TextParseUtil.translateVariables('%', expr, this.stack);
的代码执行流程如下图所示:
其中关键方法定位到tryFindValue
方法
通过lookupForOverrides
方法在this.overrides
中查找key为expr对应的值。由于上述分析,在经过com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor#intercept
处理时将age和其value值put进this.overrides
中。由此得到的expr为注入到age值的ognl表达式,通过this.getValue(expr,asType)
成功执行。
漏洞修复
在getOverrideExpr
函数中进行了StringEscape
,从而无法闭合单引号,也就无法构造OGNL表达式
0x03 S2-009
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-009)
ParametersInterceptor拦截器只检查传入的参数名是否合法,不会检查参数值。因此可先将Payload设置为参数值注入到上下文中,而后通过某个特定语法取出来就可以执行之前设置过的Payload。
影响版本
Struts 2.0.0 - Struts 2.3.1.1
漏洞分析
和S2-003/S2-005一样都是由ParametersInterceptor
拦截器引起的。在S2-009是由于ParametersInterceptor
拦截器在只检查参数名是否合法,而不检查参数值,从而可通过设计参数名与参数值执行ognl表达式。
com.opensymphony.xwork2.interceptor.ParametersInterceptor#doIntercept
拦截方法中通过this.setParameters(action, stack, parameters);
设置参数。分析setParameters
方法:
通过acceptableName(name)
方法判断参数名是否合规,合规则将该参数名和参数值加入到acceptableParameters
中。
this.acceptedPattern=[a-zA-Z0-9\.\]\[\(\)_'\s]+
this.excludeParams=[dojo.., ^struts..]
以上可见,只要参数值符合[a-zA-Z0-9\.\]\[\(\)_'\s]+
的正则表达且不包含[dojo\..*, ^struts\..*]
则可通过合法校验。而后,从acceptableParameters
中依次取出参数,通过newStack.setValue(name, value);
处理,从而触发ognl表达式执行。
newStack.setValue(name, value);
触发ognl表达式执行流程:
最终到Ognl.setValue(this.compile(name), context, root, value);
执行完compile方法,再执行setValue方法,参数为四个:语法树、上下文、根对象以及传入的value值。
解释下payload为何这么写:
1、foo=(#context["xwork.MethodAccessor.denyMethodExecution"]= new java.lang.Boolean(false), #_memberAccess["allowStaticMethodAccess"]= new java.lang.Boolean(true), @java.lang.Runtime@getRuntime().exec('open /System/Applications/Calculator.app'))(meh)
2、z[(foo)(aa)]=true
分别对应:
value1=(expression1)(constant)
z[(expression2)(constant)]=value2
由于Ognl解析引擎是这样处理的,每个括号对应语法树上的一个分支,并且从最右边的叶子节点开始解析执行。对于value1=(expression1)(constant)
会执行value1=expression1
。对于z[(expression2)(constant)]=value2
则执行expression2=value2
。对于本payload,expression2
为foo
,已经赋值为ognl表达式,因此成功将ongl表达式带入参数key,变相绕过前方对参数名的合法校验,从而执行ongl表达式。
0x04 S2-013
漏洞影响
Struts 2.0.0 - Struts 2.3.14.1
漏洞分析
该漏洞同样与标签相关。Struts2 标签中 <s:a>
和 <s:url>
都包含一个 includeParams 属性,其值可设置为 none,get 或 all。<s:a>
用来显示一个超链接,当includeParams=all
的时候,会将本次请求的GET和POST参数都放在URL的GET参数上。在放置参数的过程中会将参数进行OGNL渲染,造成任意命令执行漏洞。
本次漏洞分析demo——index.jsp
与S2-001相同,按照doStartTag()
——> doEndTag()
的顺序处理标签。
首先分析doStartTag()
方法。
通过this.getBean
获取该标签的component
对象,这里<s:url>
标签对应的是org.apache.struts2.components.URL
对象,并进行初始化。然后经container.inject(this.component);
调用对应component
对象中经@Inject
注解的方法,再通过this.component.start
方法处理。
在org.apache.struts2.components.URL
的start
方法中,
这里this.urlRenderer
的includeParams
值被设置为all
。这里其值也可设置为 none,get 。参考官方其对应意义如下:
1. none - 链接不包含请求的任意参数值(默认)
2. get - 链接只包含 GET 请求中的参数和其值
3. all - 链接包含 GET 和 POST 所有参数和其值
跟踪进入beforeRenderUrl()
方法。通过mergeRequestParameters
方法将请求参数放入parameters
中。
补充一句,这里如果includeParams
值被设置为get
,也可通过this.includeGetParameters(urlComponent);
方法将get请求的请求参数放入parameters
中,从而触发漏洞。唯一不同是,all
可添加post请求的参数,而get
不可以。
接下来到了doEndTag()
方法。进入org.apache.struts2.components.URL
的end
方法。
跟进renderUrl
->determineActionURL
->buildURL
,参数parameters
为上面通过beforeRenderUrl()
方法处理后得到的map。
继续跟进到org.apache.struts2.views.util.UrlHelper#buildParametersString()
方法,将parameters
中的key依次取出,再进入buildParametersString
() -> translateAndEncode()
。
通过translateAndEncode()
处理get请求的参数名,看到熟悉的translateVariable()
方法,后面就是正常的ongl表达式解析流程了:translateVariable
()->TextParseUtil.translateVariables()
->translateVariables()
->stack.findValue(var, asType)
-> ...->Ognl.getValue()
调用链:
poc
${#_memberAccess["allowStaticMethodAccess"]=true,#a=@java.lang.Runtime@getRuntime().exec('open /System/Applications/Calculator.app')}
0x05 S2-016
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-016)
在struts2
中,DefaultActionMapper
类支持以action:
、redirect:
、redirectAction:
作为导航或是重定向前缀,但是这些前缀后面同时可以跟OGNL
表达式,由于struts2
没有对这些前缀做过滤,导致利用OGNL
表达式调用java
静态方法执行任意系统命令。
影响范围
Struts 2.0.0 - Struts 2.3.15
漏洞分析
经org.apache.struts2.dispatcher.FilterDispatcher
的doFilter
方法拦截用户请求,doFilter
创建值栈、上下文、包装request等一些初始化操作,然后进入DefaultActionMapper
的getMapping()
方法。
通过uri
解析对应action的namespace
、actionname
等配置信息。再进入handleSpecialParameters
方法处理请求参数。在handleSpecialParameters
中通过parameterAction.execute(key, mapping);
进入DefaultActionMapper
处理相应逻辑。
redirect.setLocation(key.substring("redirect:".length()));
将请求参数redirect:
后的内容设置为location
,此时key
值为redirect:ognl
表达式,即ognl表达式被设置为
location,然后将该
ServletRedirectResult类型且
location为ognl表达式的
redirect对象设置为该
mapping的
Result。然后关注
location的值是如何被执行的。
逻辑回到
FilterDispatcher的
doFilter方法,通过
this.dispatcher.serviceAction进入Dispatcher
的serviceAction
方法继续。
通过mapping.getResult();
获取该mapping
的result
对象,也就是上面的ServletRedirectResult
类型且location
为ognl表达式的redirect
对象。进入ServletRedirectResult
的execute
方法,一直跟踪到org.apache.struts2.dispatcher.StrutsResultSupport
的execute
方法。可见通过conditionalParse()
方法处理location
,最后经TextParseUtil.translateVariables()
处理,从而执行location
中的ognl表达式。
0x06 s2-045
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-045)
在使用基于Jakarta插件的文件上传功能时,恶意用户可在上传文件时通过修改HTTP请求头中的Content-Type值来触发该漏洞,进而执行系统命令。
漏洞影响
Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10
漏洞分析
在Struts2中,所有的请求都会经过FilterDispatcher
的doFilter
方法拦截处理,doFilter
创建值栈、上下文、包装request等一些初始化操作。自 Struts 2.5
以上已经将这个换成了 StrutsPrepareAndExecuteFilter
。在StrutsPrepareAndExecuteFilter
的doFilter
方法中通过this.prepare.wrapRequest(request);
处理请求,一直跟踪到org.apache.struts2.dispatcher.Dispatcher
的wrapRequest()
方法,该方法判断请求的ContentType
是否包含multipart/form-data
,如果包含,则进入new MultiPartRequestWrapper()
进行初始化。
跟进MultiPartRequestWrapper
的构造方法,在this.multi.parse(request, saveDir);
解析request
。其中this.multi
为JakartaMultiPartRequest
类的对象,跟进该对象的parse
方法,在处理恶意构造的Content-Type
数据时抛出异常,通过buildErrorMessage
方法处理异常信息。
跟进buildErrorMessage
方法,通过LocalizedTextUtil.findText()
处理包含异常信息,其中异常信息完整包含了恶意构造的ognl表达式。
后续流程com.opensymphony.xwork2.util.LocalizedTextUtil的findText()
->getDefaultMessage()
->TextParseUtil.translateVariables()
方法执行表达式。
S2-045是在Content-Type
值注入ognl表达式从而引起解析异常,S2-046则是在上传文件的Content-Disposition
中的Filename
参数存在空字节,在检查时抛出异常,从而进入buildErrorMessage()方法。实则S2-045
和S2-046
漏洞原理相同。
0x07 s2-053
漏洞描述
Struts2在使用Freemarker模板引擎渲染用户输入时,渲染后的ognl表达式可被执行。
漏洞影响
Struts 2.0.1 - Struts 2.3.33, Struts 2.5 - Struts 2.5.10
漏洞分析
本漏洞实质和S2-001原理是一样的。都是在解析标签时在org.apache.struts2.components.UIBean
的end()
方法中通过evaluateParams()
对标签中的值进行解析从而执行ongl表达式的。在S2-053漏洞场景中,是通过Freemarker模版引擎渲染用户输入数据。如本demo场景中:通过Freemarker渲染index.ftl文件。
index.ftl文件内容:
这里message
对应IndexAction
中的message
:
当submit用户输入的message
时,经IndexAction
的setMessage()
处理后重新利用Freemarker渲染index.ftl文件。此时${message}
渲染为用户输入的包含ognl表达式的内容。
也即<@s.hidden name="${message}"><!--@s.hidden-->
的内容为<@s.hidden name="ognl表达式"><!--@s.hidden-->
,由此后续与S2-001流程一样,org.apache.struts2.components.UIBean#end()
->evaluateParams()
->findString()
->findValue()
->com.opensymphony.xwork2.util.TextParseUtil#translateVariables()
poc:
%25%7B%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23_memberAccess%3F%28%23_memberAccess%3D%23dm%29%3A%28%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ognlUtil%3D%23container.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ognlUtil.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ognlUtil.getExcludedClasses%28%29.clear%28%29%29.%28%23context.setMemberAccess%28%23dm%29%29%29%29.%28%23cmd%3D%27id%27%29.%28%23iswin%3D%28%40java.lang.System%40getProperty%28%27os.name%27%29.toLowerCase%28%29.contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd.exe%27%2C%27%2Fc%27%2C%23cmd%7D%3A%7B%27%2Fbin%2Fbash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew+java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%40org.apache.commons.io.IOUtils%40toString%28%23process.getInputStream%28%29%29%29%7D%0D%0A
0x08 s2-059
漏洞描述(https://cwiki.apache.org/confluence/display/WW/S2-059)
Struts2 会对某些标签属性(比如id
,其他属性有待寻找)的属性值进行二次表达式解析,当这些标签属性中包含了%{xx}
且xx
用户可控,则在渲染该标签时可造成ognl表达式执行。
漏洞影响
Struts 2.0.0 - Struts 2.5.20
漏洞分析
漏洞分析场景:index.jsp
其中skillName
是IndexAction
的属性值
首先通过com.opensymphony.xwork2.interceptor.ParametersInterceptor
的setParameters()
方法设置对应的IndexAction
的skillName
属性,即%{3*5}
。然后进入jsp中的标签处理环节。处理<s:label id="%{skillName}" value="label test"/>
时,与S2-001一样,首先通过org.apache.struts2.views.jsp.ComponentTagSupport
的doStartTag()
方法根据不同标签属性进行初始化处理。跟踪populateParams()
方法。
本demo中标签属性是label
,因此进入LabelTag
的populateParams()
方法,随后进入org.apache.struts2.views.jsp.ui.AbstractUITag
的populateParams()
方法。该方法对所有可能涉及到的属性,如name
,theme
,value
等进行set处理。
跟进org.apache.struts2.components.UIBean
的setId
()方法,通过this.findString(id)
对id进行第一次解析,结果为%{skillName}
,由此完成对this.id
的初始化赋值。
然后进入org.apache.struts2.views.jsp.ComponentTagSupport
的doEndTag()
方法完成对标签的闭合处理。流程同s2-001,doEndTag()
->UIBean.end()
->UIBean.evaluateParams()
在UIBean.evaluateParams()
中进入populateComponentHtmlId()
方法。通过findStringIfAltSyntax()
对id
进行第二次解析。此时this.id
经过上面处理已经是skillName
的值,也就是%{3*5}
。
跟进findStringIfAltSyntax()
。this.altSyntax()
默认为true,因此通过this.findString(expr)
->findValue()
->TextParseUtil.translateVariables('%', expr, this.stack);
执行ognl表达式。
调用链如图:
命令回显:
poc:
%25%7B%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23_memberAccess%3F%28%23_memberAccess%3D%23dm%29%3A%28%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ognlUtil%3D%23container.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ognlUtil.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ognlUtil.getExcludedClasses%28%29.clear%28%29%29.%28%23context.setMemberAccess%28%23dm%29%29%29%29.%28%23cmd%3D%27id%27%29.%28%23iswin%3D%28%40java.lang.System%40getProperty%28%27os.name%27%29.toLowerCase%28%29.contains%28%27win%27%29%29%29.%28%23cmds%3D%28%23iswin%3F%7B%27cmd.exe%27%2C%27%2Fc%27%2C%23cmd%7D%3A%7B%27%2Fbin%2Fbash%27%2C%27-c%27%2C%23cmd%7D%29%29.%28%23p%3Dnew+java.lang.ProcessBuilder%28%23cmds%29%29.%28%23p.redirectErrorStream%28true%29%29.%28%23process%3D%23p.start%28%29%29.%28%40org.apache.commons.io.IOUtils%40toString%28%23process.getInputStream%28%29%29%29%7D%0D%0A
<sub>~</sub>~
0x09 S2-061和S2-062
S2-061和S2-062漏洞原理与S2-059相同,是对S2-059沙盒绕过。由于漏洞原理方面并无不同,这里不再阐述。
总结
大致对Struts2历史漏洞做了个汇总和梳理,在整理跟踪过程中发现,Struts2各种漏洞的触发大多在于各种拦截器、标签属性处理过程中。后续则是对沙盒的各种绕过,譬如S2-061和S2-062。