[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
并编译表达式