[toc]
# 0x01 SpEL 表达式基础
# SpEL 简介
在 Spring 3 中引入了 Spring 表达式语言(Spring Expression Language,简称 SpEL),这是一种功能强大的表达式语言,支持在运行时查询和操作对象图,可以与基于 XML 和基于注解的 Spring 配置还有 bean 定义一起使用。
在 Spring 系列产品中,SpEL 是表达式计算的基础,实现了与 Spring 生态系统所有产品无缝对接。Spring 框架的核心功能之一就是通过依赖注入的方式来管理 Bean 之间的依赖关系,而 SpEL 可以方便快捷的对 ApplicationContext 中的 Bean 进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量 Java 代码。
SpEL 有许多特性:
- 使用 Bean 的 ID 来引用 Bean
- 可调用方法和访问对象的属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作
# SpEL 定界符 —— #{}
SpEL 使用 #{}
作为定界符,所有在大括号中的字符都将被认为是 SpEL 表达式,在其中可以使用 SpEL 运算符、变量、引用 bean 及其属性和方法等。
这里需要注意 #{}
和 ${}
的区别:
#{}
就是 SpEL 的定界符,用于指明内容通过 SpEL 表达式并执行;${}
主要用于加载外部属性文件中的值;- 两者可以混合使用,但是必须
#{}
在外面,${}
在里面,如#{'${}'}
,注意单引号是字符串类型才添加的;
# 0x02 环境搭建
https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
直接运行
打开本地 9091 即可
payload
1 | http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new%20String(new%20byte[]{0x63,0x61,0x6c,0x63}))} |
# 0x03 漏洞分析
随便打个断点
往下跟进
这里捕获到 web 端的异常信息,判断 targetException
是 RuntimeException
类的对象,将我们输入的内容赋值给了 targetException
经过分支,Throwable 提取保存在堆栈中的错误信息。
后面都是一些无关紧要的。。。
直接将断点打在
这里是调用 SpEL 解析器来解析上下文内容
可以看到有报错的类,路径,报错类型、输入内容,事件和状态码
根据上面的信息,我们直接来看 message
即可, timestamp
和 status
可以直接跳过
跟进 getValue
第一步和上面一样,都是调用 SpEL 解析器根据上下文来解析内容
这里是已经编译了,未 false
,跳过 if
这里利用标准评估上下文对象 StandardEvaluationContext 来对抽象语法树进行解析,实际是一个深度优先搜索的计算过程,最终返回整个表达式的计算结果;
1 | ExpressionState expressionState = new ExpressionState(context, this.configuration); |
获取输入的内容并调用 toString
接着跟进到这里
其中 placeholder
拿到值 message
, proval
为 payload
并调用 StringBuilder
来处理修改我们的 payload
1 | int startIndex = strVal.indexOf(this.placeholderPrefix); |
获取 payload
的前缀 ${
进入 while
后,定义了 endIndex
1 | int endIndex = findPlaceholderEndIndex(result, startIndex); |
来看下 findPlaceholderEndIndex
方法
1 | private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { |
从上图可知,int index 也就等于 24+2=26
显然 index<buf.length()
,进入 while 循环
接着会遍历字符串是否为后缀 "}",从 index=26 开始
似乎到 109 就结束了
下一步
这里 For input string: "
是 24,
1 | String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex); |
换句话说也就是
1 | String placeholder = result.substring(24 + 2, 109); |
换言之
也就是将 ${} 中的内容提取出来
1 | T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63})) |
并将其赋值给 originalPlaceholder
重写的 resolvePlaceholder
处理 name
还是同样的 getValue
获取上下文和 Expression
并编译表达式