struts2系列漏洞 S2-002

前言

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


环境搭建

环境搭建参考 S2-001,我这里使用的是 2.0.9 版本的 struts2。

修改一下模板和 Action 代码,模板加上两个漏洞触发点:

1
2
<s:a href="%{url}">click</s:a>
<s:url includeParams="all"> </s:url>

Action 加上 url 变量。

漏洞利用

阅读官方通告,可以看出漏洞原因就是 s:url 和 s:a 标签在渲染模板的时候没有做好转义编码,从而导致了 XSS。

官方公布了两种触发点,第一种是渲染 s:a 标签的属性时没有处理好双引号,可以闭合属性、标签然后引入 script 标签,比如这样:

1
">click</a><script>alert(/xss/)</script><a href="

第二个则是发生在 s:url 的 includeParams 属性中,s:url 是一个生成 URI 的标签,includeParams 属性的作用是设置使用什么请求参数来生成 URI,有 none、get、all 三个选项,当 includeParams 为 all 的时候(get 时无法触发),因为没有对参数名进行恰当处理,所以可以引入 script 标签:

1
http://localhost:8080/login.action?<script>alert(1)</script>test=hello

漏洞分析

s:a 闭合属性

这个触发点没有什么好分析的,跟 S2-001 的一个流程,只不过因为没有恰当处理导致可以闭合双引号。

s:url includeParams

这个断点比较难下,我最后下在了 doOpenTag 方法里面,找到 start 方法中对 includeParams 的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String includeParams = this.urlIncludeParams != null ? this.urlIncludeParams.toLowerCase() : "get";
if (this.includeParams != null) {
includeParams = this.findString(this.includeParams);
}

if ("none".equalsIgnoreCase(includeParams)) {
this.mergeRequestParameters(this.value, this.parameters, Collections.EMPTY_MAP);
} else if ("all".equalsIgnoreCase(includeParams)) {
this.mergeRequestParameters(this.value, this.parameters, this.req.getParameterMap());
this.includeGetParameters();
this.includeExtraParameters();
} else if (!"get".equalsIgnoreCase(includeParams) && (includeParams != null || this.value != null || this.action != null)) {
if (includeParams != null) {
LOG.warn("Unknown value for includeParams parameter to URL tag: " + includeParams);
}
} else {
this.includeGetParameters();
this.includeExtraParameters();
}

all 和 get 的不同只有一个 mergeRequestParameters 方法,在 includeParams 为 all 的情况下会调用 mergeRequestParameters 将 tomcat 处取来的参数:

1
2
3
4
5
6
7
8
9
Iterator iterator = ((Map)mergedParams).entrySet().iterator();

while(iterator.hasNext()) {
Entry entry = (Entry)iterator.next();
Object key = entry.getKey();
if (!parameters.containsKey(key)) {
parameters.put(key, entry.getValue());
}
}

而在 get 时会调用的两个函数中,includeGetParameters 方法则是先获取请求参数字符串,再进行处理分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void includeGetParameters() {
if (!Dispatcher.getInstance().isPortletSupportActive() || !PortletActionContext.isPortletRequest()) {
String query = this.extractQueryString();
this.mergeRequestParameters(this.value, this.parameters, UrlHelper.parseQueryString(query));
}

}

private String extractQueryString() {
String query = this.req.getQueryString();
if (query == null) {
query = (String)this.req.getAttribute("javax.servlet.forward.query_string");
}

if (query != null) {
int idx = query.lastIndexOf(35);
if (idx != -1) {
query = query.substring(0, idx);
}
}

return query;
}

而调用 this.req.getQueryString 从 tomcat 处获取的请求参数字符串是经过 URL 编码的,所以无法造成 XSS 漏洞。

includeExtraParameters 则一般为 null。

而在获取完参数之后,在 end 方法中会调用 buildUrl 开始拼接,最后在 buildParametersString 方法中进行了拼接:

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
Iterator iter = params.entrySet().iterator();
String[] valueHolder = new String[1];

while(iter.hasNext()) {
Entry entry = (Entry)iter.next();
String name = (String)entry.getKey();
Object value = entry.getValue();
String[] values;
if (value instanceof String[]) {
values = (String[])((String[])value);
} else {
valueHolder[0] = value.toString();
values = valueHolder;
}

for(int i = 0; i < values.length; ++i) {
if (values[i] != null) {
link.append(name);
link.append('=');
link.append(translateAndEncode(values[i]));
}

if (i < values.length - 1) {
link.append(paramSeparator);
}
}

if (iter.hasNext()) {
link.append(paramSeparator);
}
}

然后发送给客户端,造成 XSS 攻击。

漏洞修复

2.0.11.1 版本修复

对 s:a 标签的修复如下:

1
2
3
if (this.href != null) {
this.addParameter("href", this.ensureAttributeSafelyNotEscaped(this.findString(this.href)));
}

加上了一个 ensureAttributeSafelyNotEscaped 方法来过滤双引号:

1
2
3
protected String ensureAttributeSafelyNotEscaped(String val) {
return val != null ? val.replaceAll("\"", "&#34;") : "";
}

对 s:url 标签的修复如下:

1
2
3
for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {
;
}

在拼接完之后对 script 进行了处理,但是只是简单的将 script 标签的两个尖括号去掉了,所以可以轻松绕过:

1
http://localhost:8080/login.action?<script%20id%3d1>alert(1)</script>test=hello

2.2.1 版本修复

本地更换版本运行的时候发生了错误,加上 javassist 依赖后错误消失。

这个版本的代码发生了大变化,从 URL 类的 end 方法定位到 buildParametersString 方法的遍历拼接中,可以看到拼接调用的是 buildParameterSubstring 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static String buildParameterSubstring(String name, String value) {
StringBuilder builder = new StringBuilder();
builder.append(translateAndEncode(name));
builder.append('=');
builder.append(translateAndEncode(value));

return builder.toString();
}

public static String translateAndEncode(String input) {
String translatedInput = translateVariable(input);
String encoding = getEncodingFromConfiguration();

try {
return URLEncoder.encode(translatedInput, encoding);
} catch (UnsupportedEncodingException e) {
LOG.warn("Could not encode URL parameter '" + input + "', returning value un-encoded");
return translatedInput;
}
}

进行了 URL 编码,完全修复了漏洞。


Orz


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