Apache CXF 是 Apache 软件基金会下的一个开源项目,用于构建 Web Services 的应用程序。CXF 支持多种标准 Web Services 规范,如 JAX-RS 和 JAX-WS,并提供了基于这些规范的高效实现。
而cxf-spring-boot-starter-jaxrs 是 CXF 在 Spring Boot 中支持 JAX-RS 的 starter 包。它为您提供了构建 RESTful 服务所需的依赖和配置,并且与 Spring Boot 自动配置相集成,使得开发者可以更轻松地创建和部署 CXF JAX-RS 服务。
以cxf-spring-boot-starter-jaxrs-3.4.1为例,查看CXF构建的JAX-RS服务具体请求解析过程:
在处理HTTP请求期间,CXF会依次调用一系列消息拦截器,这些拦截器可以对请求或响应进行各种操作,例如添加、修改或删除头部信息、转换消息格式等。
org.apache.cxf.jaxrs.interceptor.JAXRSInInterceptor
是Apache CXF框架中的一个拦截器,它用于在JAX-RS服务请求之前拦截HTTP请求,并将其转换为CXF消息对象。其核心方法为processRequest()
。查看具体的实现:
首先会获取请求预处理器RequestPreprocessor,对请求进行预处理,这里会对message中的一些属性key进行赋值处理:
然后获取类似请求方法、请求真实路径等信息:
获取完相应的信息以后,会进行资源的匹配。在Apache CXF中,JAXRSUtils
是一个重要的工具类,它主要用于处理与JAX-RS相关的功能。根据请求path定位和调用相应的资源类或方法可以简单的分为这三个过程:
JAXRSUtils.getRootResources
获取所有的RootResourcesJAXRSUtils.selectResourceClass
选择RootResources中特定资源JAXRSUtils.findTargetMethod()
获取对应的资源方法如果matchedResources为null,会设置对应的response内容,并设置404状态码,抛出toNotFoundException异常,说明没有找到对应的资源类:
否则会调用调用JAXRSUtils.findTargetMethod()
获取对应的资源方法。
其中会对存储了已匹配资源及其对应值的映射matchedResources遍历matchedResources.entrySet()
中的每个实体,提取出ClassResourceInfo
对象(资源)和与之关联的MultivaluedMap
(值):
然后会遍历resource.getMethodDispatcher().getOperationResourceInfos()
(资源相关的所有方法信息),首先会提取出当前的URITemplate
,并与当前方法的路径进行匹配:
这里看一下uriTemplate.match()
的具体实现,同样的,对于每个路由规则,会判断请求路径是否与该规则匹配。主要是调用java.util.regex.Pattern#matcher方法进行匹配的:
如果第一次匹配失败的话,会判断是否是因为;
影响,通过获取PathSegment进行重组后进行二次匹配:
可以看到Apache CXF会对请求Path中的;
进行处理:
如果不为null并且成功匹配,会获取FINAL_MATCH_GROUP
的值,并根据该值确定是否为最终路径(finalPath
)。如果当前方法是子资源定位器(SubResourceLocator),将其添加到candidateList(候选列表)中。 如果是最终路径,将其添加到finalPathSubresources(一个存储了子资源定位器的链表)中。如果已匹配到最终路径,会进一步检查HTTP方法、请求类型和接受的内容类型等条件。最终会根据匹配的结果返回设置不同的状态码,然后在response返回对应的响应结果:
在整个解析过程中,存在一些关键属性,看看具体是怎么生成&处理的。
前面分析中会获取Endpoint Address(用于标识该端点提供的Web服务)。
getEndpointAddress
方法首先获取当前消息对象所对应的目标对象(Destination),然后判断目标对象是否是AbstractHTTPDestination类型。如果是,则表示当前消息对象与HTTP传输相关,需要根据当前的HTTP请求上下文信息来确定服务端点地址,这里是通过关键属性org.apache.cxf.transport.endpoint.address
来获取的:
看下org.apache.cxf.transport.endpoint.address
的封装过程:
org.apache.cxf.transport.servlet.ServletController
是Apache CXF框架中的一个重要组件,用于处理基于Servlet容器的HTTP传输方式的请求。它是CXF的Servlet控制器,负责将HTTP请求转发到CXF框架中的适当位置进行处理。该过程主要是在invoke方法完成的。
而这个过程中会调用updateDestination
方法用于更新服务端点地址信息。其会根据当前的HTTP请求上下文信息,将服务端点地址与Servlet容器的基本URL进行拼接,以便确定要调用的服务实现类:
查看该方法的具体实现,首先从当前请求方法中获取BaseURL,然后调用updateDestination
重载的方法:
在updateDestination
重载的方法里可以看到这里对request请求上下文进行了一定的封装,包括关键属性org.apache.cxf.transport.endpoint.address
,这里主要是将base以及ad合并并设置对应属性的值:
查看base以及ad的解析逻辑,首先是base,是调用org.apache.cxf.transport.servlet.ServletController#getBaseURL方法进行处理的:
如果forcedBaseAddress不为空,返回对应的值,否则调用BaseUrlHelper.getBaseURL方法进行处理,首先通过getRequestURL方法获取reqPrefix,然后通过getPathInfo获取路径信息:
如果pathInfo不是/
或者reqPrefix包含;
会进行额外的处理,否则直接返回前面getRequestURL方法获取到的reqPrefix。
首先根据reqPrefix创建URI对象,然后进行字符串的拼接(主要是获取协议以及主机名),然后调用request.getContextPath
获取请求的Context-path,最后通过request.getServletPath
方法获取HTTP请求的Servlet路径,此时新的reqPrefix组装完成:
最后是ad的处理逻辑,主要是从EndpointInfo的address属性获取,获取不到的话会设置为/
:
前面提到了Apache CXF在解析时,会结合JAXRSUtils.selectResourceClass()方法检索rawPath找到对应的资源,所以所以有必要看看rawPath具体是怎么生成的:
根据对应的方法,可以看到其是从Message的path_to_match_slash
属性获取的:
所以实际上需要查看Message的path_to_match_slash
属性是如何被赋值的。在processRequest方法中,会有预处理请求的过程:
在preprocess方法中,会对类似受支持的客户端类型进行处理,最终返回通过new UriInfoImpl(m, (MultivaluedMap)null)
创建的UriInfoImpl
对象的路径:
doGetPath方法实际上调用的是httpUtils.getPathToMatch()
:
因为此时path_to_match_slash
对应的值为null,所以会走到如下逻辑,而不是直接返回对应的值:
首先会尝试从getProtocolHeader()方法中获取请求地址。如果未找到,则默认为根路径/
。这里实际上是直接从message的org.apache.cxf.request.uri
键获取的:
然后根据?
处理参数部分,然后getBaseAddres()方法获取基本路径。在getBaseAddres()方法中,会先调用getEndpointAddress()方法对message进行处理:
在getEndpointAddress()方法中,首先从message中获取目标地址的Destination
对象。如果该对象不为空,则继续执行后面的步骤;否则,代码使用Message.ENDPOINT_ADDRESS
键从消息上下文中获取服务端点地址,并返回该地址。
如果Destination
不为null且对象是AbstractHTTPDestination
实例,则代码通过该对象获取服务端点信息EndpointInfo
。并尝试从HTTP请求中获取属性org.apache.cxf.transport.endpoint.address
的值,以覆盖服务端点地址。如果请求对象不为空且属性存在,则将属性值作为服务端点地址;否则,使用EndpointInfo
中的地址作为服务端点地址:
获取到endpointAddress后,会结合java.net``.URI
对象进行处理,主要是获取协议以及真实的path,处理后如果path为null,则返回/
,此时baseAddress处理完成,会调用getPathToMatch进行处理:
indexOf()
方法找到address
在path
中第一次出现的位置substring()
方法截取path
字符串,从地址部分的长度开始,得到处理后的路径部分addSlash
参数的值是否为true
,并检查处理后的路径部分是否以斜杠开头,否则在处理后的路径前追加/
:处理完后将pathToMatch的值赋予给message中的path_to_match_slash键对应的值。综上,path_to_match_slash键值与request请求中org.apache.cxf.transport.endpoint.address的值有很大的关系。
在cxf-spring-boot-starter-jaxrs中,会引入spring-boot-starter-web依赖,从而引入spring-boot-starter-tomcat,也就是说默认是使用tomcat作为中间件进行解析的。
跟Jersey类似,Apache CXF正常情况下也是不会对路径穿越符../进行额外的处理的。
例如如下的Resource Class:
@GET
@Path("/manage")
public Response manage() {
return Response.ok().entity("admin page").build();
}
尝试以/admin/info/../manage
访问会返回404,找不到对应的资源:
当使用默认的tomcat中间件进行解析时,假设当前Resource Class如下:
定义的路由@Path("/{path : .*}")
中的正则为.*
,表示匹配任意字符。
@GET
@Path("/{path : .*}")
public Response getUser(@PathParam("path") String path) throws IOException {
return Response.ok().entity(path).build();
}
正常情况下应该跟Jersey一样,访问/..
也是能匹配到该资源的,但是实际上访问会返回404:
看下具体的原因,根据前面的分析,Apache CXF在解析时,会结合JAXRSUtils.selectResourceClass()方法检索rawPath找到对应的资源。主要跟path_to_match_slash
和org.apache.cxf.transport.endpoint.address
这两个属性有关。看看当请求/admin/..
时具体的属性是如何赋值的。
首先是org.apache.cxf.transport.endpoint.address
,其值是由base跟ad组成的,首先是base,其是在getBaseURL方法处理的:
因为通过request.getPathInfo()获取pathInfo时是会对请求的path进行规范化处理的,所以/admin/..
会变成/
,那么此时不会重组reqPrefix,直接返回原始的内容:
加上ad的值为/
,此时request上下文中org.apache.cxf.transport.endpoint.address
对应的值为http://127.0.0.1:8080/admin/../
然后是path_to_match_slash
,会在第一次调用httpUtils.getPathToMatch()
时进行设置。
首先从从message的org.apache.cxf.request.uri
键获取requestAddress为/admin/..
:
然后获取baseAddress,根据前面的分析,这里会从org.apache.cxf.transport.endpoint.address
键中获取对应的值,然后进行额外的处理,处理后返回内容为/admin/../
:
此时会调用getPathToMatch进行二次处理,因为requestAddress结尾追加/
后与baseAddress相等,当ind=0时经过切割后,返回的值为/
:
所以当Apache CXF在尝试获取rawPath时,得到的是/
:
此时在查找对应的ResourceClass时会返回null从而无法继续匹配资源,最终返回404。
那么是不是说当前场景下类似@Path("/{path : .*}")
就没办法获取到/..
呢?问题主要在org.apache.cxf.transport.endpoint.address
里,request.getPathInfo()获取pathInfo时是会对请求的path进行规范化处理的,所以/admin/..
会变成/
,那么此时不会重组reqPrefix,直接返回原始的内容:
同样是上面的解析过程,当请求/admin/info/..
时,此时pathInfo经过规范化处理后为/admin/
,此时会对preqPrefix进行重组:
加上ad的值为/
,此时request上下文中org.apache.cxf.transport.endpoint.address
对应的值为http://127.0.0.1:8080/
。
既然这里存在差异,当在getPathToMatch方法处理时,baseAddress为/
,requestAddress为/admin/info/..
:
经过处理后,最后返回的值为/admin/info/..
,也就是说rawPath的值也是一致的,那么此时查询ResourceClass是可以匹配到对应的资源的,所以可以成功访问资源:
通过上面对Apache cxf请求解析过程的分析,结合现有的一些漏洞场景,列举下其中潜在的安全风险。
ContainerRequestContext
表示当前请求的上下文信息,包括请求头、URI、HTTP 方法、实体等信息。一般情况下会结合ContainerRequestFilter
进行使用。同样的Apache CXF也支持对应的实现。
在基于ContainerRequestFilter
实现的权限Filter中,某些时候可能会有基于URI白名单的方式对特定的请求进行放行。跟Servlet中的request.getRequestURI()方法一样,当获取请求Path的方法不规范时,可能会存在绕过权限Filter的风险。
看看获取请求Path的方法主要有哪些,效果是什么。
javax.ws.rs.core.UriInfo
提供了有关当前请求URI的各种信息。可以通过ContainerRequestContext的getUriInfo方法进行获取:
UriInfo uriInfo = requestContext.getUriInfo();
获取到UriInfo后,可以调用其方法来获取请求Path信息。
以请求http://127.0.0.1:8080/api/manage;bypass/ 为例,查看各个方法的返回值:
方法名 | 功能 | 返回值 |
---|---|---|
getAbsolutePath() | 获取请求的绝对路径 | http://localhost:8080/admin/manage;bypass/ |
getPath() | 获取请求的路径部分 | api/manage;bypass/ |
getRequestUri() | 返回一个URI对象,表示客户端发出请求的完整请求URI | http://localhost:8080/admin/manage;bypass/ |
getPathSegments() | 返回一个List对象,其中包含路径中每个段的字符串值 | [admin, manage;bypass, ] |
可以看到获取到的返回值均未进行标准化处理。如果只是简单的使用startwith或者contiain方法进行白名单/黑名单的鉴权处理的话,在某种情况下是存在绕过的可能的。
除了UriInfo以外,使用requestContext.getUriInfo().getRequestUri()
方法来获取访问请求的URI后,可以调用相应方法来获取各种URI组件的信息,包括请求的path,同样以以请求http://127.0.0.1:8080/api/manage;bypass/为例,查看各个方法的返回值,同样的均未进行标准化处理:
方法名 | 功能 | 返回值 |
---|---|---|
getPath() | 返回请求URI的路径部分,并解析任何转义字符(如URL编码的斜杠) | /api/manage;bypass/ |
getRawPath() | 返回请求URI的路径部分,但不进行解码或规范化 | /api/manage;bypass/ |
其次,requestContext.getUriInfo().getRequestUri().compareTo()
方法用于比较两个URI,这个方法返回一个整数值,表示两个URI的排序顺序。如果两个URI相等,则返回0;如果第一个URI小于第二个URI,则返回负数;否则,返回正数。但是该方法比较http://127.0.0.1:8080/api/manage;bypass/
和http://localhost:8080/admin/manage;bypass/
同样会认为不是一个URI。
/
结尾的Bypass例如如下的例子,正常来说访问/manage
会匹配到manage方法然后进行相应的处理:
@GET
@Path("/manage")
public Response manage() {
return Response.ok().entity("admin page").build();
}
根据前面的分析,Apache CXF主要的路径匹配是在org.apache.cxf.jaxrs.model.URITemplate#match方法进行处理的,对于每个路由规则,会判断请求路径是否与该规则匹配。主要是调用java.util.regex.Pattern#matcher方法进行匹配的:
其中请求的path是可以以/
结尾的:
同样的,跟Spring/Jersey类似,Apache CXF在解析时如果请求路径有尾部斜杠也能成功匹配(类似Spring里TrailingSlashMatch的作用):
那么在使用filter或者某些权限控制框架进行鉴权处理的的时候需要额外注意,避免绕过的风险。
以shiro为例,对应的权限控制如下,/admin
目录下的所有接口都需要经过安全认证才能访问:
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
bean.setUnauthorizedUrl("/unauthorizedurl");
Map map = new LinkedHashMap<>();
map.put("/doLogin/", "anon");
map.put("/admin/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
假设对应的请求资源如下:
@GET
@Path("/{path : .*}")
public Response getUser(@PathParam("path") @Encoded String path) throws IOException {
return Response.ok().entity(path).build();
}
正常情况下,在缺少安全认证的情况下访问/admin/page,会返回302状态码重定向到login页面:
利用shiro会解析..
而Apache CXF不会的差异,因为可以这里路由匹配的正则表达式为.*
表示匹配任意字符,那么理论上请求/admin/..
即可绕过对应的限制。
可以看到绕过了shiro的权限控制,但是没办法访问相应的资源:
根据前面的分析主要是处理org.apache.cxf.transport.endpoint.address
时,request.getPathInfo()进行了规范化处理。实际上只需以/admin/../info
访问即可绕过并访问对应的资源了:
在Apache CXF中,同样可以通过在 @Path
注解中使用 {variable:regexp}
的形式,来指定请求路径中的变量名和对应的正则表达式,例如如下的例子:
通过@PathParam
注解从请求路径中提取和获取指定的参数值,并将其作为方法的参数path进行传递:
@GET
@Path("/download/{path : .*}")
public Response fileDownload(@PathParam("path") @Encoded String path) throws IOException {
File file = new File(resource + path);
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
return Response.ok().entity(buffer).build();
}
因为前面定义的路由@Path("/{path : .*}")
中的正则为.*
,表示匹配任意字符。因为Jersey不会对../
进行额外的处理,所以是否能获取用户输入的多个路径穿越符../
主要还是受中间件的影响。
因为在Tomcat的场景下(cxf-spring-boot-starter-jaxrs默认是使用Tomcat进行处理的),此时漏洞利用需要考虑请求URI的目录层级以及/../
个数限制的关系。
这里以jetty为例,具体解析过程可以参考之前的分析:
只需要以..//..
的形式进行访问即可达到利用的效果:
同理,对于非正则的情况,Jersey默认是会对url编码进行解码的(使用@Encoded
注解可以防止Jersey对URI进行解码),也就是说可以以%2f
的方式获取到路径穿越需要的元素,剩下的就是中间件解析的问题了,例如tomcat默认会对%2f进行拦截,这样请求是不可行的:
@GET
@Path("/download/{path}")
public Response fileDownload(@PathParam("path")String path) throws IOException {
File file = new File("/tmp" + path);
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
return Response.ok().entity(buffer).build();
}
同样的如果是Jetty环境下,只需要将/
url编码,然后以..//..
的形式进行访问即可:
Apache CXF并不直接提供 Controller 的概念。通常情况下,Apache CXF中的资源类(Resource Class)可以看做是类似于 Spring 中的 Controller 的实现方式。
默认情况下,资源类也是单例的(Singleton)。也就是说,在应用程序初始化时,Apache CXF会创建每个资源类的一个实例,并由框架维护其生命周期,以供后续请求使用。而在单例模式中,由于多个线程共享同一个对象实例,因此存在线程安全问题。
下面证明Apache CXF 的Resource Class是单例的:
1.首先创建一个简单的 Resource Class:
@Path("/admin")
public class ApiController {
private int count = 0;
@GET
@Path("/count")
public Response getCount() {
count++;
return Response.ok().entity("count:"+count).build();
}
}
2.启动应用程序,并使用浏览器或其他客户端工具访问该接口http://127.0.0.1:8080/api/admin/count
:
3.多次访问该接口,并观察返回结果:
count=1
count=2
count=3
...
从输出结果可以看出,在多次访问同一个接口时,每次都会增加 count 的值,说明不同的请求实际上都在使用同一个 Resource Class 实例。
64 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!