CTF Writeup

Biohazard

TryHackMe · Web Enumeration / Steganography / Encoding Chains · Medium · by 0xb0rn3

Platform TryHackMe Theme Resident Evil (RE1) — Spencer Mansion Difficulty Medium Services FTP (21) / SSH (22) / HTTP (80) Stack Web Enum + Stego + GPG + Vigenere + PrivEsc Flags 7 collectibles + root flag
0
Context

Overview

Biohazard is a Resident Evil-themed CTF that follows Jill Valentine through the Spencer Mansion. Players must explore a series of interconnected web rooms, collect items/keys, decode ciphers, extract FTP files, and ultimately gain SSH access then escalate to root. The challenge combines web enumeration, steganography, encoding/decoding chains, and Linux privilege escalation.

Every collectible item found in the mansion is required to unlock the next area — skip one and the chain breaks. The full path from HTTP root to root shell requires 6 sequential phases.

ATTACK CHAIN
Phase 1  Web Rooms    → emblem, lock_pick, blue_jewel (HTML source mining)
Phase 2  Item Collect → Crests 1-4 (form POSTs, ROT13, base32, bar room chain)
Phase 3  Decode Chain → 4x multi-encoding → FTP user:pass
Phase 4  FTP + Stego  → 3 JPEG key parts (steghide / EXIF / ZIP) → GPG pass
Phase 5  GPG Decrypt  → helmet_key → unlock study room + hidden closet
Phase 6  SSH Access   → umbrella_guest → MO Disk 2 key → Vigenere → weasker → sudo root
1
Reconnaissance

Port Scan & Service Discovery

BASH
$ nmap -sV -sC -p- --min-rate 5000 -Pn TARGET
PortServiceVersion
21/tcpFTPvsftpd 3.0.3
22/tcpSSHOpenSSH 7.6p1 (Ubuntu)
80/tcpHTTPApache 2.4.29 (Ubuntu)

The attack surface is minimal — all the complexity lives inside the web application. The HTTP server simulates exploring the Spencer Mansion room by room.

2
Web Enumeration

Phase 1 — Mansion Room Crawl

The web root contains an HTML comment pointing to /mansionmain/. Each room page reveals the next via comments, redirects, or hints buried in the page source. The full navigation path:

NAVIGATION CHAIN
/             → source comment: <!-- href="/mansionmain/" -->
/mansionmain/ → hint to /diningRoom/
/diningRoom/  → emblem.php gives emblem flag
              → base64 comment → /teaRoom/
/teaRoom/     → master_of_unlock.html → lock_pick flag
              → hint to /artRoom/
/artRoom/     → MansionMap.html → full room list
BASH
$ curl -s http://TARGET/ | grep -i href
<!-- href="/mansionmain/" -->

$ curl -s http://TARGET/diningRoom/emblem.php
emblem{fec832623ea498e20bf4fe1821d58727}

$ curl -s http://TARGET/diningRoom/ | base64 -d  # base64 comment → /teaRoom/

$ curl -s http://TARGET/teaRoom/master_of_unlock.html
lock_pick{037b35e2ff90916a9abf99129c8e1837}

$ curl -s http://TARGET/artRoom/MansionMap.html  # reveals all remaining room URLs

/diningRoom2F/ contains a ROT13-encoded comment. Decoded: "Visit sapphire.html":

BASH
$ curl -s http://TARGET/diningRoom/sapphire.html
blue_jewel{e1d457e96cac640f863ec7bc475d48aa}
Emblem
emblem{fec832623ea498e20bf4fe1821d58727}
Lock Pick
lock_pick{037b35e2ff90916a9abf99129c8e1837}
Blue Jewel
blue_jewel{e1d457e96cac640f863ec7bc475d48aa}
3
Item Collection

Phase 2 — Four Crests

Each crest is hidden behind a form POST or encoded in a room's content. All four are required to derive the FTP credentials.

Crest 1 — Tiger Status Room

BASH
$ curl -s -X POST http://TARGET/tigerStatusRoom/gem.php \
    -d "gem=blue_jewel{e1d457e96cac640f863ec7bc475d48aa}"
Crest 1: S0pXRkVVS0pKQkxIVVdTWUpFM0VTUlk9  # base64-encoded

Crest 2 — Gallery Room

BASH
$ curl -s http://TARGET/galleryRoom/note.txt
Crest 2: GVFWK5KHK5WTGTCILE4DKY3DNN4GQQRTM5AVCTKE  # base32-encoded

Crests 3 & 4 — Bar Room Chain → Shield Key → Armor Room / Attic

The bar room requires the lock_pick to unlock. The secret URL is returned as a Location redirect header. Inside, a base32-encoded music note decodes to the music_sheet — POSTed to piano.php, which redirects to barRoomHidden.php containing the gold_emblem. Posting that to /diningRoom/emblem_slot.php returns a Vigenere ciphertext (key: rebecca) revealing the shield key page name:

BASH
# 1. Unlock bar room with lock_pick
$ curl -sv -X POST http://TARGET/barRoom/unlock_door.php \
    -d "door_flag=lock_pick{037b35e2ff90916a9abf99129c8e1837}" 2>&1 | grep Location
Location: ../barRoom357162e3db904857963e6e0b64b96ba7/

# 2. Decode music note (base32)
$ curl -s http://TARGET/barRoom357162e3db904857963e6e0b64b96ba7/musicNote.html | \
    python3 -c "import sys,base64; print(base64.b32decode(sys.stdin.read().strip()).decode())"
music_sheet{362d72deaf65f5bdc63daece6a1f676e}

# 3. POST music_sheet to piano.php → redirect → barRoomHidden.php
$ curl -s http://TARGET/barRoom357162e3db904857963e6e0b64b96ba7/gold_emblem.php
gold_emblem{58a8c41a9d08b8a4e38d02a4d7ff4843}

# 4. POST gold_emblem → Vigenere ciphertext (key: rebecca)
# → "there is a shield key inside the dining room.
#    the html page is called the_great_shield_key"
$ curl -s http://TARGET/diningRoom/the_great_shield_key.html
shield_key{48a7a9227cd7eb89f0a062590798cbac}

With the shield_key, unlock the Armor Room and Attic. Each note yields a crest with a different encoding depth:

BASH
# Armor Room → Crest 3 (base64 → binary → hex, encoded 3x)
$ curl -sv -X POST http://TARGET/armorRoom/unlock_door.php \
    -d "door_flag=shield_key{48a7a9227cd7eb89f0a062590798cbac}" 2>&1 | grep Location
$ curl -s http://TARGET/armorRoom.../note.txt

# Attic → Crest 4 (base58 → hex, encoded 2x)
$ curl -sv -X POST http://TARGET/attic/unlock_door.php \
    -d "door_flag=shield_key{48a7a9227cd7eb89f0a062590798cbac}" 2>&1 | grep Location
$ curl -s http://TARGET/attic.../note.txt
Crest 4: gSUERauVpvKzRpyPpuYz66JDmRTbJubaoArM6CAQsnVwte6zF9J4GGYyun3k5qM9ma4s
Music Sheet
music_sheet{362d72deaf65f5bdc63daece6a1f676e}
Gold Emblem
gold_emblem{58a8c41a9d08b8a4e38d02a4d7ff4843}
Shield Key
shield_key{48a7a9227cd7eb89f0a062590798cbac}
4
Crypto

Phase 3 — Crest Decode Chain → FTP Credentials

The four crests are fragments of a base64-encoded string containing FTP credentials. Each fragment uses a different encoding chain and must be decoded independently before being concatenated:

CrestRaw ValueDecoding MethodFragment
1S0pXRkVVS0pKQkxIVVdTWUpFM0VTUlk9base64 → base32RlRQIHVzZXI6IG
2GVFWK5KHK5WTGTCILE4DKY3DNN4GQQRTM5AVCTKEbase32 → base58h1bnRlciwgRlRQIHBh
3(base64 of space-separated binary bits)base64 → binary → hexc3M6IHlvdV9jYW50X2h
4gSUERauVpvKzRpyPpuYz66JDmRTbJubaoArM6CAQsnVwte6zF9J4GGYyun3k5qM9ma4sbase58 → hexpZGVfZm9yZXZlcg==
PYTHON3
import base64

# Concatenate all four decoded fragments, then base64 decode
combined = "RlRQIHVzZXI6IGh1bnRlciwgRlRQIHBhc3M6IHlvdV9jYW50X2hpZGVfZm9yZXZlcg=="
result = base64.b64decode(combined).decode()
print(result)
FTP user: hunter, FTP pass: you_cant_hide_forever
 FTP Credentials Recovered
hunter : you_cant_hide_forever
5
Steganography

Phase 4 — FTP Files & Three-Part Key Assembly

FTP as hunter exposes five files: three JPEG images, a GPG-encrypted file, and important.txt — Barry's note confirming the helmet key is inside the GPG file and that a /hidden_closet/ path exists. Each JPEG conceals a key fragment via a different steganography method:

BASH
$ curl -s --user "hunter:you_cant_hide_forever" \
    "ftp://TARGET/001-key.jpg" -o 001-key.jpg
$ curl -s --user "hunter:you_cant_hide_forever" \
    "ftp://TARGET/002-key.jpg" -o 002-key.jpg
$ curl -s --user "hunter:you_cant_hide_forever" \
    "ftp://TARGET/003-key.jpg" -o 003-key.jpg
$ curl -s --user "hunter:you_cant_hide_forever" \
    "ftp://TARGET/helmet_key.txt.gpg" -o helmet_key.txt.gpg

# 001-key.jpg → steghide (empty password)
$ steghide extract -sf 001-key.jpg -p ""
key1: cGxhbnQ0Ml9jYW

# 002-key.jpg → EXIF Comment field
$ exiftool 002-key.jpg | grep Comment
Comment: 5fYmVfZGVzdHJveV9

# 003-key.jpg → ZIP file appended to JPEG → key-003.txt
$ cp 003-key.jpg 003-key.zip && unzip 003-key.zip
key3: 3aXRoX3Zqb2x0

# Assemble key1 + key2 + key3, then base64 decode
$ python3 -c "
import base64
print(base64.b64decode('cGxhbnQ0Ml9jYW5fYmVfZGVzdHJveV93aXRoX3Zqb2x0').decode())
"
plant42_can_be_destroy_with_vjolt
BASH
# Decrypt GPG with assembled passphrase
$ echo "plant42_can_be_destroy_with_vjolt" | \
    gpg --batch --passphrase-fd 0 -d helmet_key.txt.gpg
helmet_key{458493193501d2b94bbab2e727f8db4b}
 Helmet Key
helmet_key{458493193501d2b94bbab2e727f8db4b}
6
Unlock

Phase 5 — Study Room & Hidden Closet

The helmet_key unlocks two secret rooms. Their hashed URLs are returned as Location redirect headers from the respective unlock_door.php endpoints.

BASH
# Study Room → doom.tar.gz → eagle_medal.txt → SSH user
$ curl -sv -X POST http://TARGET/studyRoom/unlock_door.php \
    -d "door_flag=helmet_key{458493193501d2b94bbab2e727f8db4b}" 2>&1 | grep Location
Location: ../studyRoom28341c5e98c93b89258a6389fd608a3c/

$ curl -s http://TARGET/studyRoom28341c5e98c93b89258a6389fd608a3c/doom.tar.gz | tar -xz
eagle_medal.txt → SSH user: umbrella_guest

# Hidden Closet → wolf_medal.txt → SSH pass + MO_DISK1 cipher
$ curl -sv -X POST http://TARGET/hidden_closet/unlock_door.php \
    -d "door_flag=helmet_key{458493193501d2b94bbab2e727f8db4b}" 2>&1 | grep Location
Location: ../hiddenCloset8997e740cb7f5cece994381b9477ec38/

$ curl -s http://TARGET/hiddenCloset8997e740cb7f5cece994381b9477ec38/wolf_medal.txt
SSH password: T_virus_rules

$ curl -s http://TARGET/hiddenCloset8997e740cb7f5cece994381b9477ec38/MO_DISK1.txt
wpbwbxr wpkzg pltwnhro, txrks_xfqsxrd_bvv_fy_rvmexa_ajk
# ↑ Vigenere cipher — key found in next phase
7
Initial Access → Privilege Escalation

Phase 6 — SSH, Vigenere × 2, Root

SSH in as umbrella_guest. The file at /home/umbrella_guest/.jailcell/chris.txt contains story text and the MO Disk 2 key: albert. This key decrypts MO Disk 1 via Vigenere, yielding Weasker's password. Weasker is in the sudo group — one command from root.

BASH
$ ssh umbrella_guest@TARGET  # password: T_virus_rules

umbrella_guest$ cat ~/.jailcell/chris.txt
... MO disk 2 key: albert ...
PYTHON3
# Vigenere decrypt MO_DISK1 (key = "albert")
cipher = "wpbwbxr wpkzg pltwnhro, txrks_xfqsxrd_bvv_fy_rvmexa_ajk"
key    = "albert"
# → "weasker login password, stars_members_are_my_guinea_pig"
BASH
$ ssh weasker@TARGET  # password: stars_members_are_my_guinea_pig

weasker$ id
uid=1002(weasker) gid=1002(weasker) groups=1002(weasker),27(sudo)

weasker$ echo stars_members_are_my_guinea_pig | sudo -S cat /root/root.txt
3c5794a00dc56c35f2bf096571edf3bf
 Root Flag
3c5794a00dc56c35f2bf096571edf3bf
Visualization

Attack Chain

1
Mansion Room Crawl
HTML source mining, ROT13, base64 comments → emblem, lock_pick, blue_jewel
2
Form POST Item Collection
Tiger Room → Crest 1 · Gallery → Crest 2 · Bar Room chain (base32 + Vigenere/rebecca) → shield_key → Armor Room / Attic → Crests 3 & 4
3
Four-Crest Decode Chain
b64→b32 / b32→b58 / b64→binary→hex / b58→hex → concatenate → base64 decode → hunter:you_cant_hide_forever
4
FTP + Three-Method Steganography
steghide (001) + EXIF Comment (002) + ZIP-in-JPEG (003) → assemble + b64 decode → GPG pass → helmet_key{458493...}
5
Secret Room Unlock
Study Room → doom.tar.gz → SSH user · Hidden Closet → SSH pass + MO_DISK1 Vigenere ciphertext
6
SSH + Double Vigenere Pivot
.jailcell/chris.txt → MO Disk 2 key albert → decrypt MO_DISK1 → weasker password
sudo Root — Game Over
weasker in sudo group → sudo cat /root/root.txt3c5794a00dc56c35f2bf096571edf3bf
Reference

Full Flag & Credential Reference

ItemValue
Emblememblem{fec832623ea498e20bf4fe1821d58727}
Lock Picklock_pick{037b35e2ff90916a9abf99129c8e1837}
Music Sheetmusic_sheet{362d72deaf65f5bdc63daece6a1f676e}
Blue Jewelblue_jewel{e1d457e96cac640f863ec7bc475d48aa}
Gold Emblemgold_emblem{58a8c41a9d08b8a4e38d02a4d7ff4843}
Shield Keyshield_key{48a7a9227cd7eb89f0a062590798cbac}
Helmet Keyhelmet_key{458493193501d2b94bbab2e727f8db4b}
FTP User / Passhunter / you_cant_hide_forever
SSH User / Passumbrella_guest / T_virus_rules
Weasker Passstars_members_are_my_guinea_pig
Root Flag3c5794a00dc56c35f2bf096571edf3bf
TryHackMe

Room Questions

QuestionAnswer
How many open ports?3
What is the team name in Operation Biohazard?STARS
Where was Chris found?jailcell
What is the name of the ultimate form?Tyrant
Who is the STARS Bravo team leader?Enrico Marini
Assessment

Vulnerabilities & Weaknesses

Navigation to restricted areas
FindingLocationSeverityImpact
Sudo-enabled user with derivable passwordSSH / weaskerCriticalFull root compromise via sudo
GPG passphrase split across JPEG stegoFTP / JPEGsHighGPG passphrase recovery → helmet_key
SSH credentials in plaintext web filesHTTP / medalsHighDirect SSH access as umbrella_guest
FTP credentials derivable from crest chainHTTP / FTPHighFTP lateral access
Secret room URLs leaked in Location headersHTTPMediumArea unlock without brute-force
Vigenere cipher protecting account credentialsWeb / MO DiskMediumTrivially reversible once key is found
Sensitive paths embedded in HTML comments / ROT13HTTP sourceLow
Defense

Takeaways

Never embed paths in HTML comments
Comments like <!-- href="/mansionmain/" --> hand attackers the sitemap. Internal paths must never appear client-side.
Redirect headers reveal secret URLs
Secret room URLs in Location headers are trivially observable with curl -v. Obscurity is not access control.
Encoding ≠ encryption
Vigenere, ROT13, and base64 are trivially reversible. Once the key appears anywhere in the system, all ciphertexts are broken. Use AES-256-GCM for secrets.
Steganography is not security
EXIF fields and ZIP-appended JPEGs are recovered in seconds with exiftool and unzip. Credentials must never live inside media files.
Audit sudo group membership
Any user in sudo with a derivable password is one command from root. Review /etc/sudoers and enforce least privilege strictly.
Sequential credential chains
Each item unlocked the next phase — a reminder that credential reuse and chained dependencies across services create exploitable pivot paths in real environments.
Automation

Full-Chain Exploit Script

All phases are automated in biohazard_pwn.sh — a Bash script that chains web item collection, crest decoding (Python3), FTP download, steghide / exiftool / unzip extraction, GPG decryption, SSH pivoting, and Vigenere decode to dump all flags in a single run.

BASH
$ chmod +x biohazard_pwn.sh
$ ./biohazard_pwn.sh TARGET_IP

[*] Biohazard CTF Exploit — Target: TARGET_IP
[+] emblem:      emblem{fec832623ea498e20bf4fe1821d58727}
[+] lock_pick:   lock_pick{037b35e2ff90916a9abf99129c8e1837}
[+] blue_jewel:  blue_jewel{e1d457e96cac640f863ec7bc475d48aa}
[+] gold_emblem: gold_emblem{58a8c41a9d08b8a4e38d02a4d7ff4843}
[+] shield_key:  shield_key{48a7a9227cd7eb89f0a062590798cbac}
[+] helmet_key:  helmet_key{458493193501d2b94bbab2e727f8db4b}
[+] FTP:         hunter / you_cant_hide_forever
[+] SSH:         umbrella_guest / T_virus_rules
[+] Weasker:     weasker / stars_members_are_my_guinea_pig
[+] ROOT FLAG:   3c5794a00dc56c35f2bf096571edf3bf
Arsenal

Tools Used

ToolPurpose
nmapPort scan and service fingerprint
curlWeb room enumeration and all form POSTs
gobusterDirectory brute-force for hidden paths
steghideExtract hidden key from 001-key.jpg (empty password)
exiftoolRead EXIF Comment field from 002-key.jpg
unzipExtract ZIP appended to 003-key.jpg → key-003.txt
gpgDecrypt helmet_key.txt.gpg with assembled passphrase
johnPassword analysis / hash cracking support
python3base64 / base32 / base58 / Vigenere decode chain
sshInitial access as umbrella_guest, pivot to weasker
biohazard_pwn.shFull-chain automated exploit (Bash + Python3)