前言

经典 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("\"", "&#34;") : "";
}

对 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


Web Java Struts2

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-003/S2-005
struts2系列漏洞 S2-001