JavaWeb安全入门之 S2-002漏洞分析


1-漏洞简介

Struts2-002是一个 XSS 漏洞,该漏洞发生在 s:urls: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><"

-w1040

s:url 标签

http://localhost:8009/S2_002_vul_war/login.action?<script>alert(1);</script>=1

-w1008

4-漏洞分析

根据官方通告,问题是出在 s:urls: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.componentorg.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 参数的值来进行不同的处理, 在 includeParamsall 的情况下会调用mergeRequestParameters方法,他的参数是从tomcat从取得:
-w1203
这里看得出,是我们的输入的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());
            }
        }

    }

-w1039

此处mergedParams 变量的值是传递进来的参数经过处理后的内容,主要是赋值等处理,在上一个截图可以看到该变量的值就是我们的payload,所以经过下图部分代码的处理,payload给到了 parameters变量
-w951

从上面的过程可以看出该方法主要是对请求的参数进行合并,该方法的第三个参数也就是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 方法则用来存入其它数据。
-w1236

如果 includeParams 参数是get的话会执行上面这两个函数,但是includeGetParameters 方法则是先获取请求参数字符串,再进行处理分割,而调用 this.req.getQueryStringtomcat 处获取的请求参数字符串是经过 URL 编码的,所以无法造成 XSS 漏洞。如下:
-w869

所以只有参数为 all 的时候才会产生漏洞。

而在获取完参数之后,在 end 方法中会调用 buildUrl 开始拼接
struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class
-w1105
最后在 buildParametersString 方法中进行了拼接:
struts2-core-2.0.8.jar!/org/apache/struts2/views/util/UrlHelper.class
-w982
-w1303

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 攻击。
-w1313

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

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

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

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

http://localhost:8080/login.action?alert(1)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 编码,完全修复了漏洞。

6-参考

Twings blog-struts2系列漏洞 S2-002

mi1k7ea-Struts2基础篇之标签

w3cschool-Struts url标签

Dean-S2-002 漏洞详细分析

mochazz-Java代码审计之S2-002

twosmi1e-S2-002漏洞分析


文章作者: LANVNAL
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LANVNAL !
  目录