Struts2框架: S2-002 漏洞详细分析
字数 5571 2020-08-01 23:39:32

0x00 前言

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

熟悉J2EE开发, 主要是JSP开发

了解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><"

请求响应内容:

1 2 3 4 5

<body>

&lt;<span style="color:rgb(249,38,114);">a</span> <span style="color:rgb(166,226,46);">href</span><span style="color:rgb(249,38,114);">=</span><span style="color:rgb(230,219,116);">"//hello/hello_struts2.action?"</span>&gt;&lt;<span style="color:rgb(249,38,114);">script</span>&gt;<span style="color:rgb(166,226,46);">alert</span>(<span style="color:rgb(174,129,255);">1</span>)&lt;/<span style="color:rgb(249,38,114);">script</span>&gt;<span style="background-color:rgb(30,0,16);color:rgb(150,0,80);">&lt;</span>"=&amp;amp;%22%3E%3Cscript%3Ealert(1)%3C/script%3E%3C%22="&gt;ä½ å¥½Struts2&lt;/<span style="color:rgb(249,38,114);">a</span>&gt;

</body>

0x02 漏洞分析

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

 1
2
3
4
5
6
7
8
9
10
11
12
13

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

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public abstract class ComponentTagSupport extends StrutsBodyTagSupport {

 <span style="color:rgb(102,217,239);">public</span> <span style="color:rgb(102,217,239);">int</span> <span style="color:rgb(166,226,46);">doStartTag</span><span style="color:rgb(249,38,114);">()</span> <span style="color:rgb(102,217,239);">throws</span> JspException <span style="color:rgb(249,38,114);">{</span>

 	<span style="color:rgb(117,113,94);">// 实现子类是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方法实现:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

public class URL extends Component {

<span style="color:rgb(102,217,239);">public</span> <span style="color:rgb(102,217,239);">boolean</span> <span style="color:rgb(166,226,46);">start</span><span style="color:rgb(249,38,114);">(</span>Writer writer<span style="color:rgb(249,38,114);">)</span> <span style="color:rgb(249,38,114);">{</span>
 &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">boolean</span> result <span style="color:rgb(249,38,114);">=</span> <span style="color:rgb(102,217,239);">super</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">start</span><span style="color:rgb(249,38,114);">(</span>writer<span style="color:rgb(249,38,114);">);</span>
 &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span> <span style="color:rgb(249,38,114);">!=</span> <span style="color:rgb(102,217,239);">null</span><span style="color:rgb(249,38,114);">)</span> <span style="color:rgb(249,38,114);">{</span>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span> <span style="color:rgb(249,38,114);">=</span> <span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">findString</span><span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span><span style="color:rgb(249,38,114);">);</span>
 &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(249,38,114);">}</span>

 &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">try</span> <span style="color:rgb(249,38,114);">{</span>

 &nbsp; &nbsp; &nbsp; &nbsp;	<span style="color:rgb(117,113,94);">// 我们在&lt;s:url&gt;这个标签内配置的includeParams="all"

        // 关于这个属性介绍,参考2
           String includeParams = this.urlIncludeParams != null ? this.urlIncludeParams.toLowerCase() : "get";
           if (this.includeParams != null) {
               includeParams = this.findString(this.includeParams);
           }

 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(230,219,116);">"none"</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">equalsIgnoreCase</span><span style="color:rgb(249,38,114);">(</span>includeParams<span style="color:rgb(249,38,114);">))</span> <span style="color:rgb(249,38,114);">{</span>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">mergeRequestParameters</span><span style="color:rgb(249,38,114);">(</span><span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">value</span><span style="color:rgb(249,38,114);">,</span> <span style="color:rgb(102,217,239);">this</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">parameters</span><span style="color:rgb(249,38,114);">,</span> Collections<span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">EMPTY_MAP</span><span style="color:rgb(249,38,114);">);</span>
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(249,38,114);">}</span> <span style="color:rgb(102,217,239);">else</span> <span style="color:rgb(102,217,239);">if</span> <span style="color:rgb(249,38,114);">(</span><span style="color:rgb(230,219,116);">"all"</span><span style="color:rgb(249,38,114);">.</span><span style="color:rgb(166,226,46);">equalsIgnoreCase</span><span style="color:rgb(249,38,114);">(</span>includeParams<span style="color:rgb(249,38,114);">))</span> <span style="color:rgb(249,38,114);">{</span>

                   // 我们跟进此方法的实现
               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);
       }

 &nbsp; &nbsp; &nbsp; &nbsp;<span style="color:rgb(102,217,239);">return</span> result<span style="color:rgb(249,38,114);">;</span>
 &nbsp; &nbsp;<span style="color:rgb(249,38,114);">}</span></p><p>this.mergeRequestParameters(this.value, this.parameters, this.req.getParameterMap()); 跟进实现:</p><p style="margin-left:0px;">&nbsp;1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

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

0x04 修复方案分析:

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

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

1 2 3 4

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

       }

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

好了,分析结束,附上一个bypass POC:

index.action?"><script 1>alert(1)</script>"

升级到2.0.11.2依然没有修复.

0x06 引用

S2-002安全公告

Struts2的url标签

转自:https://dean2021.github.io/posts/s2-002/

&lt;/ body &gt; 0x02 漏洞分析 通过官网安全公告 参考[ 1],我们大概知道问题是出在 和标签里,如下是我们的index.jsp部分代码: &nbsp;1 2 3 4 5 6 7 8 9 10 11 12 13 &lt; %@taglib prefix="s" uri="/struts-tags" %&gt; &lt; html xmlns = "http://www.w3.org/1999/xhtml" xml:lang = "en" lang = "en" &gt; &lt; body &gt; &lt; a href = "&lt;s:url action=" / hello / hello_ struts2 " includeParams = "all" &gt;&lt;/ s:url &gt;"&gt;你好Struts2&lt;/ a &gt; &lt;/ body &gt; &lt;/ html &gt; 两个标签我们就分析一个就行了,读过我上篇文章的同学应该知道我们先从找到标签的实现对象入手,这里就不多说了,由于s2的标签库都是集成与ComponentTagSupport类, doStartTag方法也是在该类里实现,所以我们直接从ComponentTagSupport类doStartTag方法进行断点调试, 首先我们看一下doStartTag方法: &nbsp;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 &nbsp; &nbsp; &nbsp; &nbsp; this . component = this . getBean ( this . getStack (), ( HttpServletRequest ) this . pageContext . getRequest (), ( HttpServletResponse ) this . pageContext . getResponse ()); &nbsp; &nbsp; &nbsp; &nbsp;Container container = Dispatcher . getInstance (). getContainer (); &nbsp; &nbsp; &nbsp; &nbsp;container . inject ( this . component ); &nbsp; &nbsp; &nbsp; &nbsp; this . populateParams (); &nbsp; &nbsp; &nbsp; &nbsp; // 跟进URL类的start方法实现 &nbsp; &nbsp; &nbsp; &nbsp; boolean evalBody = this . component . start ( this . pageContext . getOut ()); &nbsp; &nbsp; &nbsp; &nbsp; if ( evalBody ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return this . component . usesBody () ? 2 : 1 ; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return 0 ; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; } 跟进URL类的start方法实现: &nbsp;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 &nbsp; &nbsp; &nbsp; &nbsp; // 关于这个属性介绍,参考2 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;String includeParams = this . urlIncludeParams != null ? this . urlIncludeParams . toLowerCase () : "get" ; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ( this . includeParams != null ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;includeParams = this . findString ( this . includeParams ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 我们跟进此方法的实现 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this . mergeRequestParameters ( this . value , this . parameters , this . req . getParameterMap ()); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this . includeGetParameters (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this . includeExtraParameters (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else if (! "get" . equalsIgnoreCase ( includeParams ) &amp;&amp; ( includeParams != null || this . value != null || this . action != null )) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if ( includeParams != null ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LOG . warn ( "Unknown value for includeParams parameter to URL tag: " + includeParams ); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } else { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this . includeGetParameters (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this . includeExtraParameters (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } catch ( Exception var4 ) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LOG . warn ( "Unable to put request parameters (" + this . req . getQueryString () + ") into parameter map." , var4 ); &nbsp; &nbsp; &nbsp; &nbsp; } 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while ( iterator . hasNext ()) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Entry entry = ( Entry ) iterator . next (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Object key = entry . getKey (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!(( Map ) mergedParams ). containsKey ( key )) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (( Map ) mergedParams ). put ( key , entry . getValue ()); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp;Iterator iterator = (( Map ) mergedParams ). entrySet (). iterator (); &nbsp; &nbsp; &nbsp; &nbsp; while ( iterator . hasNext ()) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Entry entry = ( Entry ) iterator . next (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Object key = entry . getKey (); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (! parameters . containsKey ( key )) { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;parameters . put ( key , entry . getValue ()); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; } &nbsp; &nbsp; &nbsp; &nbsp; } } 从方法明明上我们已经能够看得出该方法是合并参数,通过阅读代码该方法的第三个参数也就是HttpServletRequest对象getParameterMap(), HttpServletRequest是Servlet原生对象,那这个方法具体是用来做什么的呢?下方是官方解释: Returns a java.util.Map of the parameters of this request. 也就是返回一个map类型的request参数。我们请求的是url是: http://localhost:8080/index.action?"&gt;&lt;script&gt;alert(1)&lt;/script&gt;&lt;" &nbsp; &nbsp; &nbsp; &nbsp; } 看到这样的修复,虽然很无语,但是站在没有web安全知识的程序员角度来看待这种修复方案,能这样写也是很正常,因为大部分程序员只知道JavaScript代码是在&lt;script&gt;标签中执行。 好了,分析结束,附上一个bypass POC: index.action?"&gt;&lt;script 1&gt;alert(1)&lt;/script&gt;"