Struts2框架: S2-002 漏洞详细分析


0x00 前言

阅读本文需要具备的知识:

  1. 熟悉J2EE开发, 主要是JSP开发
  2. 了解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><"

请求响应内容:

<body>

    <a href="//hello/hello_struts2.action?"><script>alert(1)</script><"=&amp;%22%3E%3Cscript%3Ealert(1)%3C/script%3E%3C%22=">ä½ å¥½Struts2</a>

</body>

0x02 漏洞分析

通过官网安全公告 参考[1],我们大概知道问题是出在 和标签里,如下是我们的index.jsp部分代码:

<!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方法:

public abstract class ComponentTagSupport extends StrutsBodyTagSupport {

     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;
        }
    }

跟进URL类的start方法实现:

public class URL extends Component {

    public boolean start(Writer writer) {
            boolean result = super.start(writer);
            if (this.value != null) {
                this.value = this.findString(this.value);
            }

            try {

                // 我们在<s:url>这个标签内配置的includeParams="all"
                // 关于这个属性介绍,参考2
                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();
                }
            } catch (Exception var4) {
                LOG.warn("Unable to put request parameters (" + this.req.getQueryString() + ") into parameter map.", var4);
            }

            return result;
        }

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);
            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框架的标签的includeParams属性设置为all的情况下,对url参数名未做过滤,导致xss漏洞。

0x04 修复方案分析:

根据公告,我们需要升级到Struts 2.0.11.1版本。(但是没有真正解决修复漏洞)

经过对2.0.11.1的代码阅读,在UrlHelper类buildUrl方法里,第136行增加了如下修复代码:

 // link是最终的生成的url
        for(result = link.toString(); result.indexOf("<script>") > 0; result = result.replaceAll("<script>", "script")) {

        }

看到这样的修复,虽然很无语,但是站在没有web安全知识的程序员角度来看待这种修复方案,能这样写也是很正常,因为大部分程序员只知道JavaScript代码是在