
Recherche originale et PoC non destructif pour un débordement de tampon de pile du mot de passe décodé en Base64, avant authentification, dans login.cgi de Netis NC63.
login.cgi du Netis NC63, conduisant à une exécution de code à distance (RCE)Chercheur : Özcan Ersan (@ozcanpng)
CVE-2026-76070NC63_V3.0.0.3327/bin/netis.cgiPOST /cgi-bin/login.cgipassword encodé en Base64Le gestionnaire de connexion public du micrologiciel Netis NC63 V3.0.0.3327
récupère le paramètre password contrôlé par l'attaquant et le décode avec la
routine Base64 personnalisée FUN_00402bd4. L'appelant fournit un tampon de
pile local de 64 octets mais ne transmet pas sa capacité au décodeur. Le
décodeur déduit son travail de l'entrée encodée et écrit des octets décodés
sans vérifier la fin de la destination.
L'adresse de retour MIPS sauvegardée se trouve à 136 octets du début du tampon décodé. Des tests dynamiques contre le CGI de production à hachage original ont confirmé :
B décodés produit un défaut à 0x42424242 ;ra sauvegardé par 0x0041a2e0 provoque une seconde
entrée observée dans le gestionnaire de connexion, prouvant le contrôle du
compteur ordinal ; etsystem() du binaire d'origine avec une valeur a0 MIPS choisie par
l'attaquant. Le /bin/sh de remplacement a journalisé
/bin/sh -c NC63_RCE_PROOF et n'a exécuté aucune commande.La preuve de concept (PoC) publique de ce dépôt s'arrête délibérément à un motif de plantage. Elle ne contient ni chaîne de retour, ni shellcode, ni commande, ni shell inverse, ni persistance.
193f6a5e2ce65972b1805bf076f8d3521379a8441c8aaeb5ad0ba174bbee0792 netis_NC63_V3.0.0.3327.bin
23faa747b7d2f067aa5431bcc227ceca97a7977cf3e7c372f715cbba57f9209b squashfs-root/bin/boa
eb298774c27070dc595fefcabb4e8c12a46cb5f4fd08f91c3ca92282c3a289a2 squashfs-root/bin/netis.cgi
La copie de /bin/netis.cgi testée dynamiquement a la même empreinte SHA-256
que l'exécutable extrait du vendeur.

Le frontal du vendeur envoie le mot de passe au point d'accès public encodé en Base64 :
obj.password = base64encode(utf16to8(password));
request({
url: "/cgi-bin/login.cgi",
data: obj
});
Le champ HTML utilise maxlength="63", mais il ne s'agit que d'une restriction
côté navigateur. Un client HTTP direct peut soumettre une valeur encodée plus
grande.

login.cgi est nécessairement accessible avant l'authentification. Le décodage
dangereux a lieu avant que le mot de passe décodé ne soit comparé au mot de
passe administrateur configuré. Aucune session valide, aucun en-tête Cookie, ni
en-tête Authorization, ni mot de passe correct n'est requis.
Client HTTP non authentifié
|
| POST /cgi-bin/login.cgi
| password=<Base64 contrôlée par l'attaquant>
v
/bin/netis.cgi : FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(tampon_pile_décodé, mot_de_passe_encodé)
|
| aucun argument de capacité de destination
| la sortie décodée dépasse 64 octets
v
s8 sauvegardé au décalage décodé 132
ra sauvegardé au décalage décodé 136
|
v
PC MIPS choisi par l'attaquant
Pseudo-code dérivé de Ghidra, avec des noms normalisés pour la lisibilité :
int login_cgi(void *request)
{
char decoded[64];
char stored[68];
char *password;
memset(decoded, 0, 64);
memset(stored, 0, 64);
password = get_request_param(request, "password");
if (password != NULL)
FUN_00402bd4(decoded, password); /* aucun argument de capacité */
apmib_get(0x15e, stored);
if (strcmp(decoded, stored) == 0)
printf("[\"SUCCESS\"]");
else {
system("echo 0 >/tmp/boa_auth");
printf("[\"%d\"]", 0x15);
}
return 0;
}

Le décodeur à FUN_00402bd4 ne reçoit que des pointeurs de destination et de
source. Sa boucle avance le pointeur de destination et stocke jusqu'à trois
octets décodés pour chaque groupe de quatre symboles Base64. Aucune comparaison
ne vérifie la destination par rapport à decoded + 64.

Base64 est la transformation d'entrée, pas le défaut sous-jacent. La cause racine est l'inadéquation entre la longueur décodée contrôlée par l'attaquant et une destination de taille fixe dont la capacité n'est jamais appliquée. Pour une entrée avec rembourrage ordinaire, quatre caractères encodés représentent jusqu'à trois octets décodés ; les vérifications côté serveur doivent donc calculer et valider la taille décodée avant d'écrire.
FUN_0041a2e0 commence à 0x0041a2e0 et crée une trame de 0xa8 octets :
0041a2e0 addiu sp,sp,-168
0041a2e4 sw ra,164(sp)
0041a2e8 sw s8,160(sp)
0041a2ec move s8,sp
La destination décodée commence à s8+0x1c ; s8 sauvegardé et ra sauvegardé
sont à s8+0xa0 et s8+0xa4 :
decoded[64] s8+0x1c décalage décodé 0
s8 sauvegardé s8+0xa0 décalage décodé 132
ra sauvegardé s8+0xa4 décalage décodé 136
La distance exacte de l'adresse de retour est 0xa4 - 0x1c = 0x88, soit
136 octets.

Un motif de 140 octets B décodés a remplacé l'adresse de retour sauvegardée
de quatre octets :
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0x42424242} ---
qemu: uncaught target signal 11 (Segmentation fault)

Une entrée distincte de 140 octets a défini ra sauvegardé à 0x0041a2e0. Le
traçage CPU de QEMU a enregistré une première entrée ordinaire du gestionnaire
suivie d'une seconde entrée avec s8=0x41414141 et ra=0x0041a2e0.

Le binaire d'origine contient un jal system direct à 0x0041a3cc. Lors de la
validation isolée privée, des instructions existantes à base fixe ont chargé un
marqueur dans a0 et ont atteint cet appel. Un programme d'observation
statique a été monté sur /bin/sh ; il a journalisé les arguments de
l'interpréteur de commandes et n'a rien exécuté :
argv[0]=</bin/sh>
argv[1]=<-c>
argv[2]=<NC63_RCE_PROOF>
CONTROLLED_MARKER_REACHED
PASS: a0 contrôlé par l'attaquant a atteint system() et argv de /bin/sh.
PASS: le garde a journalisé la requête et n'a exécuté aucune commande.
Cela démontre une primitive RCE dans le chemin de code de production isolé. Cela n'établit pas une fiabilité d'exploitation identique sur un routeur physique avec son noyau déployé et sa configuration de randomisation de la pile.
La configuration Boa d'origine spécifie User root, Group root et un chemin
CGI contenant /bin et /web/cgi-bin. L'exécutable de production a une base
fixe (0x00400000), ne possède ni canari de pile ni RELRO, et déclare une pile
GNU exécutable avec des segments RWX.


Le script inclus est en mode essai à blanc (dry-run) par défaut et ne génère
qu'un corps de formulaire Base64 contenant 140 octets B après décodage :
python3 poc/poc.py
L'envoi nécessite une cible autorisée explicite et --send :
python3 poc/poc.py --target http://192.168.1.1 --send
L'envoi du motif peut faire planter le processus CGI. Utilisez-le uniquement dans un environnement autorisé et jetable. La PoC n'implémente pas la chaîne de validation RCE privée.
Une exploitation réussie peut exécuter du code ou des commandes choisis par l'attaquant dans le contexte de gestion du routeur. Dans la configuration Boa d'origine, ce contexte s'exécute en tant que root. Les conséquences potentielles incluent la divulgation de configuration et de secrets, la manipulation du DNS, du pare-feu et du routage, la redirection du trafic, l'interruption de service et la compromission totale de l'appareil.
FUN_00402bd4.Voir evidence/README.md pour les captures d'écran et le
mappage des traces. Les extraits Ghidra normalisés se trouvent dans
attachments/decompiled-functions/.
CVE-2026-76070 et autorisé la
divulgation publique.Aucun routeur physique n'a été flashé. Aucune commande shell réelle, aucun shell inverse, aucune persistance, aucune connexion externe, aucun vol d'identifiants ni aucune opération destructive du micrologiciel n'a été utilisé.