Spring + Jetty 场景下越权分析

date
Jun 28, 2022
slug
spring-jetty-vuln-analyse
status
Published
tags
Java安全
安全研究
summary
在群里看到两个关于权限校验的漏洞,然后想起之前遇到一个jetty+Spring环境的越权案例,毕竟比起使用 Tomcat,jetty的环境在云环境上有更高的效率,各互联网中大厂都逐渐使用jetty部署应用,这里分析一下
type
Post

前言

在群里看到两个关于权限校验的漏洞,然后想起之前遇到一个jetty+Spring环境的越权案例,毕竟比起使用Tomcat,jetty的环境在云环境上有更高的效率,各互联网中大厂都逐渐使用jetty部署应用,这里分析一下

环境搭建

新建一个Springboot版本为2.1.9.RELEASE的demo环境,因为需要jetty环境,就需要将原来的Tomcat去除掉,加载filter的config如下:
FilterConfig:
package com.example.jettytest.Config;

import com.example.jettytest.Filter.TestFilter;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public TestFilter testFilter(){
        return new TestFilter();
    }
    
    @Bean
    public FilterRegistrationBean registrationBean(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(testFilter());
        bean.addUrlPatterns("/portals/resume/*");
        bean.setName("testjetty");
        bean.setOrder(1);
        return bean;
    }

}
具体的环境供大哥们测试: https://github.com/0v3rW4tch/Jetty-Spring-demo
触发效果对比
notion image

流程分析

diff了绕过与未绕过payload之间函数调用栈,发现不同点就在于多出了自定义filter的处理上
notion image
另外从函数调用栈也能分析出Jetty的一个运行特点,是一个链式调用的结构,从一个ServletHandler到下一个ServletHandler的处理,每个handler处理具体的操作流程
notion image
类似下面的图结构,该图是以Jetty中ScopedHandler为例,本质上ServletHandler继承了ScopedHandler,所以原理是一样的
图来自学城《比较:Jetty如何实现具有上下文信息的责任链?》
图来自学城《比较:Jetty如何实现具有上下文信息的责任链?》
再具体往下跟进就是Spring的处理流程了,但是此时处理的url是没有经过处理的,但还是能够命中对应Controoler的功能,所以这块地方也是有问题的:
notion image
所以经过初步调试,这个问题可以判断为是jetty+Spring场景导致的绕过,于是分析就放在两个重点上:
  1. jetty对filter的处理上
  1. Spring对路由的处理上

深入分析

jetty对filter的处理
从filter处理入口分析 ,在org.eclipse.jetty.servlet.ServletHandler#doHandle 会初始化相关的filterchain,此外,在jetty启动服务的时候,会直接将全局的所有filter以及对应的url mapping内容存放在_filterMappings 这个变量中,后续初始化filterchain会根据这个变量进行处理
notion image
通过getFilterChain 函数去确定当前路由所需要的filterchain,jetty处理filter的关键点就在于org.eclipse.jetty.servlet.ServletHandler#getFilterChain ,这个过程中,jetty会对处理过的路由做一次缓存,缓存的filterchain内容会放在_chainCache 变量中,这是一个Map类型的变量,后续只要是重复出现的路由都会使用这个Map去匹配key,快速返回对应的filterchain
notion image
如果不存在缓存就会迭代所有的_filterMappings ,然后会把所有路由能够匹配上的filter添加到filters里面,也就是生成这个对应路由的filterchain了,这个也是jetty处理filter路由的关键了,此时会走这样一个流程,org.eclipse.jetty.servlet.FilterMapping#appliesTo(java.lang.String, int) —>org.eclipse.jetty.http.PathMap#match(java.lang.String, java.lang.String, boolean) —>org.eclipse.jetty.http.PathMap#isPathWildcardMatch —> java.lang.String#regionMatches(int, java.lang.String, int, int)
notion image
最后在java.lang.String#regionMatches(int, java.lang.String, int, int)函数进行具体的匹配,可以在这里分析为什么//这样的payload是可以绕过的
一开始filter对应的Mapping是/portals/resume/* ,处理之前会先把/* 删除掉,剩下/portals/resume ,以这个字符的长度再去与输入的路由/portals//resume/get进行比较,一旦出现不一样的的字符就会认为这个当前的filter这个路由不匹配
notion image
所以可以得出一个结论,只要这个filter的路由通配符之前的内容有一点点不一样都不会把相关的filter放进filterchain里面,也就不会进行相关的filter检测
 
 
 
Spring对路由的处理
org.springframework.web.servlet.DispatcherServlet#doDispatch 入口分析,相关的请求会进入到org.springframework.web.servlet.DispatcherServlet#getHandler 最后会进入到org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
notion image
getHandlerInternal函数中有一个lookupHandlerMethod 方法,是Spring处理路由的关键方法,可以分析这个函数,去了解Spring的路由解析的原理
该方法首先会寻找是否存在对应的路由,在this.mappingRegistry 里面加载了项目里面所有路由,触发org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#getMappingsByUrl 函数,以输入的/portals//resume/get为例,一开始是找不到对应的路由在这里面的,所以directPathMatches 返回了null,所以会进入到matches.isEmpty() 的判断分支,在这个分支里面遍历所有路由进行具体路由解析操作,会把适合/portals//resume/get 的路由匹配出来,代码的注释也很清楚了// No choice but to go through all mappings...
notion image
后续跟进到org.springframework.web.servlet.mvc.method.RequestMappingInfo#getMatchingCondition ,会在这一处地方进行具体路由的正则匹配
notion image
继续往下跟,跟进到org.springframework.util.AntPathMatcher#doMatch ,在这里可以观察到是如何进行路由解析的,对应的匹配的路由/portals/resume/get 会被分割成["portals", "resume", "get"],而输入的路由/portals//resume/get 也会被分割成["portals", "resume", "get"] ,然后再对这个数组内容进行相应的判断,最后返回是否匹配,可以看到代码里面关键处理path的地方在tokenize 的相关操作
notion image
跟进tokenize 相关函数并进行分析,其实本质上的功能就是以/进行分割 ,并取到内容不为空的作为判断的列表内容
notion image
所以根据分析结果,绕过的payload其实可以在中间添加上无数的斜杠也能打
notion image
 
 

思路扩展

那为什么;%2f 之类的操作也是可以绕过的呢
notion image
原因在于org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal 执行过程中会存在一个path的处理过程,在org.springframework.web.util.UrlPathHelper#getLookupPathForRequest函数中getPathWithinServletMapping 函数有具体的操作,默认会走这个分支,因为alwaysUseFullPath 变量默认初始化未false
notion image
只要path有点不同的话,就会返回servletPathservletPath 是通过getServletPath 获取的,里面走的是HttpServletRequestgetServletPath 函数,可以尝试着使用./../; 等符号扩展新的攻击向量,以上攻击向量组合最终目的都是为了经过getServletPath都会变成中间多个斜杠的路由形式,比如:/portals///resume/get ,后面的流程就和之前触发controller的流程一样了
/portals/;/resume/get
/portals/;xxxxxxx/resume/get     #这里的xxxx代表任意字符
/portals/;/;/resume/get
/portals/;xxxx/;xxxxx/resume/get
/portals/abc/..//resume/get
/portals/abc/haha/../..//resume/get
/portals//abc/../resume/get
portals//abc/../;xxx/./resume/get
/portals/.//resume/get
/portals/%2f%2f/resume/get
/portals/%2f%2f/;xxxxxxx/resume/get

高版本测试

JDK 8 下使用的都应该是jetty 9(9.4.48.v20220622是目前的最高版本),高版本jetty需要jdk 11+ 的支持,所以高版本对业务并不友好
notion image
测试了以下版本:
测试很奇怪的是2.5.13的spring-boot-starter-parent版本的jetty版本比2.6.x更高,但是却还是可以打,推测应该跟spring的版本有一定关系
成功防护的版本调试发现应该是对路径做了一些防护,导致最后将请求重置,回复一个/error路由的response
最后在调试的时候发现,果然是spring的问题,修改的地方主要在org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod 之前在这个函数里面存在addMatchingMappings 会遍历所有的Mapping寻找一个最合适的路由,但是从2.6.0开始这个方式就改变了,没有这个路由就是没有,不会去寻找一个类似的路由,最后返回handleNoMatch 对象,后续就会触发请求返回404了
notion image
那为什么2.5.13又能够可以越过权限呢?明明spring-core版本比2.6.0的还高,分析了一下差异:主要在 org.springframework.web.servlet.handler.AbstractHandlerMapping#initLookupPath ,在这个函数里面
notion image
2.6.0走的是上面的if条件,而2.5.13走的是else的分支,else分支会返回/portals/resume/get 路径,导致后面能够寻找到这个路由,而if语句返回的路径是/portals//resume/get 根据上面严格寻找路由的方法当然在后面的流程中就找不到了,这个问题应该是跟Spring的匹配模式相关(AntPathMatcher与PathPattern之间谁用谁的问题),2.6.0之后应该是强行使用PathPattern模式了,也导致问题的修复了
 
jetty 10测试
Jetty 10测试,必须要jdk 11以上的版本启动中间件,不然会报错
notion image
Jetty 10.0.11 (jetty 10的最新版本)是不会存在这个问题的
notion image
原理是啥?问题主要是在Jetty的处理上,存在一个解析HTTP请求内容的函数
org.eclipse.jetty.server.HttpConnection#parseRequestBuffer—> org.eclipse.jetty.http.HttpParser#parseFields
后面会到org.eclipse.jetty.http.UriCompliance#checkUriCompliance这个函数里面检测路径相关的信息
notion image
判断是否符合一下这些特性,如果符合这些特性就会产生一个badMessage,抛出异常,当然在respose就已经能看到这个错误信息了,reason: Ambiguous URI empty segment
notion image
这些属性可以在官网里面找到,分别代表不同的URL特性:
notion image
查了一下,在一个changelog里面看到详细的版本更换信息,在10.0.5和11.0.5之前都是没有这个属性的,也就是这之后的版本才修的// 的这个问题
notion image
测试了一下10.0.4版本,确实没防住
notion image

总结

jetty+Spring场景下可以使用// 进行权限绕过的测试,在知道实际路由的情况下,打实际站点的时候完全可以测试一下,可能就成了呢

© 4me 2021 - 2024