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.
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
Port Scan & Service Discovery
$ nmap -sV -sC -p- --min-rate 5000 -Pn TARGET| Port | Service | Version |
|---|---|---|
21/tcp | FTP | vsftpd 3.0.3 |
22/tcp | SSH | OpenSSH 7.6p1 (Ubuntu) |
80/tcp | HTTP | Apache 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.
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:
/ → 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$ 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":
$ curl -s http://TARGET/diningRoom/sapphire.html blue_jewel{e1d457e96cac640f863ec7bc475d48aa}
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
$ curl -s -X POST http://TARGET/tigerStatusRoom/gem.php \ -d "gem=blue_jewel{e1d457e96cac640f863ec7bc475d48aa}" Crest 1: S0pXRkVVS0pKQkxIVVdTWUpFM0VTUlk9 # base64-encoded
Crest 2 — Gallery Room
$ 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:
# 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:
# 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
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:
| Crest | Raw Value | Decoding Method | Fragment |
|---|---|---|---|
| 1 | S0pXRkVVS0pKQkxIVVdTWUpFM0VTUlk9 | base64 → base32 | RlRQIHVzZXI6IG |
| 2 | GVFWK5KHK5WTGTCILE4DKY3DNN4GQQRTM5AVCTKE | base32 → base58 | h1bnRlciwgRlRQIHBh |
| 3 | (base64 of space-separated binary bits) | base64 → binary → hex | c3M6IHlvdV9jYW50X2h |
| 4 | gSUERauVpvKzRpyPpuYz66JDmRTbJubaoArM6CAQsnVwte6zF9J4GGYyun3k5qM9ma4s | base58 → hex | pZGVfZm9yZXZlcg== |
import base64 # Concatenate all four decoded fragments, then base64 decode combined = "RlRQIHVzZXI6IGh1bnRlciwgRlRQIHBhc3M6IHlvdV9jYW50X2hpZGVfZm9yZXZlcg==" result = base64.b64decode(combined).decode() print(result)
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:
$ 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
# 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}
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.
# 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
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.
$ ssh umbrella_guest@TARGET # password: T_virus_rules umbrella_guest$ cat ~/.jailcell/chris.txt ... MO disk 2 key: albert ...
# 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"
$ 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
Attack Chain
emblem, lock_pick, blue_jewelshield_key → Armor Room / Attic → Crests 3 & 4doom.tar.gz → SSH user · Hidden Closet → SSH pass + MO_DISK1 Vigenere ciphertext.jailcell/chris.txt → MO Disk 2 key albert → decrypt MO_DISK1 → weasker passwordweasker in sudo group → sudo cat /root/root.txt → 3c5794a00dc56c35f2bf096571edf3bfFull Flag & Credential Reference
| Item | Value |
|---|---|
| Emblem | emblem{fec832623ea498e20bf4fe1821d58727} |
| Lock Pick | lock_pick{037b35e2ff90916a9abf99129c8e1837} |
| Music Sheet | music_sheet{362d72deaf65f5bdc63daece6a1f676e} |
| Blue Jewel | blue_jewel{e1d457e96cac640f863ec7bc475d48aa} |
| Gold Emblem | gold_emblem{58a8c41a9d08b8a4e38d02a4d7ff4843} |
| Shield Key | shield_key{48a7a9227cd7eb89f0a062590798cbac} |
| Helmet Key | helmet_key{458493193501d2b94bbab2e727f8db4b} |
| FTP User / Pass | hunter / you_cant_hide_forever |
| SSH User / Pass | umbrella_guest / T_virus_rules |
| Weasker Pass | stars_members_are_my_guinea_pig |
| Root Flag | 3c5794a00dc56c35f2bf096571edf3bf |
Room Questions
| Question | Answer |
|---|---|
| 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 |
Vulnerabilities & Weaknesses
| Finding | Location | Severity | Impact |
|---|---|---|---|
| Sudo-enabled user with derivable password | SSH / weasker | Critical | Full root compromise via sudo |
| GPG passphrase split across JPEG stego | FTP / JPEGs | High | GPG passphrase recovery → helmet_key |
| SSH credentials in plaintext web files | HTTP / medals | High | Direct SSH access as umbrella_guest |
| FTP credentials derivable from crest chain | HTTP / FTP | High | FTP lateral access |
| Secret room URLs leaked in Location headers | HTTP | Medium | Area unlock without brute-force |
| Vigenere cipher protecting account credentials | Web / MO Disk | Medium | Trivially reversible once key is found |
| Sensitive paths embedded in HTML comments / ROT13 | HTTP source | Low | Navigation to restricted areas |
Takeaways
<!-- href="/mansionmain/" --> hand attackers the sitemap. Internal paths must never appear client-side.Location headers are trivially observable with curl -v. Obscurity is not access control.exiftool and unzip. Credentials must never live inside media files.sudo with a derivable password is one command from root. Review /etc/sudoers and enforce least privilege strictly.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.
$ 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
Tools Used
| Tool | Purpose |
|---|---|
nmap | Port scan and service fingerprint |
curl | Web room enumeration and all form POSTs |
gobuster | Directory brute-force for hidden paths |
steghide | Extract hidden key from 001-key.jpg (empty password) |
exiftool | Read EXIF Comment field from 002-key.jpg |
unzip | Extract ZIP appended to 003-key.jpg → key-003.txt |
gpg | Decrypt helmet_key.txt.gpg with assembled passphrase |
john | Password analysis / hash cracking support |
python3 | base64 / base32 / base58 / Vigenere decode chain |
ssh | Initial access as umbrella_guest, pivot to weasker |
biohazard_pwn.sh | Full-chain automated exploit (Bash + Python3) |