Skip to content

# 越权读取万能密码 → 认证绕过接管任意账号 #137

Description

@hacker-wp

越权读取万能密码 → 认证绕过接管任意账号

概述

/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 身份登录,接管超管

根因

  1. 接口鉴权缺失(主因):ConfigController.queryByKey 未加 @SaCheckPermission,拦截器对普通用户不施加任何方法级约束。
  2. 敏感配置无隔离:通用配置查询接口可读取 super_password 等敏感键,返回时无脱敏。
  3. 万能密码后门设计脆弱:明文比对、命中即绕过密码校验与 2FA,且默认值 1024ok 为已知弱口令。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions