
Recherche originale et PoC pour un débordement de tampon de pile sur le mot de passe décodé en Base64 avant authentification dans login.cgi du Netis NC63
login.cgi du Netis NC63 menant à une RCEChercheur : Ö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 local de pile
de 64 octets mais ne transmet pas sa capacité au décodeur. Le décodeur dérive son
travail à partir de l'entrée encodée et écrit les 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é. Les tests dynamiques contre le CGI de production à hachage d'origine ont confirmé :
B décodé de 140 octets produit un défaut à 0x42424242 ;ra sauvegardé par 0x0041a2e0 provoque une seconde entrée observée au
niveau du gestionnaire de connexion, prouvant le contrôle du compteur de programme ; etsystem() du binaire d'origine avec une valeur a0 MIPS choisie par l'attaquant. Le
remplacement /bin/sh a journalisé /bin/sh -c NC63_RCE_PROOF et n'a exécuté aucune commande.Le PoC public de ce dépôt s'arrête délibérément à un motif de plantage. Il
ne contient aucune chaîne de retour, shellcode, commande, reverse shell ou persistance.
Le script RCE maintenu séparément dans poc/exploit-db/ est préparé pour une
soumission à Exploit-DB et est destiné uniquement à des tests autorisés sur les systèmes
affectés.
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 possède le même SHA-256 que
l'exécutable extrait du firmware constructeur.

Le frontend constructeur envoie le mot de passe au point de terminaison public 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 non sécurisé
se produit avant que le mot de passe décodé ne soit comparé au mot de passe
administrateur configuré. Aucune session valide, en-tête Cookie, en-tête Authorization,
ni mot de passe correct n'est requis.
Unauthenticated HTTP client
|
| POST /cgi-bin/login.cgi
| password=<attacker-controlled Base64>
v
/bin/netis.cgi: FUN_0041a2e0
|
| get_request_param("password")
v
FUN_00402bd4(decoded_stack_buffer, encoded_password)
|
| no destination-capacity argument
| decoded output exceeds 64 bytes
v
saved s8 at decoded offset 132
saved ra at decoded offset 136
|
v
attacker-selected MIPS PC
Pseudocode 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); /* no capacity argument */
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 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 ordinaire avec remplissage, 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 l'écriture.
FUN_0041a2e0 commence à 0x0041a2e0 et crée un cadre 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 ; les registres s8 et ra sauvegardés se trouvent à
s8+0xa0 et s8+0xa4 :
decoded[64] s8+0x1c decoded offset 0
saved s8 s8+0xa0 decoded offset 132
saved ra s8+0xa4 decoded offset 136
La distance exacte de l'adresse de retour est 0xa4 - 0x1c = 0x88, soit 136 octets.

Un motif B décodé de 140 octets 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)
