Jellyfin Server 12.2 - High - Remote - IDOR - GET /Items/Suggestions omit-userId가 숨겨진 라이브러리 Path와 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, provider ID, 파일시스템 Path, 호출자가 할당되지 않은 라이브러리의 이미지 메타데이터. 숨겨진 사용자에게만 부여된 폴더도 포함된다. 폴더 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, 리버스 셸, 비밀번호 탈취를 주장하는 것이 아니다. 이 덤프는 호출자가 할당되지 않은 라이브러리의 카탈로그(및 Path)이다. GUID로 스트리밍하는 후속 동작은 오래된 Jellyfin HTML5/DLNA 동작이며, 이 잔여물은 그 ID를 열거하는 부분이다.
v12.2는 12.0 GHSA 물결을 종결했다. 그래도 나는 그 태그에 머물며 RequestHelpers.GetUserId 옆의 모든 userId 쿼리를 읽었다. Items는 항상 이를 사용한다. Suggestions는 분기한다: 빈 userId -> user = null, GetUserId는 절대 호출되지 않음. 그것이 잔여물이다.
run.sh는 두 개의 영화 라이브러리(PrivateA는 witness 이름으로, 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를 봅니다. admin GUID를 전달해도 여전히 403이 발생합니다.
ItemsController가 이미 하는 것처럼 userId가 비어 있을 때 RequestHelpers.GetUserId를 호출합니다. 외부 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은 인스턴스 전체 카탈로그입니다.