浅析SmartBi逻辑漏洞(3)

浅析SmartBi逻辑漏洞(3)

前言

这个系列终于到了第三篇,指条路,如果忘记了可以再看看之前写的文章

浅析Smartbi逻辑漏洞

浅析Smartbi逻辑漏洞(2)

之前我就曾在第二篇末尾提到过(没人继续深入看),仍然存在一个问题,今天这个问题终于得以修复

image-20240419101828056

当然老规矩,这里仅分享逻辑漏洞部分补丁绕过思路,不提供完整payload

补丁

补丁中新增了一个规则

1
2
3
4
5
"rules": [{
"className": "*",
"methodName": "*",
"type": "RejectGetAndFormData"
}

一眼丁真,鉴定为不能同时使用GET与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
protected int patchRMI(String className, String methodName, HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
String contentType = request.getContentType();
if (this.isNullOrEmpty(contentType)) {
contentType = "";
}

String method = request.getMethod();
if (this.isNullOrEmpty(method)) {
method = "";
}

if ("get".equals(method.toLowerCase(Locale.ENGLISH)) && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/form-data")) {
return 1;
} else {
String params = request.getParameter("params");
if (params == null) {
params = (String)request.getAttribute("params");
}

if (this.isNotNullAndEmpty(className) && this.isNotNullAndEmpty(methodName) && this.isNotNullAndEmpty(params)) {
request.setAttribute("className", className);
request.setAttribute("methodName", methodName);
request.setAttribute("params", params);
}

return 0;
}
}

利用分析

其实这个有两种打法,因为两个版本代码不一样,这里我们以V9代码为例

简单做个回顾(详细的自己看老版本分析),当我们调用RMIServlet之前需要绕过CheckIsLoggedFilter的判断,保证在CheckIsLoggedFilter中指向的类与方法在白名单当中,而在RMIServlet实际调用时指向真正要执行黑名单方法

接下来回到CheckIsLoggedFilter,如果我们请求是GET方法,之后就会通过httpRequest.getParameter获取我们的className\methodName\encode参数

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
65
66
67
68
69
70
71
72
73
74
boolean isRmi = "/vision/RMIServlet".equals(((HttpServletRequest)httpRequest).getServletPath());
String requestStr;
if (isRmi) {
String queryString = ((HttpServletRequest)httpRequest).getQueryString();
String params;
if (queryString != null && queryString.startsWith("windowUnloading")) {
params = queryString.length() > "windowUnloading".length() && queryString.charAt("windowUnloading".length()) == '=' ? "windowUnloading=&" : "windowUnloading&";
.......此处省略.......
} else if ("GET".equals(((HttpServletRequest)httpRequest).getMethod())) {
className = ((HttpServletRequest)httpRequest).getParameter("className");
methodName = ((HttpServletRequest)httpRequest).getParameter("methodName");
encode = ((HttpServletRequest)httpRequest).getParameter("encode");
}

if (encode != null && className == null && methodName == null) {
String[] decode = RMICoder.decode(encode);
className = decode[0];
methodName = decode[1];
params = decode[2];
((HttpServletRequest)httpRequest).setAttribute("className", className);
((HttpServletRequest)httpRequest).setAttribute("methodName", methodName);
((HttpServletRequest)httpRequest).setAttribute("params", params);
((HttpServletRequest)httpRequest).setAttribute("request_encoded", Boolean.TRUE);
}

if (className == null && methodName == null && (((HttpServletRequest)httpRequest).getContentType() == null || !((HttpServletRequest)httpRequest).getContentType().startsWith("multipart/form-data;"))) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buff = new byte[4096];
InputStream is = ((HttpServletRequest)httpRequest).getInputStream();

int readed;
while((readed = is.read(buff)) >= 0) {
baos.write(buff, 0, readed);
}

requestStr = baos.toString("UTF-8");
String[] requestParams = requestStr.split("\\&");
String encodeValue = null;
String[] decode = requestParams;
int var19 = requestParams.length;

for(int var20 = 0; var20 < var19; ++var20) {
String param = decode[var20];
int index = param.indexOf(61);
if (index != -1) {
String key = param.substring(0, index);
String value = param.substring(index + 1);
value = URLDecoder.decode(value, "UTF-8");
if (encodeValue == null && "encode".equals(key)) {
encodeValue = value;
}

((HttpServletRequest)httpRequest).setAttribute(key, value);
}
}

className = (String)((HttpServletRequest)httpRequest).getAttribute("className");
methodName = (String)((HttpServletRequest)httpRequest).getAttribute("methodName");
if (className == null && methodName == null && encodeValue != null) {
decode = RMICoder.decode(encodeValue);
className = decode[0];
methodName = decode[1];
String params = decode[2];
((HttpServletRequest)httpRequest).setAttribute("className", className);
((HttpServletRequest)httpRequest).setAttribute("methodName", methodName);
((HttpServletRequest)httpRequest).setAttribute("params", params);
((HttpServletRequest)httpRequest).setAttribute("request_encoded", Boolean.TRUE);
}

if (LOG.isTraceEnabled()) {
LOG.trace("Get parameter 'className' return null. Parse request.getInputStream() result:" + className + "." + methodName);
}
}
}

接下来我们再来回顾RMIServlet中对参数的获取(smartbi.util.RMIUtil#parseRMIInfo(javax.servlet.http.HttpServletRequest, boolean))

首先尝试通过request.getParameter尝试获取className\methodName\params参数,如果为空则通过自定义实现的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;
}
}
}

看完以后,聪明的同学已经能想到一个解析差异的问题

如果我们将请求方法设置为GET,在queryString中仅传入encode参数(白名单类方法),再将真实要执行的放在Multipart部分不就能绕过Filter的校验了么

将以上带入执行流程做个简单梳理:

—–Filter——

  1. 请求方法为GET、获取参数className(空)\methodName(空)\encode(非空)

  2. 由于className与methodName为空,通过RMICoder解码内容为其赋值

  3. 判断类与方法名在白名单中,Filter校验通过

    —–Servlet—-

  4. 通过request.getParameter未获取到类、方法以及参数

  5. 判断Header的Content-Type头存在multipart/form-data;

  6. 解析Body,获取到真实执行的类、方法以及参数,最终完成调用

因此V9系统下的分析就完成了,补个利用截图(V9)

8f6210bdf50374a6b2e3221f1fa19e5

当然其实在V11当中这个解析也有一定差异

在V11系统当中不能通过encode+multipart的组合姿势完成绕过

就当是留个小作业吧,V11当中到底有什么差异呢?又该如何构造绕过的Payload呢?利用截图(V11)

8349e0734eb66c79a033ae0b796858a