0x00 前言
阅读本文需要具备的知识:
熟悉J2EE开发, 主要是JSP开发
了解Struts2框架执行流程
0x01 漏洞复现
影响漏洞版本:
Struts 2.0.0 - Struts 2.0.11
漏洞靶机代码: (下方通过该代码进行分析, 务必下载本地对比运行)
https://github.com/dean2021/java_security_book/tree/master/Struts2/s2_002
测试POC:
http://localhost:8080/index.action?"><script>alert(1)</script><"
请求响应内容:
1 2 3 4 5
<body>
<<span style="color:rgb(249,38,114);">a</span> <span style="color:rgb(166,226,46);">href</span><span style="color:rgb(249,38,114);">=</span><span style="color:rgb(230,219,116);">"//hello/hello_struts2.action?"</span>><<span style="color:rgb(249,38,114);">script</span>><span style="color:rgb(166,226,46);">alert</span>(<span style="color:rgb(174,129,255);">1</span>)</<span style="color:rgb(249,38,114);">script</span>><span style="background-color:rgb(30,0,16);color:rgb(150,0,80);"><</span>"=&amp;%22%3E%3Cscript%3Ealert(1)%3C/script%3E%3C%22=">ä½ å¥½Struts2</<span style="color:rgb(249,38,114);">a</span>>
</body>
0x02 漏洞分析
通过官网安全公告 参考[1],我们大概知道问题是出在 和标签里,如下是我们的index.jsp部分代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@taglib prefix="s" uri="/struts-tags" %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<body>
<a href="<s:url action="/hello/hello_struts2" includeParams="all" ></s:url>">你好Struts2</a>
</body>
</html>
两个标签我们就分析一个就行了,读过我上篇文章的同学应该知道我们先从找到标签的实现对象入手,这里就不多说了,由于s2的标签库都是集成与ComponentTagSupport类, doStartTag方法也是在该类里实现,所以我们直接从ComponentTagSupport类doStartTag方法进行断点调试, 首先我们看一下doStartTag方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class ComponentTagSupport extends StrutsBodyTagSupport {
<span style="color:rgb(102,217,239);">public</span> <span style="color:rgb(102,217,239);">int</span> <span style="color:rgb(166,226,46);">doStartTag</span><span style="color:rgb(249,38,114);">()</span> <span style="color:rgb(102,217,239);">throws</span> JspException <span style="color:rgb(249,38,114);">{</span>
<span style="color:rgb(117,113,94);">// 实现子类是URL.class
this.component = this.getBean(this.getStack(), (HttpServletRequest)this.pageContext.getRequest(), (HttpServletResponse)this.pageContext.getResponse());
Container container = Dispatcher.getInstance().getContainer();
container.inject(this.component);
this.populateParams();
// 跟进URL类的start方法实现
boolean evalBody = this.component.start(this.pageContext.getOut());
if (evalBody) {
return this.component.usesBody() ? 2 : 1;
} else {
return 0;
}
}
跟进URL类的start方法实现:
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
32
33
34
35
36
37
38
39
40
41
public class URL extends Component {
<span style="color:rgb(102,217,239);">public</span> <span style="color:rgb(102,217,239);">boolean</span> <span style="color:rgb(166,226,46);">start</span><span style="color:rgb(249,38,114);">(</span>Writer writer<span style="color:rgb(249,38,114);">)</span> <span style="color:rgb(249,38,114);">{</span>
<span style="color:rgb(102,217,239);">boolean</span> result <span style="color:rgb(249,38,114);">=</span> <span style="color:rgb(102,217,239);">super</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">start</span><span style="color:rgb(249,38,114);">(</span>writer<span style="color:rgb(249,38,114);">);</span>
<span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span> <span style="color:rgb(249,38,114);">!=</span> <span style="color:rgb(102,217,239);">null</span><span style="color:rgb(249,38,114);">)</span> <span style="color:rgb(249,38,114);">{</span>
<span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span> <span style="color:rgb(249,38,114);">=</span> <span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">findString</span><span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span><span style="color:rgb(249,38,114);">);</span>
<span style="color:rgb(249,38,114);">}</span>
<span style="color:rgb(102,217,239);">try</span> <span style="color:rgb(249,38,114);">{</span>
<span style="color:rgb(117,113,94);">// 我们在<s:url>这个标签内配置的includeParams="all"
// 关于这个属性介绍,参考2
String includeParams = this.urlIncludeParams != null ? this.urlIncludeParams.toLowerCase() : "get";
if (this.includeParams != null) {
includeParams = this.findString(this.includeParams);
}
<span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(230,219,116);">"none"</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">equalsIgnoreCase</span><span style="color:rgb(249,38,114);">(</span>includeParams<span style="color:rgb(249,38,114);">))</span> <span style="color:rgb(249,38,114);">{</span>
<span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">mergeRequestParameters</span><span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span><span style="color:rgb(249,38,114);">,</span> <span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">parameters</span><span style="color:rgb(249,38,114);">,</span> Collections<span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">EMPTY_MAP</span><span style="color:rgb(249,38,114);">);</span>
<span style="color:rgb(249,38,114);">}</span> <span style="color:rgb(102,217,239);">else</span> <span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(230,219,116);">"all"</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">equalsIgnoreCase</span><span style="color:rgb(249,38,114);">(</span>includeParams<span style="color:rgb(249,38,114);">))</span> <span style="color:rgb(249,38,114);">{</span>
// 我们跟进此方法的实现
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();
}
} catch (Exception var4) {
LOG.warn("Unable to put request parameters (" + this.req.getQueryString() + ") into parameter map.", var4);
}
<span style="color:rgb(102,217,239);">return</span> result<span style="color:rgb(249,38,114);">;</span>
<span style="color:rgb(249,38,114);">}</span></p><p>this.mergeRequestParameters(this.value, this.parameters, this.req.getParameterMap()); 跟进实现:</p><p style="margin-left:0px;"> 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
protected void mergeRequestParameters(String value, Map parameters, Map contextParameters) { Map mergedParams = new LinkedHashMap(contextParameters); if (value != null && value.trim().length() > 0 && value.indexOf("?") > 0) { new LinkedHashMap(); String queryString = value.substring(value.indexOf("?") + 1); mergedParams = UrlHelper.parseQueryString(queryString); Iterator iterator = contextParameters.entrySet().iterator();
while(iterator.hasNext()) {
Entry entry = (Entry)iterator.next();
Object key = entry.getKey();
if (!((Map)mergedParams).containsKey(key)) {
((Map)mergedParams).put(key, entry.getValue());
}
}
}
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());
}
}
}
从方法明明上我们已经能够看得出该方法是合并参数,通过阅读代码该方法的第三个参数也就是HttpServletRequest对象getParameterMap(), HttpServletRequest是Servlet原生对象,那这个方法具体是用来做什么的呢?下方是官方解释:
Returns a java.util.Map of the parameters of this request.
也就是返回一个map类型的request参数。我们请求的是url是:
http://localhost:8080/index.action?"><script>alert(1)</script><"
那么解析后的map就是 : KEY = "><script>alert(1)</script><" VAL = “” 然后进行参数合并, 并未看到对参数进行任何过滤,最后写入到html中,导致造成xss漏洞。
TIPS: 经过测试HttpServletRequest对象getParameterMap()方法只会对参数值进行转换编码,并不会对参数名进行任何处理.
0x03 总结:
Struts2框架的<s:url>标签的includeParams属性设置为all的情况下,对url参数名未做过滤,导致xss漏洞。
0x04 修复方案分析:
根据公告,我们需要升级到Struts 2.0.11.1版本。(但是没有真正解决修复漏洞)
经过对2.0.11.1的代码阅读,在UrlHelper类buildUrl方法里,第136行增加了如下修复代码:
1 2 3 4
// link是最终的生成的url for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {
}
看到这样的修复,虽然很无语,但是站在没有web安全知识的程序员角度来看待这种修复方案,能这样写也是很正常,因为大部分程序员只知道JavaScript代码是在<script>标签中执行。
好了,分析结束,附上一个bypass POC:
index.action?"><script 1>alert(1)</script>"
升级到2.0.11.2依然没有修复.