abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-suggestions-idor
Jellyfin Server 12.2 - Jellyfin
类别: IDOR。可达性: 远程(任意已认证的非管理员用户)。
GET /Items/Suggestions 仅标注了 [Authorize]。当省略 userId 时,SuggestionsController.GetSuggestions 会将 user = null,且从不调用 RequestHelpers.GetUserId。InternalItemsQuery(null) 不会调用 SetUser。LibraryManager.GetItemsResult 会跳过 AddUserToQuery。new DtoOptions() 包含 ItemFields.Path。GetBaseItemDtos(..., user: null) 会跳过 IsVisible。
GHSA-9x85 教会了 GetUserId(省略时回退到 claims;外部 GUID 返回 403)。ItemsController.GetItems 始终使用它。Suggestions 却没有。旧版 GET /Users/{userId}/Suggestions 始终带有路由 GUID,因此该同级接口确实会经过 GetUserId。
受限账户可以导出其未被分配库的标题、ID、简介、提供方 ID 以及文件系统 Path。这些 GUID 可在未认证的 GET /Videos/{id}/stream 上播放。
| ID | 暂无 CVE |
| CWE | CWE-639 / CWE-862 |
| CVSS | 高危:6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| 产品 | Jellyfin Server |
| 受影响版本 | v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6),镜像 jellyfin/jellyfin:12.2 |
| 认证 | 已认证(任意非管理员,包括儿童 / 隐藏库账户) |
| 许可证 | GNU Affero GPL v3.0 |
| 实验环境 | 仅 127.0.0.1:18790 |
Jellyfin 12.2 leftovers 的第 1 个遗留问题。相同版本锁定,其他包:jellyfin-svg-gzip(GHSA-cf3c 遗留 gzip data:image/png)、jellyfin-playlist-itemids(GHSA-9x85 遗留 ItemIds + 未认证流)、jellyfin-ffmpeg-colonslash(GHSA-wwwm 遗留 .strm URL 引号)。本包是导出隐藏库的那个。
任意已认证的非管理员用户,包括儿童 / 隐藏库 / MaxParentalRating 账户。需要普通用户令牌。无需管理员,无需 API 密钥。他们不修改密码。他们不铸造管理员权限。他们清点未被分配的库,包括文件系统 Path,然后播放这些 GUID。
权限表(详尽)。与 Signal-Server Dropwizard admin 同等标准:方法 + 路径、该调用授予什么、侦察 vs 完整性 vs 可用性、实验验证 vs 推断、保持关闭的相邻攻击面。
| 轴 | 方法 + 路径 | 授予内容 | 来源 |
|---|---|---|---|
| 侦察 | GET /Items/Suggestions,不带 userId(可选 limit / mediaType / type) | 无作用域的递归条目列表:Name、Id、Overview、提供程序 ID、文件系统 Path、调用者未被分配的库的图像元数据,包括仅授予隐藏用户的文件夹。文件夹 ACL、家长分级和允许标签均未应用。 | 实验室验证。 受限的 labuser(EnableAllFolders=false,仅 EnabledFolders=PublicB):GET /Items witness=0;Suggestions witness=1 |
| 侦察 | 同上,省略 limit | InternalItemsQuery.Limit 保持未设置;存储库返回完整匹配集(整个条目表,随机顺序) | 源码(SuggestionsController.cs:70-95) |
| 侦察 | 未认证 GET /Videos/{itemId}/stream,使用该列表中的 GUID | 以 jellyfin UID 身份获取该文件的字节 | 源码(VideosController 没有类级 [Authorize];StreamingHelpers 的 GetItemById 不带用户)。流已在同类遗留项 jellyfin-playlist-itemids 中实验,不在本包内 |
| 完整性 | Suggestions 上无 | 无密码、策略、收藏或会话写入 | 已关闭 |
| 可用性 | Suggestions 上无 | 除普通查询开销外无 DoS | 已关闭 |
在此固定版本上保持关闭
| 轴 | 方法 + 路径 | 为何关闭 |
|---|---|---|
| 侦察 | GET /Items?userId= 和 GET /Items?parentId=<hiddenFolder> | 始终调用 GetUserId + IsVisible(ItemsController.cs:265, 342-350) |
| 侦察 | GET /Playlists/{id}/Items?userId= | GetUserId(PlaylistsController.cs:524)。GHSA-9x85 类,已修补 |
| 侦察 | GET /Items/Suggestions?userId=<other> | 外部 GUID 仍会命中 GetUserId,对非管理员返回 403。实验室验证(impersonate-403=yes) |
| 侦察 | 旧版 GET /Users/{userId}/Suggestions | 路由 GUID 是必需的,因此它确实调用 GetUserId |
| 完整性 | POST /Users/Password?userId= | AssertCanUpdateUser |
| 完整性 | POST /Users/{id}/Policy | RequiresElevation |
| 完整性 | 设备、会话、AddAdditionalUser | 仅提升权限或自身会话 |
| 侦察 | GET /Users / GET /Users/{id} | UserDto + 策略(文件夹 GUID、管理员位),而非库内容或哈希。家庭枚举,不是此遗留项 |
我并不声称存在插件 DLL RCE、反向 shell 或密码窃取。该转储是调用者未被分配的库的目录(以及 Path)。按 GUID 流式传输的后续行为是 Jellyfin HTML5/DLNA 的长期行为;此遗留项是枚举这些 ID 的部分。
v12.2 关闭了 12.0 GHSA 浪潮。我仍然停留在那个标签上,阅读了每个 userId 查询旁边的 RequestHelpers.GetUserId。Items 始终使用它。Suggestions 分支:空 userId -> user = null,从不调用 GetUserId。这就是遗留项。
run.sh 播种两个电影库(PrivateA 以 witness 命名,PublicB),完成向导,创建 labuser,设置 EnableAllFolders=false 且仅 EnabledFolders=PublicB,然后比较 GET /Items 与 GET /Items/Suggestions。
已记录的弯路:将 GET /Items 视为泄漏(它是有作用域的);将 Suggestions?userId=<admin> 视为泄漏(403);将旧版 /Users/{userId}/Suggestions 路由视为漏洞(它有 GUID,因此会调用 GetUserId);声称未认证流是本包的 bug(同类遗留项);反向 shell。都是表演。
客户端 bug 看起来像产品 bug。一个遮蔽 http.client 的辅助程序从不发送数据包。对 HTTPResponse 调用 .recv() 不是 .read()。我提一次是因为它耗费了时间,并且有一分钟看起来像 12.2 已经完成了工作。
python3 jellyfin-suggestions-idor-Abraxas-Labs.py
或
cd lab
./run.sh
仅针对 http://127.0.0.1:18790。镜像 jellyfin/jellyfin:12.2。需要 Docker。启动器会 chdir 进入 lab/ 并执行 run.sh。
IOC user-items http=200 witness=0
IOC user-suggestions http=200 witness=1
IOC impersonate-suggestions http=403
SUCCESS jellyfin-suggestions-idor items-hidden=yes suggestions-has-witness=yes impersonate-403=yes JELLYFIN-SUGGESTIONS-IDOR-WITNESS
受限的 labuser 无法在 /Items 上看到 PrivateA。同一个令牌在 /Items/Suggestions 上能看到见证内容,且没有 userId。传入管理员 GUID 仍然返回 403。
当 userId 为空时调用 RequestHelpers.GetUserId,就像 ItemsController 已经做的那样。对外部 GUID 保持 403。
var requestUserId = RequestHelpers.GetUserId(User, userId);
user = _userManager.GetUserById(requestUserId);
删除 userId.IsNullOrEmpty() -> user = null 分支。针对已修补的构建重新运行 loopback 客户端:受限 labuser 的 Suggestions 必须与 /Items 一致(witness=0)。
在此之前,不要把“我对 /Items 做了作用域限制”当作“我对所有返回 BaseItemDto 的查询都做了作用域限制”。空的 userId 加上 user = null 意味着实例级目录。