← All write-ups
July 1, 2025MEDIUMFINDING MISSION

Un XML déguisé qui met le serveur à genoux (amplification d'entités)

Finding de mission (application web métier, client anonymisé). Découvert et exploité par 34zY / Hamza Terir ; outil de flood concurrent développé from scratch. Testé sur instance de staging.

Contexte

Application web métier exposant un endpoint d’import de fichiers XML (format d’échange sectoriel), accessible avec un simple compte utilisateur authentifié, sans privilège particulier.

Comment je l’ai trouvée

En cartographiant les endpoints via un proxy d’interception, j’ai repéré cet upload XML. Réflexe : tester les classiques du parsing XML. XXE d’abord (lecture de fichiers, SSRF), sans succès. J’ai basculé sur l’amplification d’entités (« Billion Laughs ») : des entités qui se référencent en cascade et explosent en mémoire à la résolution.

Particularité de la cible : le corps n’attendait pas du XML brut mais une représentation encapsulée (tableau d’octets dans du JSON), ce qui passait sous le radar des filtres n’inspectant que Content-Type: application/xml. Le backend décodait puis parsait quand même en XML.

<!DOCTYPE lol [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;">
]>
<root><field>&lol3;</field></root>

Envoyé en rafale (plusieurs threads concurrents), le parseur saturait : latence en hausse, erreurs 500 en série.

Reproduction et garde-fous

Le write-up public ne reproduit pas le flood. La vérification minimale utilise un fichier borné et une seule requête :

curl --fail-with-body --max-time 5 \
  -H 'Content-Type: application/json' \
  --data-binary @fixtures/bounded-xml-wrapper.json \
  'https://AUTHORIZED_HOST/api/import'

Le parseur doit d’abord bloquer les DTD et entités externes, puis appliquer des budgets de taille et de temps :

from defusedxml.ElementTree import fromstring

def parse_untrusted_xml(data: bytes):
    if len(data) > 1_048_576:
        raise ValueError("XML exceeds request budget")
    return fromstring(data)

Le suivi CPU/mémoire/latence permet de distinguer un rejet propre d’un worker qui meurt ou redémarre. Le rate-limit ne remplace pas la configuration sécurisée du parseur.

La faille

Le parseur XML résolvait les entités récursives sans limite de profondeur ni de taille (CWE-776 / CWE-400) : pas de borne d’expansion, pas de garde sur la consommation de ressources, pas de rate-limit. Un petit payload répété suffisait à faire grimper CPU et mémoire.

Impact

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H ≈ 6.5 (Medium).

Remédiation

CWE-776 (Recursive Entity References) · CWE-400 (Uncontrolled Resource Consumption).