浅析SmartBi逻辑漏洞(3)
前言
这个系列终于到了第三篇,指条路,如果忘记了可以再看看之前写的文章
浅析Smartbi逻辑漏洞
浅析Smartbi逻辑漏洞(2)
之前我就曾在第二篇末尾提到过(没人继续深入看),仍然存在一个问题,今天这个问题终于得以修复
当然老规矩,这里仅分享逻辑漏洞部分补丁绕过思路,不提供完整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——
请求方法为GET、获取参数className(空)\methodName(空)\encode(非空)
由于className与methodName为空,通过RMICoder
解码内容为其赋值
判断类与方法名在白名单中,Filter校验通过
—–Servlet—-
通过request.getParameter
未获取到类、方法以及参数
判断Header的Content-Type头存在multipart/form-data;
解析Body,获取到真实执行的类、方法以及参数,最终完成调用
因此V9系统下的分析就完成了,补个利用截图(V9)
当然其实在V11当中这个解析也有一定差异
在V11系统当中不能通过encode+multipart的组合姿势完成绕过
就当是留个小作业吧,V11当中到底有什么差异呢?又该如何构造绕过的Payload呢?利用截图(V11)