问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
浅谈Spring WebFlux文件上传解析
渗透测试
在 Spring WebFlux 中,可以使用 org.springframework.http.codec.multipart.FilePart 来处理文件上传。浅谈其具体实现。
0x00 前言 ======= 在 Spring MVC 中,可以使用 `org.springframework.web.multipart.MultipartFile` 类来处理文件上传,将multipart中的对象封装到MultipartRequest对象中然后进行相应的处理: ```Java @RequestMapping(value={"/uploadFile"},method={RequestMethod.POST}) public String uploadFile(MultipartFile file,String type,HttpServletResponse response) throws Exception{ String UPLOADED_FOLDER="/resource/upload/"; if(!file.isEmpty()){ String path = UPLOADED_FOLDER + file.getOriginalFilename(); File targetFile = new File(path); FileUtils.inputStreamToFile(file.getInputStream(),targetFile); ...... ...... } } ``` 而在 Spring WebFlux 中,不能直接使用 `org.springframework.web.multipart.MultipartFile` 类来处理文件上传,因为它是为 Spring MVC 提供的传统 Servlet 基础的文件上传功能。而Spring WebFlux是基于 Reactor 的。 在 Spring WebFlux 中,可以使用 `org.springframework.http.codec.multipart.FilePart` 来处理文件上传。`FilePart` 是 Spring WebFlux 提供的用于表示上传文件的类,例如如下的例子: ```Java @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Mono upload( @RequestPart("file") final FilePart filePart, final FormData formData ) { log.debug("formData => {}", formData); System.out.println(filePart.filename()); final File directory = new File(UPLOAD_DIRECTORY); if(!directory.exists()){ directory.mkdirs(); } final File file = new File(directory, filePart.filename()); return filePart .transferTo(file) .then(Mono.fromCallable(() -> { final Map map = new HashMap<>(); map.put("name", file.getName()); map.put("lastModified", file.lastModified()); map.put("size", file.length()); return map; })); } ``` `DefaultServerWebExchange` 是 Spring WebFlux 框架中的一个类,它实现了 `ServerWebExchange` 接口,用于表示一个服务器和客户端之间的交互。其封装了底层的 HTTP 请求和响应,提供了访问请求和响应信息的方法,如获取请求方法、路径、请求头、请求体等,以及设置响应状态码、响应头、响应体等。 其中的`initMultipartData` 方法的用户初始化处理 `multipart` 请求时的相关数据,以spring-web-5.3.27为例,查看其解析过程:  0x01 判断是否是Multipart请求 ===================== 1.1 判断方式 -------- 在`initMultipartData` 方法中,首先会判断当前请求是否是Multipart上传请求,首先会从获取当前请求header的ContentType,然后调用org.springframework.http.MediaType#isCompatibleWith方法进行判断:  实际上调用的是其父类MimeType的方法:  首先如果传入的 other 参数为 null,则返回 false。如果两个 MIME 类型的类型和子类型都不是通配符类型(即非 `*`),会比较两者是否类型相同且子类型相同,是的话则返回true:  例如在判断文件上传请求时,会判断是否是匹配`multipart/form-data`:  如果一个 MIME 类型的子类型是通配符类型 `*`,则返回true:  也就是说,当Content-Type为`multipart/*`或者`multipart/form-data`都会认为是Multipart请求。(跟Spring MVC一样支持大小写转换)。 当然了也支持通过配置`consumes = MediaType.MULTIPART_FORM_DATA_VALUE`,限制接受的内容类型为`multipart/form-data`。仅当请求的Content-Type为`multipart/form-data`时,该方法才会被调用: ```Java @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Mono upload(@RequestPart("file") final FilePart filePart, final FormData formData ) { ...... } ``` 1.2 与其他框架的区别 ------------ - Struts2 ```Java if (content_type != null && content_type.contains("multipart/form-data")){ ...... } ``` - Spring MVC 主要有两个解析器: 1. **StandardServletMultipartResolver解析器:** 通过判断请求的Content-Type来判断是否是文件请求: ```Java public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? "multipart/form-data" : "multipart/")); } ``` 其中,strictServletCompliance是StandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliance为false,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliance为true,则需要请求头以multipart/form-data开头。 2. **CommonsMultipartResolver解析器:** CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求: ```Java public boolean isMultipart(HttpServletRequest request) { return (this.supportedMethods != null ? this.supportedMethods.contains(request.getMethod()) && FileUploadBase.isMultipartContent(new ServletRequestContext(request)) : ServletFileUpload.isMultipartContent(request)); } ``` supportedMethods成员变量表示支持的请求方法,默认为null,可以在初始化时指定。主要判断的方法在isMultipartContent(),请求头Content-Type为以multipart/开头即会认为是Multipart请求:  0x02 上传解析请求过程 ============= 在Spring WebFlux中,`SynchronossPartHttpMessageReader` 和 `DefaultPartHttpMessageReader` 都是用于处理和解析 HTTP multipart 请求的组件。 **默认情况下,Spring WebFlux使用`DefaultPartHttpMessageReader`**。 也可以使用 `SynchronossPartHttpMessageReader`,它是基于 [Synchronoss NIO Multipart](https://github.com/synchronoss/nio-multipart) 库的。两者都可以通过 `ServerCodecConfigurer` Bean进行配置: ```Java @Configuration @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { SynchronossPartHttpMessageReader reader = new SynchronossPartHttpMessageReader(); reader.setMaxParts(1); reader.setMaxDiskUsagePerPart(10L * 1024L); reader.setEnableLoggingRequestDetails(true); MultipartHttpMessageReader multipartReader = new MultipartHttpMessageReader(reader); multipartReader.setEnableLoggingRequestDetails(true); configurer.defaultCodecs().multipartReader(multipartReader); } } ``` 下面查看具体的解析方式。 2.1 DefaultPartHttpMessageReader解析 ---------------------------------- 在解析每一个part的时候,会根据header调用org.springframework.http.codec.multipart.PartGenerator#newPart进行处理:  这里会判断是否是formFiled:  这里会判断上传的filename参数是否为空,而filename参数是通过org.springframework.http.ContentDisposition#parse方法解析的,这里对相关的http内容进行了处理跟封装:  查看具体的处理过程,实际上跟Spring MVC中使用StandardMultipartHttpServletRequest解析器解析是类似的:  如果传入的multipart请求无法直接使用filename=解析出文件名,会判断filename是否以=?开头,是的话会进入BASE64\_ENCODED\_PATTERN的正则匹配中,大致的可以知道需要匹配的内容应该是`=?编码方式?B?编码内容?=` :   对于filename\*=的内容,例如传入的`UTF-8'1.jpg'1.jsp`会被解析成`UTF-8`编码,最终的文件名为`1.jsp`,而`1.jpg`则会被丢弃(获取到filename\*=后的内容后,首先切割第一个',通过Charset获取对应的编码方式,然后再切割第二个' 后的内容,并根据前面的编码方式进行解码操作,最后返回对应的filename。可以看到实际上两个`'`之间是可以任意填充内容的(单引号之间的内容在实际解析时会被忽略掉)):   如果不满足上述条件的话,则直接将value赋值给filename,实际上就是获取`"`间的内容: ```Java String value = part.startsWith("\"", eqIndex + 1) && part.endsWith("\"") ? part.substring(eqIndex + 2, part.length() - 1) : part.substring(eqIndex + 1); ``` 最后会将分析的结果封装成ContentDisposition对象进行返回:  2.2 SynchronossPartHttpMessageReader解析 -------------------------------------- 该解析器在使用时除了需要额外配置以外,还需要引入对应的依赖:  在解析每一个part的时候,会调用org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader中FluxSinkAdapterListener的createPart方法进行处理:  实际上调用的是org.synchronoss.cloud.nio.multipart.MultipartUtils#getFileName对请求的headers进行处理:  在getFileName方法中,首先会调用getHeader方法获取contentDisposition:  这里会先将headerName转换成小写(multipart请求的话,headerName就是Content-disposition),然后再获取对应的值进行返回:  获取到contentDisposition后,首先将其转换成小写,若其是以form-data或者attachment开头的话,调用org.synchronoss.cloud.nio.multipart.util.ParameterParser解析器进行处理:  查看org.synchronoss.cloud.nio.multipart.util.ParameterParser解析器的核心方法parse,这里主要是将contentDisposition转换成char数组进行处理:  这里会根据`=`划分对应的paramName和paramValue,如果当前字符是等号 (=),则提取参数值,同时还会对参数值进行解码操作:  查看解码操作org.synchronoss.cloud.nio.multipart.util.MimeUtility#decodeText的具体实现,可以看到这里实际上是对`=?`开头的内容进行解析:  在ParameterParser解析器解析完成后,如果解析的param中包含fileName,会进行trim操作,删除字符串开头和结尾的空白字符:  最后如果返回fileName不为null,则会创建SynchronossFilePart对象,从而进一步被处理或者传递给其他组件。例如保存文件、处理表单数据等。 2.3 两者的区分 --------- - SynchronossPartHttpMessageReader不支持使用`filename*=`进行文件名解析:  - SynchronossPartHttpMessageReader会对解析的filename进行trim操作,删除字符串开头和结尾的空白字符,而DefaultPartHttpMessageReader不会:   - 获取filename的位置不一样 DefaultPartHttpMessageReader在解析时会有这么一个判断条件,当当前解析的参数为filename且值为null的时候才会进行解析,第一次解析时已经获取到对应的值了,所以后续的值不会进行解析,也就是说当存在多个filename参数的时候,其会获取第一个的值:   SynchronossPartHttpMessageReader在解析时会通过HashMap的形式来存储解析到的param以及对应的值,那么当第二次获取到filename参数时,会覆盖掉原来的值,所以当存在多个filename参数的时候,其会获取最后一个的值:   - 对`=?`开头的内容解析的差异 DefaultPartHttpMessageReader在解析时是通过正则进行匹配的,并且需要value以`=?`开头:   SynchronossPartHttpMessageReader在解析时若当前字符为一个空白字符(空格、制表符、回车或换行)时,记录空白数字的起始和结束位置并进行处理:   0x03 获取上传的文件名 ============= Spring WebFlux在处理multipart请求时,如果请求中包含文件上传的部分(Part),可以使用`filePart.filename()`方法来获取该部分对应的文件名。该方法返回一个字符串,表示文件的原始文件名。 通常,开发人员可以使用该方法来获取上传文件的文件名,并进行相应的处理,例如文件存储、文件名校验等操作。 当使用DefaultPartHttpMessageReader解析进行解析时,实际上是从ContentDispositio对象中获取的:  使用SynchronossPartHttpMessageReader解析时,是从SynchronossFilePart对象的filename属性进行获取的:  根据前面的分析,两者在整个过程没有对类似../的路径进行检查&过滤,由于获取的fileName未进行安全处理,在使用File创建文件时,若路径处path写入../../穿越符号,是可以跨目录新建文件的。 那么在实际利用时,可以尝试通过linux写入定时任务(etc/cron.d/下的文件可以以任意后缀命名,如果未对filename进行重命名的话还可以绕过上传的后缀限制)、ssh公钥(需要满足root权限),甚至是替换 JDK HOME 目录下的系统 jar 文件,再主动触发 jar 文件里的类初始化来达到执行任意代码的效果。 0x04 SpringMVC的Multipart请求绕过 ============================ 众所周知,在SpringMVC中,当接收到一个multipart请求后,会调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析并将解析结果封装到中HttpServletRequest。很多时候一些安全检测的filter在进行类似SQL注入、XSS的过滤时没有考虑到上述情况,那么就可以尝试将普通的GET/POST转换成Multipart请求,绕过对应的安全检查。 以如下Controller为例,查看Spring WebFlux是否也存在类似的绕过场景: ```Java @RequestMapping("/manage") public String manage(@RequestParam String param) { return "param:"+param; } ``` 正常情况下,通过GET请求访问该资源可以正常获取param参数的内容:  当使用POST方法请求时,会返回400 Status:  查阅官方文档https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/requestparam.html 可知: 在Spring webflux中,@RequestParam注解仅支持url传参方式,无法处理form-data和multipart的方法。如果想处理类似的请求,可以使用ServerWebExchange进行处理。 ```Java The Servlet API “request parameter” concept conflates query parameters, form data, and multiparts into one. However, in WebFlux, each is accessed individually through ServerWebExchange. While @RequestParam binds to query parameters only, you can use data binding to apply query parameters, form data, and multiparts to a command object. ``` 例如需要通过form-data的方式获取param参数: ```Java @PostMapping("/manage") public Mono manage(ServerWebExchange exchange) { return exchange.getFormData() .flatMap(formData -> { String paramName1 = formData.getFirst("param"); // 获取 POST 参数 "param" // 处理参数并返回结果 return Mono.just("Param: " + paramName1); }); } ```  同样的,上述Controller代码并不能处理multipart的请求:  如果需要获取multipart的数据,需要额外的调用ServerWebExchange方法进行处理: ```Java @PostMapping(value = "/handleMultipartRequest", consumes = "multipart/form-data") public Mono handleMultipartRequest(ServerWebExchange exchange) { return exchange.getMultipartData() .flatMap(parts -> { Part part = parts.getFirst("param"); if (part instanceof FormFieldPart){ return Mono.just("param: " + ((FormFieldPart) part).value()); } // 处理参数并返回结果 return Mono.just("param: " + null); }); } ```  也就是说,默认情况下,Spring WebFlux并不像Spring MVC一样,能在多个请求方式之间灵活解析转换。
发表于 2023-06-26 09:00:02
阅读 ( 7642 )
分类:
代码审计
0 推荐
收藏
0 条评论
请先
登录
后评论
tkswifty
64 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!