前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
环境搭建
环境搭建参考 S2-001,我这里使用的是 2.0.9 版本的 struts2。
修改一下模板和 Action 代码,模板加上两个漏洞触发点:
<s:a href="%{url}">click</s:a>
<s:url includeParams="all"> </s:url>
Action 加上 url 变量。
漏洞利用
阅读官方通告,可以看出漏洞原因就是 s:url 和 s:a 标签在渲染模板的时候没有做好转义编码,从而导致了 XSS。
官方公布了两种触发点,第一种是渲染 s:a 标签的属性时没有处理好双引号,可以闭合属性、标签然后引入 script 标签,比如这样:
">click</a><script>alert(/xss/)</script><a href="
第二个则是发生在 s:url 的 includeParams 属性中,s:url 是一个生成 URI 的标签,includeParams 属性的作用是设置使用什么请求参数来生成 URI,有 none、get、all 三个选项,当 includeParams 为 all 的时候(get 时无法触发),因为没有对参数名进行恰当处理,所以可以引入 script 标签:
http://localhost:8080/login.action?<script>alert(1)</script>test=hello
漏洞分析
s:a 闭合属性
这个触发点没有什么好分析的,跟 S2-001 的一个流程,只不过因为没有恰当处理导致可以闭合双引号。
s:url includeParams
这个断点比较难下,我最后下在了 doOpenTag 方法里面,找到 start 方法中对 includeParams 的处理:
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 处取来的参数:
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 方法则是先获取请求参数字符串,再进行处理分割:
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 方法中进行了拼接:
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 标签的修复如下:
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?<script%20id%3d1>alert(1)</script>test=hello
2.2.1 版本修复
本地更换版本运行的时候发生了错误,加上 javassist 依赖后错误消失。
这个版本的代码发生了大变化,从 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 编码,完全修复了漏洞。
Orz
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!