
SolarWinds Serv-U CVE-2026-28318 : crash non authentifié de Content-Encoding: deflate. Analyse de la cause racine (invalid free of an interior pointer -> heap corruption) + DoS-only PoC. Corrigé dans la version 15.5.4 Hotfix 1.
Analyse de la cause racine + preuve de concept DoS. L'avis public classe cela comme un déni de service / consommation de ressources non contrôlée sans authentification. L'analyse binaire montre que le défaut sous-jacent est memory-safety : un
free()invalide d'un pointeur intérieur dans le chemin de décodagedeflateHTTP, qui corrompt le tas du processus (STATUS_HEAP_CORRUPTION,0xC0000374). Il ne s'agit pas d'une bombe de décompression, et le crash est indépendant de la taille décompressée.
N'affecte que l'écouteur HTTP/HTTPS (le chemin web/administration). Les protocoles FTP/FTPS/SFTP ne sont pas sur ce chemin de code.
Toute requête HTTP portant Content-Encoding: deflate avec un corps deflate valide fait crasher le service. Le gestionnaire décompresse le corps, puis libère le pointeur vers le corps compressé et échange avec le tampon décompressé — mais ce pointeur est un pointeur intérieur dans le tampon de réception HTTP (il pointe vers le corps, juste après le \r\n\r\n), pas une base d'allocation du tas. Appeler free() sur un pointeur non-base et non aligné corrompt le tas et le processus meurt.
Parce que le bogue réside dans la façon dont le pointeur du corps est libéré — et non dans la quantité de données produites — même un corps compressé d'environ 25 octets (qui se décompresse en quelques Ko) le crashe de manière fiable. La mémoire utilisée n'augmente pas ; ce n'est pas une "bombe zip/deflate."
La routine de décodage deflate se trouve dans RhinoNET.dll (la bibliothèque réseau de Serv-U) et est atteinte depuis le chemin de réception HTTP (ProcessReceive) une fois que l'analyseur d'en-tête a défini le drapeau "deflate" lors de la détection de Content-Encoding: deflate.
Simplifiée, la routine est appelée comme decode(this, &bodyPtr, &bodyLen) et fait :
1. inflate bodyPtr[0..bodyLen] -> grossit un tampon accumulateur `acc` (zlib standard, borné, correct)
2. free(*bodyPtr) <-- *bodyPtr est un pointeur INTÉRIEUR dans le tampon de réception
3. *bodyPtr = acc // remplace le corps compressé par le tampon décompressé
4. *bodyLen = total
L'étape 2 est le bogue. *bodyPtr n'est pas un bloc alloué indépendamment : il pointe vers le corps de la requête à l'intérieur du tampon de réception HTTP unique, c'est-à-dire receive_buffer + header_length. Libérer un pointeur intérieur (et non aligné sur 16 octets) amène l'allocateur à analyser une région influencée par l'attaquant comme un en-tête de chunk du tas → corruption des métadonnées du tas → 0xC0000374.
Les deux hypothèses "évidentes" sont fausses, vérifiées à l'exécution :
zlib1.dll!inflate standard et respecte avail_out à chaque appel (observé sur plus de 100 chunks ; aucune écriture hors limites).prev_total + produced + 1 à chaque tour et écrit exactement cette quantité — serré, aucun débordement.La corruption est uniquement due au free() invalide de l'étape 2.
Capturées à l'exécution (Frida) contre Serv-U 15.5.4.108 lors de l'envoi d'une requête Content-Encoding: deflate dont le corps se décompresse en "A" * 8192 :
…ce — mod 16 == 14. Les bases d'allocation du tas sont alignées sur 16 octets, donc ce n'est pas une base d'allocation ; c'est un pointeur intérieur.\r\n\r\n :
…tream\r\nContent-Length: 26\r\nConnection: close\r\n\r\n
ed c1 01 0d 00 …).ntdll.dll, exception 0xC0000374 (STATUS_HEAP_CORRUPTION), avec l'appel provenant du free dans le gestionnaire deflate de RhinoNET.dll. La dernière opération de tas avant le crash est exactement ce free(bodyPtr).0xC0000374) dans nos tests, et nous n'avons pas obtenu d'exécution de code. Considérez cela comme un DoS par corruption du tas sans authentification avec une cause racine de sécurité mémoire ; ne supposez pas d'exécution de code à distance.Nécessite Python 3 (bibliothèque standard uniquement). Pointez-le vers un écouteur HTTP Serv-U que vous êtes autorisé à tester. Sous Windows, il utilise un appel Get-NetTCPConnection pour lire le PID de l'écouteur et le journal des événements d'application pour confirmer le crash de corruption du tas.
python poc_verify.py # par défaut 127.0.0.1:80, petit paquet, 1 tir
python poc_verify.py --host <ip> --port 80
python poc_verify.py --big # corps se décompresse en 8192 octets
python poc_verify.py --shots 3
python poc_verify.py --no-events # sauter la vérification du journal des événements (aucun privilège nécessaire)
La PoC construit une requête minimale :
POST / HTTP/1.1
Host: <cible>
Content-Encoding: deflate
Content-Type: application/octet-stream
Content-Length: <n>
Connection: close
<corps deflate brut — même quelques dizaines d'octets suffisent>
Elle signale PASS si le service a crashé (PID de l'écouteur changé / événement 0xC0000374 apparu) et FAIL s'il a survécu (déjà corrigé, non affecté, ou le corps n'a pas été traité comme deflate).
Content-Encoding entrant sur l'écouteur HTTP/HTTPS, par exemple sur un proxy inverse :
if ($http_content_encoding) { return 400; }
RhinoNET.dll / Serv-U.exe (base de l'image 0x180000000) pour cartographier le chemin réception → analyse d'en-tête → décodage deflate et localiser le free erroné.zlib respecte avail_out, traçage de chaque alloc/free/memcpy de l'appel de décodage vers le disque, et un gestionnaire d'exception de processus pour capturer l'instruction fautive et la pile 0xC0000374 au moment de la corruption. Le free du pointeur intérieur a ensuite été confirmé en vidant les octets autour du pointeur libéré (queue d'en-tête HTTP + corps deflate brut, non aligné sur 16 octets).Les décalages référencés dans l'analyse sont spécifiques à la build 15.5.4.108 et différeront selon les builds.
Publié en tant que recherche défensive après disponibilité du correctif du fournisseur. La PoC est uniquement DoS et destinée à des tests autorisés.
| Produit | SolarWinds Serv-U (FTP / MFT / File Server) |
| Vulnérable | 15.5.4 et versions antérieures dans cette branche sans le Hotfix 1 (analyse effectuée sur la build 15.5.4.108) |
| Corrigé | Serv-U 15.5.4 Hotfix 1 (publié le 2026-06-04) |
| Vecteur | Sans authentification, réseau (port HTTP/HTTPS de gestion/web) |
| Classe publique | CWE-400 Uncontrolled Resource Consumption · DoS · CVSS 7.5 · CISA KEV |
| Classe réelle | CWE-763 Release of Invalid Pointer / CWE-590 Free of Memory not on the Heap → corruption du tas |