当前位置: 首页 > 科技观察

Java代码审计之SpEL表达式注入

时间:2023-03-18 23:34:50 科技观察

一、SpEL表达式注入SpringExpressionLanguage(简称SpEL)是一种强大的表达式语言,用于在运行时查询和操作对象图;语法类似于UnifiedEL,但提供了更多功能,特别是方法调用和基本字符串模板函数。SpEL的诞生就是为了给Spring社区提供一种表达语言,可以无缝对接Spring生态中的所有产品,提供一站式的支持。2、SpEL表达式的基本表达式:文字表达式、关系、逻辑和算术运算表达式、字符串链接和截取表达式、三元运算、正则表达式、括号优先表达式;类相关表达式:类类型表达式、类实例化、instanceof表达式、变量定义和引用、赋值表达式、自定义函数、对象属性访问和安全导航表达式、对象方法调用、Bean引用;集合相关的表达式:内联List、内联数组、集合、字典访问、列表、字典;其他表达式:模板表达式3、SpEL基础importmaveninpom.xmlor"org.springframework.expression-3.0.5.RELEASE.jar"添加到classpath5.0.8.RELEASEorg.springframeworkspring-表达式${org.springframework.version}1。SpEL的使用SpEL在计算表达式值的时候一般分为四步,第三步是可选的:首先构造一个解析器,然后解析器解析字符串表达式,在这里构造上下文,然后获取表达式运算后的值根据上下文。ExpressionParserparser=newSpelExpressionParser();Expressionexpression=parser.parseExpression("('Hello'+'freebuf').concat(#end)");EvaluationContextcontext=newStandardEvaluationContext();context.setVariable("end","!");System.out.println(表达式.getValue(上下文));创建解析器:SpEL使用ExpressionParser接口来表示解析器,并提供了SpelExpressionParser的默认实现;解析表达式:使用ExpressionParser的ParseExpression将对应的表达式解析成一个Expression对象。构造上下文:准备变量定义等表达式所需的上下文数据。求值:通过Expression接口的getValue方法根据上下文获取表达式值。二、SpEL主界面1.ExpressionParser界面:表示解析器。默认实现是org.springframework.expression.spel.standard包中的SpelExpressionParser类。使用parseExpression方法将字符串表达式转换为Expression对象。对于ParserContext接口,使用定义字符串表达式是否为模板,以及模板开始和结束字符;publicinterfaceExpressionParser{ExpressionparseExpression(StringexpressionString);ExpressionparseExpression(StringexpressionString,ParserContextcontext);}示例演示:ExpressionParserparser=newSpelExpressionParser();ParserContextparserContext=newParserContext(){@OverridepublicbooleanisTemplate(){returntrue;}@OverridepublicStringgetExpressionPrefix(){;{return"#@OverridepublicStringgetExpressionSuffix(){return"}";}};Stringtemplate="#{'hello'}#{'freebuf!'}";Expressionexpression=parser.parseExpression(template,parserContext);System.out.println(expression.获取值());演示了ParserContext的使用,其中定义了ParserContext的实现:定义表达式是一个模块,表达式前缀为“#{”,后缀为“}”;parseExpression解析时传入的模板必须以“#{”开头,以“}”结尾。默认传入的字符串表达式不是模板形式,比如前面演示的HelloWorld。EvaluationContext接口:表示上下文,默认实现是org.springframework.expression.spel.support包中的StandardEvaluationContext类,使用setRootObject方法设置根对象,使用setVariable方法注册自定义变量,使用registerFunction注册自定义函数等等等等。表达式接口:表示一个表达式对象。默认实现是org.springframework.expression.spel.standard包中的SpelExpression。它提供了获取表达式值的getValue方法和设置对象值的setValue方法。3.SpEL语法——类相关表达式类类型表达式:使用“T(Type)”表示java.lang.Class实例,“Type”必须是全限定类名,“java.lang”包除外,即包下的类不需要指定包名;类类型表达式也可用于访问类静态方法和类静态字段。具体使用方法:ExpressionParserparser=newSpelExpressionParser();//java.lang包类访问Classresult1=parser.parseExpression("T(String)").getValue(Class.class);System.out.println(result1);//其他包类访问Stringexpression2="T(java.lang.Runtime).getRuntime().exec('open/Applications/Calculator.app')";Classresult2=parser.parseExpression(expression2).getValue(Class.class);System.out.println(result2);//类静态字段访问intresult3=parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.类);System.out。println(result3);//类静态方法调用intresult4=parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);System.out.println(result4);类实例分类:java关键字“new”也用于类实例化,类名必须是完全限定名,java.lang包中的类型除外,如String和Integer。instanceof表达式:SpEL支持instanceof运算符,与Java同义;例如,“'haha'instanceofT(String)”将返回true。变量定义和引用:变量定义通过EvaluationContext接口的setVariable(variableName,value)方法定义;在表达式中使用“#variableName”引用;除了引用自定义变量,SpE还允许引用根对象和当前上下文对象,使用“#root”指代根对象,使用“#this”指代当前上下文对象;自定义函数:目前只支持类静态方法注册为自定义函数;SpEL使用StandardEvaluationContext的registerFunction方法来注册自定义函数,其实可以用setVariable代替,两者其实本质是一样的。4、审计流程这里以SpringMessage远程命令执行漏洞为例1、环境搭建gitclonehttps://github.com/spring-guides/gs-messaging-stomp-websocket代码,搜索org.springframework。全局查看expression.spel.standard,发现导入了DefaultSubscriptionRegistry.java文件。然后搜索SpelExpressionParser,跟进找到如下关键代码。具体分析看代码注释可以看出,SpEL表达式表达式取自headers中的selector字段Stringselector=SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(),headers);if(selector!=null){try{//生成表达式对象expression=this.expressionParser.parseExpression(selector);this.selectorHeaderInUse=true;if(logger.isTraceEnabled()){logger.trace("订阅选择器:["+selector+"]");}}catch(Throwableex){if(logger.isDebugEnabled()){logger.debug("Failedtoparseselector:"+selector,ex);}}}//表达式传入addSubscription函数,保存在this.subscriptionRegistrythis.subscriptionRegistry.addSubscription(sessionId,subsId,destination,expression);this.destinationCache.updateAfterNewSubscription(destination,sessionId,subsId);}再次搜索.subscriptionRegistry,看看有没有calltopassinexpression,发现了!调用this.subscriptionRegistry.getSubscriptions(sessionId)并从中获取info->sub->expression。最重要的是这里直接调用了expression.getValue()!这说明如果能控制SpEL的表达,就可以直接执行命令!让我们看一下调用filterSubscriptions函数的位置。函数调用的调用链如下:filterSubscriptions->findSubscriptionsInternal->findSubscriptions->sendMessageToSubscribers2。sendMessageToSubscribers是发送消息的函数回顾一下整个流程,SpEL表达式是从headers中的selector中获取的,也就是在发送请求的时候将selector添加到request中header可以传入,然后生成expression对象传递给this.subscriptionRegistry,然后在发送消息时,会直接从this.subscriptionRegistry中取出并调用expression.getValue()执行我们传入的SpEL表达式。在校验过程中,设置在expression.getValue()中打个断点,看发送消息是否会被拦截,检查调用链是否和上面分析的一样。答对了!简单总结一下SpEL表达注入的分析思路。可以全局搜索org.springframework.expression.spel.standard,或者expression.getValue(),expression.setValue()定位到具体的漏洞代码,然后分析传递过来的输入参数是否可以使用,然后跟踪参数来源,看是否可控。SpringDataCommonsRemoteCodeExecution的SpEL注入导致的代码执行也可以类似分析。五、漏洞修复SimpleEvaluationContext、StandardEvaluationContext是SpEL提供的两个EvaluationContext:SimpleEvaluationContext?-公开Spal语言功能的子集和表达式类别的配置选项,这些类别不需要SpEL语言语法的全部范围,应有意限制。StandardEvaluationContext?-公开全套SpEL语言功能和配置选项。您可以使用它来指定默认根对象并配置每个可用的评估相关策略。SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集。它不包括Java类型引用、构造函数和bean引用;所以最直接的修复方法是将StandardEvaluationContext替换为SimpleEvaluationContext?。这是我个人学习代码审计的一个小总结。逻辑可能没有那么严谨,但我个人认为这是一个比较通俗易懂的分析方法,不喜勿喷。