Référentiel de recherche complet sur CVE-2026-42945 avec analyse de débordement de tampon du tas, exploit RCE (heap spray + Feng Shui), scripts de détection et guide de correctifs pour la vulnérabilité du module de réécriture de NGINX.
Dépassement de tampon sur le tas dans NGINX ngx_http_rewrite_module
| Métrique | Valeur |
|---|---|
| CVSS v4.0 | 9.2 (Critique) |
| CVSS v3.1 | 8.1 (Élevée) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | 122 — Débordement de tampon basé sur le tas |
| Introduit | Juin 2008 — v0.6.27 |
| Découvert | Avril 2026 — Recherche DepthFirst |
| Corrigé | 13 mai 2026 — v1.30.1, v1.31.0 |
| CVE publié | 21 mai 2026 |
| Durée de vie | ~18 ans (non détecté) |
| Commit de correction | 524977e7c534e87e5b55739fa74601c9f1102686 |
Un attaquant distant non authentifié peut déclencher un dépassement de tampon sur le tas déterministe dans les processus worker NGINX en envoyant une requête HTTP spécialement conçue à un serveur avec un modèle de configuration spécifique rewrite + set/if/rewrite. Le dépassement corrompt les métadonnées du tas (pointeurs ngx_pool_cleanup_t), permettant l'exécution de code à distance (RCE) via des techniques de heap spray et de Feng Shui.
server { listen 19321;
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1;
}
}
**Key requirements:**
- Une directive `rewrite` dont le remplacement contient `?` (séparateur de chaîne de requête)
- Une directive `set`, `if` ou `rewrite` ultérieure qui fait référence à une **capture PCRE sans nom** (`$1`, `$2`, etc.)
- Le `?` dans le remplacement de rewrite déclenche `ngx_http_script_start_args_code` qui définit `e->is_args = 1`
### Ce qu'un attaquant peut accomplir
| Capacité | Description |
|-----------|-------------|
| **Déni de service** | Provoque des crashs déterministes des processus workers, entraînant des boucles de redémarrage (fonctionne indépendamment de l'ASLR) |
| **Exécution de code à distance** | Avec l'ASLR désactivé (ou contourné via une réécriture partielle), obtenir une RCE complète en tant qu'utilisateur nginx |
| **Exfiltration de données** | Via des primitives de lecture mémoire, extraire des données sensibles du tas du worker |
| **Persistance** | Installer des portes dérobées via l'exécution de code dans la mémoire du processus worker |
---
## 2. Analyse de la cause racine
### Le moteur de script à deux passes
Le module `ngx_http_rewrite_module` de NGINX utilise un **moteur de script à deux passes** dans `src/http/ngx_http_script.c` :
1. **Passage de longueur** (`ngx_http_script_run`) : parcourt tous les codes de script pour calculer la taille totale du tampon nécessaire. Écrit les longueurs dans `le.ip` et `le.pos`.
2. **Passage de copie** (`ngx_http_script_copy_len`/`_code`) : parcourt à nouveau, écrivant les octets réels dans le tampon pré-alloué à `e->ip` et `e->pos`.
Chaque code de script a deux gestionnaires : un pour chaque passage. Par exemple :
- `ngx_http_script_copy_len` → `ngx_http_script_copy_code`
- `ngx_http_script_start_args_len` → `ngx_http_script_start_args_code`
### Le drapeau `is_args`
Le drapeau `e->is_args` sur la **structure du moteur** (`ngx_http_script_engine_t`) contrôle la manière dont le passage de copie gère certains caractères :```c
typedef struct {
u_char *ip;
u_char *pos;
ngx_http_variable_value_t *sp;
ngx_str_t buf;
int flushed;
unsigned is_args:1; // <-- THE BUG
unsigned ncaptures:1;
ngx_uint_t captures_size;
// ...
} ngx_http_script_engine_t;
When e->is_args = 1, le code de copie pour les références de capture $N appelle ngx_escape_uri() avec NGX_ESCAPE_ARGS, qui développe :
+ → %2B (1 octet → 3 octets, +200%)% → %25 (1 octet → 3 octets, +200%)& → %26 (1 octet → 3 octets, +200%)Le flux d'exécution pour le modèle vulnérable :``` rewrite ^/api/(.*)$ /internal?migrated=true;
1. Pendant **l'évaluation de la réécriture**, le moteur rencontre `?` dans la chaîne de remplacement, ce qui déclenche `ngx_http_script_start_args_code`, définissant `e->is_args = 1`.
2. La réécriture modifie l'URI de la requête puis continue vers la directive suivante.
3. **`e->is_args` n'est JAMAIS EFFACÉ**.
Ensuite :```
set $original_endpoint $1;
le) est créé pour le passage de longueur : ```c
ngx_memzero(&le, sizeof(ngx_http_script_engine_t));
Cela met correctement le.is_args = 0, donc le passage de longueur retourne la longueur de capture brute, non échappée.
e, qui a toujours e->is_args = 1 depuis l'étape 1. Le passage de copie applique l'échappement URI, développant chaque caractère échappable de 1 octet à 3 octets dans un tampon qui a été dimensionné pour la longueur brute — dépassement de tas.Pass 1 (Length — sub-engine le): le.is_args = 0 capture $1 = "A+++++B" → length = 7
Buffer allocated: 7 bytes
Pass 2 (Copy — main engine e): e.is_args = 1 ← LEAKED from rewrite capture $1 = "A+++++B" ngx_escape_uri("A+++++B", NGX_ESCAPE_ARGS): A → A (1 byte) + → %2B (3 bytes) ← EXPANSION + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) + → %2B (3 bytes) B → B (1 byte) total written: 17 bytes buffer size: 7 bytes OVERFLOW: 10 bytes
Le ratio d'expansion est `7 + (n_escapable * 2)` où `n_escapable` est le nombre de `+`, `%` et `&` dans la capture.
---
## 3. Mécanismes d'exploitation
### Aperçu
| Étape | Technique | Description |
|------|-----------|-------------|
| 1 | Overflow | Envoi d'une URI conçue avec un remplissage de `+` pour déborder le tampon du tas |
| 2 | Heap Spray | POST de grandes données vers `/spray` pour remplir le tas avec des données contrôlées |
| 3 | Feng Shui | Disposer les allocations de sorte que la cible du débordement (`ngx_pool_cleanup_t`) soit adjacente |
| 4 | Corrupt Handler | Le débordement écrase `ngx_pool_cleanup_t.handler` avec l'adresse de `system()` |
| 5 | Trigger Cleanup | Attendre la destruction du pool → `system(cmd)` exécute la commande de l'attaquant |
| 6 | Reverse Shell | Chaîne vers une charge utile de shell inversé pour un accès interactif |
### Feng Shui inter-requêtes
**Le Feng Shui en une seule requête échoue** car le débordement corrompt les métadonnées du pool (`->d.next`, `->d.failed`) avant d'atteindre le pointeur `cleanup`. Lorsque le pool est détruit à la fin de la requête, les métadonnées corrompues provoquent un **crash avant que `system()` ne soit appelé**.