1-漏洞简介
Struts2-002
是一个 XSS
漏洞,该漏洞发生在 s:url
和 s:a
标签中,当标签的属性 includeParams=all
时,即可触发该漏洞。
漏洞影响版本: Struts 2.0.0 - Struts 2.1.8.1
。更多详情可参考官方通告:
https://cwiki.apache.org/confluence/display/WW/S2-002
2-环境搭建
在S2-001的环境基础上稍微修改一下就可以。
s2_002的环境也传到了github上,直接idea打开文件夹,配置一下tomcat就行。
修改index.jsp
,去掉001中的form标签内容,添加如下内容:
<s:url action="login" includeParams="all"></s:url>
<s:a href="%{url}">click</s:a>
struts.xml
<struts>
<package name="S2-002" extends="struts-default">
<action name="login"
class="com.lanvnal.s2002.action.LoginAction"
method="execute">
<result name="success">index.jsp</result>
</action>
</package>
</struts>
Action加上url变量,删除001的相关代码,添加如下:
private String url = null;
public String execute() throws Exception {
return "success";
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
3-漏洞复现
xss payload:s:a
标签:
http://localhost:8009/S2_002_vul_war/login.action?"><script>alert(1)</script><"
s:url
标签
http://localhost:8009/S2_002_vul_war/login.action?<script>alert(1);</script>=1
4-漏洞分析
根据官方通告,问题是出在 s:url
和 s:a
标签里,在渲染模板的时候没有做好转义编码导致了XSS,先分析s:url
标签。
s:url 标签
根据大佬之前的分析文章知道我们先从找到标签的实现对象入手。
根据001的分析,当程序开始解析 Struts
标签时,会调用org.apache.struts2.views.jsp.ComponentTagSupport:doStartTag()
方法。
由于s2的标签库都是集成与ComponentTagSupport类, doStartTag方法也是在该类里实现,所以我们直接从ComponentTagSupport类doStartTag方法进行断点调试, 首先我们看一下doStartTag方法:
位置:struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
public int doStartTag() throws JspException {
// 实现子类是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;
}
}
由于我们这里处理的是 s:url
标签,所以这里用来处理标签的组件 this.component
为org.apache.struts2.components.URL
类对象。我们跟进 URL:start()
方法。
位置:struts2-core-2.0.8.jar!/org/apache/struts2/components/URL.class
public boolean start(Writer writer) {
boolean result = super.start(writer);
if (this.value != null) {
this.value = this.findString(this.value);
}
try {
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)) {
// 我们在<s:url>这个标签内配置的includeParams="all",我们跟进此方法的实现
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);
}
return result;
}
在上面这段代码中可以看到,会根据 includeParams
参数的值来进行不同的处理, 在 includeParams
为 all
的情况下会调用mergeRequestParameters
方法,他的参数是从tomcat从取得:
这里看得出,是我们的输入的payload,具体怎么取到的看下面的 this.mergeRequestParameters
方法
跟进 this.mergeRequestParameters(this.value, this.parameters, this.req.getParameterMap());
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);
// contextParameters的处理
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());
}
}
}
// 下面的操作是取到了我们的payload,并添加到 `parameters`变量中
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());
}
}
}
此处mergedParams
变量的值是传递进来的参数经过处理后的内容,主要是赋值等处理,在上一个截图可以看到该变量的值就是我们的payload,所以经过下图部分代码的处理,payload给到了 parameters
变量
从上面的过程可以看出该方法主要是对请求的参数进行合并,该方法的第三个参数也就是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>=1
那么解析后的map就是 : KEY =<script>alert(1)</script>
VAL = “1” 然后进行参数合并, 并未看到对参数进行任何过滤,最后写入到html中,导致造成xss漏洞。
之后是 includeGetParameters
方法和 includeExtraParameters
方法,includeGetParameters 方法主要是将 HttpRequest.getQueryString() 的数据存入 this.parameters 属性,而 includeExtraParameters 方法则用来存入其它数据。
如果 includeParams
参数是get的话会执行上面这两个函数,但是includeGetParameters
方法则是先获取请求参数字符串,再进行处理分割,而调用 this.req.getQueryString
从 tomcat
处获取的请求参数字符串是经过 URL 编码的,所以无法造成 XSS 漏洞。如下:
所以只有参数为 all 的时候才会产生漏洞。
而在获取完参数之后,在 end 方法中会调用 buildUrl 开始拼接struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class
最后在 buildParametersString 方法中进行了拼接:struts2-core-2.0.8.jar!/org/apache/struts2/views/util/UrlHelper.class
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 攻击。
s:a 标签
跟 S2-001 的一个流程,只不过因为没有恰当处理导致可以闭合双引号。
详细分析后续补充
5-漏洞修复
2.2.1 版本修复
对 s:a 标签的修复如下:
if (this.href != null) {
this.addParameter("href", this.ensureAttributeSafelyNotEscaped(this.findString(this.href)));
}
加上了一个 ensureAttributeSafelyNotEscaped
方法来过滤双引号:
protected String ensureAttributeSafelyNotEscaped(String val) {
return val != null ? val.replaceAll("\"", """) : "";
}
对 s:url 标签的修复如下:
for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {
;
}
在拼接完之后对 script 进行了处理,但是只是简单的将 script 标签的两个尖括号去掉了,所以可以轻松绕过:
http://localhost:8080/login.action?test=hello
2.2.1 版本修复
这个版本的代码发生了大变化,从 URL 类的 end 方法定位到 buildParametersString
方法的遍历拼接中,可以看到拼接调用的是 buildParameterSubstring
方法:
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 编码,完全修复了漏洞。