
Jellyfin Server 12.2 - 高 - リモート - IDOR - GET /Items/Suggestions omit-userId が隠しライブラリのパスとIDをダンプ
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 を教えました(省略時はクレームにフォールバックし、外部の 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 | High: 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 推測、閉じたままの隣接サーフェス。
| 軸 | メソッド + パス | 付与されるもの | 根拠 |
|---|---|---|---|
| 偵察 | userId なしの GET /Items/Suggestions(任意で limit / mediaType / type) | スコープ外の再帰的アイテム一覧: Name、Id、Overview、プロバイダーID、ファイルシステムパス、呼び出し元が割り当てられていないライブラリの画像メタデータ。これには非表示ユーザーにのみ付与されたフォルダも含まれる。フォルダACL、視聴年齢制限、許可タグは適用されない。 | ラボ実証済み。 制限付き labuser(EnableAllFolders=false、EnabledFolders=PublicB のみ): GET /Items witness=0、Suggestions witness=1 |
| 偵察 | limit を省略した同じリクエスト | InternalItemsQuery.Limit が未設定のままとなり、リポジトリは一致する全件(アイテムテーブル全体、ランダム順)を返す | ソース(SuggestionsController.cs:70-95) |
| 偵察 | その一覧から得たGUIDを使った未認証の GET /Videos/{itemId}/stream | 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、リバースシェル、パスワード窃取を主張しているのではない。このダンプは、呼び出し元が割り当てられていないライブラリのカタログ(およびパス)である。GUIDによるストリーミングの後続は、JellyfinのHTML5/DLNAにおける長年の挙動であり、この残存物はそのIDを列挙するものである。
v12.2 は 12.0 の GHSA の波をクローズした。それでも私はそのタグに留まり、RequestHelpers.GetUserId の隣にあるすべての userId クエリを読んだ。Items は常にこれを使う。Suggestions は分岐する: userId が空 -> user = null となり、GetUserId を決して呼ばない。これが残存物である。
run.sh は2つの映画ライブラリ(witness 名を持つ PrivateA と PublicB)をシードし、ウィザードを完了し、EnableAllFolders=false かつ EnabledFolders=PublicB のみで labuser を作成し、その後 GET /Items と GET /Items/Suggestions を比較する。
すでに記録済みの誤った道: GET /Items をリークとみなすこと(スコープされている); Suggestions?userId=<admin> をリークとみなすこと(403); レガシーな /Users/{userId}/Suggestions ルートを穴とみなすこと(GUIDがあるため GetUserId を呼ぶ); 未認証ストリームを本パックのバグと主張すること(姉妹の残存物); リバースシェル。すべて見せかけである。
クライアントのバグは製品のバグのように見える。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 が必要。ランチャーは lab/ に chdir して run.sh を exec する。
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 を見ることができません。同じトークンは userId なしで /Items/Suggestions 上の witness を見ることができます。管理者 GUID を渡しても依然として 403 になります。
ItemsController が既に行っているように、userId が空の場合は RequestHelpers.GetUserId を呼び出します。外部の GUID に対しては 403 を維持します。
var requestUserId = RequestHelpers.GetUserId(User, userId);
user = _userManager.GetUserById(requestUserId);
userId.IsNullOrEmpty() -> user = null 分岐を削除します。パッチ適用済みビルドに対してループバッククライアントを再実行します。制限された labuser の Suggestions は /Items と一致しなければなりません(witness=0)。
それまでは、「/Items をスコープした」を「BaseItemDto を返すすべてのクエリをスコープした」と扱わないでください。空の userId と user = null はインスタンス全体のカタログです。