前言
我看完CVE-2022-22963,便着手深入Spel表达式,希望能够深入研究Spel究竟出现在哪些地方,怎么挖掘新的漏洞。挺感谢导师的帮助的,毕竟我实在太菜了,连反馈邮箱都不会写。
考虑到这么多的组件如果一个个去尝试,显得太累了,我把自己实验时的代码都做成一个集合,放出来,方便有兴趣的师傅们一起讨论(项目:spel search)。先介绍各个组件使用Spel的情况。
application.properties
turbine.cluster-name-expression="T(Runtime).getRuntime().exec('calc')"
无法配合actuator,需要/actuator/env、/actuator/restart
其中/actuator/restart时,会异常,导致服务崩溃
@StreamListener注解
@StreamListener(value = Sink.INPUT, condition = "T(Runtime).getRuntime().exec('calc')")
注解是一个不可控的点
application.properties
spring.cloud.kubernetes.discovery.filter=T(Runtime).getRuntime().exec('calc')
可配合/actuator/env,但是无法命令执行,原因如下
执行Spel时需要执行上下文,SimpleEvaluationContext并不支持T(Runtime)
因此如果执行会报以下问题
@Query
@Query(value="select * from user where id = ?#{T(Runtime).getRuntime().exec('calc')}", nativeQuery=true)
可以getshell,但是可以忽略危害性。
基本上可以确定的是,Spel的存在可能基本是存在于配置类与注解中的。我在其他与Spring cloud相关的组件也存在一些Spel解析的地方但都是在配置类或者注解中,并且还是硬编码,这就没有放出来的意义了。
在我不断的挖掘过程中,终于找到了一个算是有问题的Spel表达式注入了。Spring-data-mongodb。说实话,这个漏洞挖掘起来应该没个运气都找不到。本人开始也不觉得是个漏洞,但是综合考虑了@Query和@Aggregation设计的目的,以及spring data jpa也支持这个,但是两者解析却存在十分大的差距,便觉得这个问题从正常角度确实不太算一个合法的漏洞,只能算是一个危险的API。但是从设计的角度,我觉得它是属于安全BUG。
其实拿spring data jpa来说,jpa也同样支持@Query中做spel。但是如果你尝试用类似于
@Query(value="?#{?0}", nativeQuery=true)
spring data jpa也支持spel,如下
@Query(select * from user where name= ?#{?1},nativeQuery=true) public User getUserByName(String name);
spring data jpa有两处会解析spel,第一次在启动的时候会触发,第二处在收到请求时触发。
先会经过if(!containsExpression(query))判断,这里的比较属于硬编码比较
即比较查询语句是否存在
这里的#entityName会被解析成Entity Pojo实体的类名称。所以无法控制。
当HTTP发起请求查询数据库,会走到org.springframework.data.jpa.repository.query.QueryParameterSetterFactory中的create方法
通过debug分析parse.parseExpression后,最后走到 org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression
最终经过eatExpression()时抛出异常,因为eatExpression直接分析 ?0 是否spel表达式,这里并没有 将 ?0 去引用来解析spel。也就是如果一定要spel命令执行,SQL必须是以下内容
@Query("select * from user where name = #{T(Runtime.getRuntime().exec('command'))}")
与上相似,Mongodb同样在@Query也支持Spel。初次之外,@Aggregation也支持Spel解析
@Query(":#{?0}") User getDataInfo(String info);
与JPA相似,能控的参数,都在请求后解析Spel,启动的Expression完全不可控,所以不考虑启动的问题。以下是Mongodb解析Expression的重要位置(org.springframework.data.mongodb.util.json.ParameterBindingJsonReader)。
其中PARAMETER_BINDING_PATTERN.matcher(expression)会检查表达式中是否为
expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
会直接将占位符替换成请求查询的内容。接着evaluateExpression函数直接对Spel表达式解析。
可以看到,例如Spring data系列的其他数据库依赖,基本逻辑是与jpa相关的,但是唯独mongodb存在这个问题(可能mongodb数据库有他独特的地方所以导致这个可能),但是无论如何,我测试多次,也只有mongodb出现在使用spel时,?0这样的占位符会被提前解析成请求的数据。
在我正准备全面的测试究竟哪些版本会出问题的时,我以为3.4.0以下的都存在这个问题,可是经过测试,似乎只有最新版的3.4.0是存在的。这让我感觉疑惑。
代码可能模糊,所以还是给出链接3.3.x(点击跳转)与3.4.x(点击跳转)
在解析@Query中的SQL语句是,会经过org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec中的decode方法。其中reader.readStartDocument()会尝试解析,确定是否正确,或异常的SQL。如果使用?:#{}这样的表达式,其实这里会报错的。在报错之后就是抛异常。我们都知道Java在处理异常时都是向上抛异常的,所以这里的异常会被decode中的try处理
由于返回的是一个Object对象不是Map,所以在catch中的处理就正常的结束了,这里catch竟然吃掉了异常!所以导致异常的SQL语句最终变成“正常的SQL语句”。而3.3.x乃至低版本,reader.readStartDocument();都在try之外,导致异常最终会被程序捕获,导致后面的spel的解析走不了。
上文都提到了Spel的一些挖掘情况,现在聊一些基于上面的研究对攻防的意义。
我也找了不少的配置可以解析Spel的,为什么这么做,因为我考虑到另一个组件的存在——actuator。说实话,虽然actuator未授权的情况基本很少,但是存在这个actuator/env未授权时,很多情况下还是停留在信息泄露,LandGrey的一个项目SpringBootVulExploit中已经详细描述了大部分的场景。我也在思考Spel在配置中的情况下,是否可以起到一定的作用,以至于放大actuator的危害,至少是个RCE吧。