越权读取万能密码 → 认证绕过接管任意账号
概述
/support/config/queryByKey 接口缺失权限校验,任何已登录的低权限用户都能读取 super_password(万能密码)明文;再将该值用于登录接口,即可命中系统内置的“万能密码”后门,跳过密码哈希校验与二次验证,以任意账号(含超级管理员)身份登录。
- 严重级别:P1(高)
- 类型:越权读取敏感配置 + 认证绕过(组合链)
- 影响版本:
smart-admin-api-java17-springboot3、smart-admin-api-java8-springboot2
攻击链
第 1 步:普通用户正常登录,拿到有效 token
/login 为公开接口,任意普通员工(如 huke)正常登录即可获取 token。
// sa-admin/.../module/system/login/controller/LoginController.java
@NoNeedLogin
@PostMapping("/login")
public ResponseDTO<LoginResultVO> login(...) { ... }
第 2 步:调用无鉴权的配置查询接口,读出万能密码
// sa-base/.../module/support/config/ConfigController.java
@Operation(summary = "查询配置详情 @author 卓大")
@GetMapping("/config/queryByKey") // 无 @SaCheckPermission,也无 @NoNeedLogin
public ResponseDTO<ConfigVO> queryByKey(@RequestParam String configKey) {
return ResponseDTO.ok(configService.getConfig(configKey)); // configKey 用户可控,原样透传
}
ConfigService.getConfig 从缓存取实体后直接拷贝返回,对 configValue 没有任何脱敏:
// sa-base/.../module/support/config/ConfigService.java
public ConfigVO getConfig(String configKey) {
if (StrUtil.isBlank(configKey)) {
return null;
}
ConfigEntity entity = this.CONFIG_CACHE.get(configKey.toLowerCase());
return SmartBeanUtil.copy(entity, ConfigVO.class); // 含 configValue,明文返回
}
configKey 的合法取值定义在同名枚举中,其中就包含万能密码:
// sa-base/.../module/support/config/ConfigKeyEnum.java
public enum ConfigKeyEnum implements BaseEnum {
SUPER_PASSWORD("super_password", "万能密码"), // 攻击目标键
LEVEL3_PROTECT_CONFIG("level3_protect_config", "三级等保配置"),
;
}
为何接口“已登录即可访问”:AdminInterceptor 对 /** 全量拦截,只校验登录态;对非超管用户,权限完全依赖方法上的注解,而 queryByKey 上没有任何权限注解,等于无约束放行:
// sa-admin/.../admin/interceptor/AdminInterceptor.java
public boolean preHandle(...) {
...
RequestEmployee requestEmployee = loginService.getLoginEmployee(loginId, request);
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) { ... return true; }
if (requestEmployee == null) { // 仅校验“是否登录”
SmartResponseUtil.write(response, ResponseDTO.error(UserErrorCode.LOGIN_STATE_INVALID));
return false;
}
...
// 超管直接放行
if (requestEmployee.getAdministratorFlag()) {
return true;
}
// 普通用户:仅校验方法上的权限注解
SaAnnotationStrategy.instance.checkMethodAnnotation.accept(method); // 方法无注解 = 不拦截
...
}
对照同业务域的管理接口,/config/query、/config/add、/config/update 都带有 @SaCheckPermission,唯独 queryByKey 漏加,属明确疏漏:
// sa-admin/.../module/system/support/AdminConfigController.java
@PostMapping("/config/query")
@SaCheckPermission("support:config:query")
public ResponseDTO<PageResult<ConfigVO>> queryConfigPage(...) { ... }
@PostMapping("/config/add")
@SaCheckPermission("support:config:add")
public ResponseDTO<String> addConfig(...) { ... }
@PostMapping("/config/update")
@SaCheckPermission("support:config:update")
public ResponseDTO<String> updateConfig(...) { ... }
请求示例(携带第 1 步的普通用户 token):
GET /support/config/queryByKey?configKey=super_password
Cookie: sa-token=<普通用户token>
→ {"ok":true,"data":{"configKey":"super_password","configValue":"1024ok", ...}}
第 3 步:用万能密码登录,命中后门,接管超管
LoginService.login 中,用户提交的密码经与 super_password 明文直接比较,命中即标记 superPasswordFlag,跳过后续哈希校验:
// sa-admin/.../module/system/login/service/LoginService.java
// 解密前端加密的密码
String requestPassword = apiEncryptService.decrypt(loginForm.getPassword());
// 验证密码 是否为万能密码
String superPassword = configService.getConfigValue(ConfigKeyEnum.SUPER_PASSWORD);
boolean superPasswordFlag = superPassword.equals(requestPassword); // 明文比对
// 校验双因子登录
ResponseDTO<String> validateEmailCode = validateEmailCode(loginForm, employeeEntity, superPasswordFlag);
if (!validateEmailCode.getOk()) {
return ResponseDTO.error(validateEmailCode);
}
// 万能密码特殊操作
if (superPasswordFlag) {
// 直接登录,跳过密码哈希校验,跳过登录失败次数限制
String saTokenLoginId = SUPER_PASSWORD_LOGIN_ID_PREFIX + ... + employeeEntity.getEmployeeId();
StpUtil.login(saTokenLoginId, 1800);
} else {
// 普通用户才走密码哈希校验 + 等保登录失败校验
...
if (!SecurityPasswordService.matchesPwd(...)) { ... }
}
同时 validateEmailCode 首行对万能密码直接放行,跳过邮箱二次验证(2FA):
// sa-admin/.../module/system/login/service/LoginService.java
private ResponseDTO<String> validateEmailCode(LoginForm loginForm, EmployeeEntity employeeEntity, boolean superPasswordFlag) {
// 万能密码则不校验
if (superPasswordFlag) {
return ResponseDTO.ok();
}
// 未开启双因子登录
if (!level3ProtectConfigService.isTwoFactorLoginEnabled()) {
return ResponseDTO.ok();
}
... // 校验邮箱验证码
}
于是用万能密码 1024ok + 任意已知登录名(如超管 admin)即可在不掌握其真实密码的情况下完成登录,接管超级管理员。
攻击链总览
① 普通员工账号正常登录 ────────────────► 获得有效 token
② GET /support/config/queryByKey?configKey=super_password
· queryByKey 无 @SaCheckPermission
· AdminInterceptor 仅校验登录态,无注解=放行
· getConfig 原样返回 configValue
────────────────────────────────────► 读出万能密码 "1024ok"
③ POST /login { loginName:"admin", password:<1024ok> }
· superPassword.equals(requestPassword) 命中
· 跳过密码哈希校验、跳过登录失败锁定
· validateEmailCode 直接返回 OK,跳过 2FA
────────────────────────────────────► 以 admin 身份登录,接管超管
根因
- 接口鉴权缺失(主因):
ConfigController.queryByKey 未加 @SaCheckPermission,拦截器对普通用户不施加任何方法级约束。
- 敏感配置无隔离:通用配置查询接口可读取
super_password 等敏感键,返回时无脱敏。
- 万能密码后门设计脆弱:明文比对、命中即绕过密码校验与 2FA,且默认值
1024ok 为已知弱口令。
越权读取万能密码 → 认证绕过接管任意账号
概述
/support/config/queryByKey接口缺失权限校验,任何已登录的低权限用户都能读取super_password(万能密码)明文;再将该值用于登录接口,即可命中系统内置的“万能密码”后门,跳过密码哈希校验与二次验证,以任意账号(含超级管理员)身份登录。smart-admin-api-java17-springboot3、smart-admin-api-java8-springboot2攻击链
第 1 步:普通用户正常登录,拿到有效 token
/login为公开接口,任意普通员工(如huke)正常登录即可获取 token。第 2 步:调用无鉴权的配置查询接口,读出万能密码
ConfigService.getConfig从缓存取实体后直接拷贝返回,对 configValue 没有任何脱敏:configKey的合法取值定义在同名枚举中,其中就包含万能密码:为何接口“已登录即可访问”:
AdminInterceptor对/**全量拦截,只校验登录态;对非超管用户,权限完全依赖方法上的注解,而queryByKey上没有任何权限注解,等于无约束放行:对照同业务域的管理接口,
/config/query、/config/add、/config/update都带有@SaCheckPermission,唯独queryByKey漏加,属明确疏漏:请求示例(携带第 1 步的普通用户 token):
第 3 步:用万能密码登录,命中后门,接管超管
LoginService.login中,用户提交的密码经与super_password明文直接比较,命中即标记superPasswordFlag,跳过后续哈希校验:同时
validateEmailCode首行对万能密码直接放行,跳过邮箱二次验证(2FA):于是用万能密码
1024ok+ 任意已知登录名(如超管admin)即可在不掌握其真实密码的情况下完成登录,接管超级管理员。攻击链总览
根因
ConfigController.queryByKey未加@SaCheckPermission,拦截器对普通用户不施加任何方法级约束。super_password等敏感键,返回时无脱敏。1024ok为已知弱口令。