
CVE-2026-15718에 대한 PoC 익스플로잇 체인: SpiderMonkey wasm 베이스라인 컴파일러의 array.fill 동기화 누락 -> 잘못된 포인터 -> addrOf/fakeobj -> 임의 읽기/쓰기 -> RCE
CVE-2026-15718에 대한 개념 증명 익스플로잇 체인으로, Mozilla SpiderMonkey의 WebAssembly baseline 컴파일러("JavaScript: WebAssembly" 구성 요소)에서 sync() 호출이 누락된 잘못된 컴파일입니다. 단 한 번의 zero-trip array.fill로 fakeobj 프리미티브를 얻고, 체인은 호스트 프로세스에 대한 임의 읽기/쓰기 및 임의 네이티브 코드 실행(posix_spawnp("/bin/sh", ...))으로 확장됩니다.
4441102e62e8 ("Bug 2045443", 한 줄)BaseCompiler::emitArrayFill() (js/src/wasm/WasmBaselineCompile.cpp)는 먼저 를 호출하지 않고 채우기 루프를 생성합니다:
sync()array.fill if (elementType.isRefRepr()) {
freePtr(RegPtr(PreBarrierReg));
}
+ sync();
+
// Perform the fill loop using `numElements` as the loop variable ...
sync()는 baseline 컴파일러의 값 스택(stk_)을 레지스터에서 머신 스택으로 플러시합니다. 채우기 루프의 반복마다 실행되는 부정확한 사후 배리어(emitPostBarrierEdgeImprecise)는 내부적으로 sync()를 호출합니다. 단, 루프가 실행되는 경우에만 그렇습니다. 루프의 종료 레이블(done)은 두 경로의 조인 지점입니다:
stk_가 동기화됩니다. 모든 피연산자는 머신 스택 슬롯에 존재합니다.numElements == 0): stk_는 건드리지 않습니다. 피연산자는 여전히 레지스터에만 존재합니다.조인 이후 컴파일러의 정적 모델은 항상 동기화된 경로를 반영하므로, array.fill 이후에 생성된 코드는 zero-trip 실행이 결코 쓰지 않은 스택 슬롯에서 피연산자를 읽습니다. 이렇게 읽어낸 reftype 피연산자는 유효하지 않은(스테일) 포인터(CWE-763)입니다.
이 비동기화는 프레임이 동일한 두 wasm 함수 간에 양방향으로 작동합니다:
n = 1, 루프 실행): 루프 내부의 sync()가 슬롯에 값을 쓰기합니다. 실제 ref(addrOf) 또는 제어된 i64(fakeobj 설정)입니다.n = 0, zero-trip): 슬롯에는 결코 쓰지 않습니다. 쌍을 이루는 함수가 스테일 슬롯을 다시 읽습니다. i64(누출) 또는 (ref $t)(fakeobj)로 읽습니다.| File | Stage | Result |
|---|---|---|
poc-min.js (+ gen_trigger.py) | desync 데모 | 제어 경로는 올바른 값을 반환하고, zero-trip 경로는 결코 쓰이지 않은 슬롯에서 읽은 가비지 포인터를 반환합니다 |
chain.js (+ gen_chain.py, chain.tpl.js, build.sh) | 전체 체인 | addrOf + fakeobj → fake WasmArrayObject → 임의 R/W → XUL 베이스 누출 → 런타임 Mach-O 임포트 탐색 → 위조된 funcref → posix_spawnp("/bin/sh", ["-c", "touch /tmp/CVE-2026-15718-PWNED"]) |
chain.js의 단계별 세부 사항:
plantSpray/plantCmd/plantFun/plantExt + leak — wasm 배열, funcref 및 JS 객체(externref)에 대한 addrOf입니다.sprayV + fakeRd/fakeWr — fakeobj입니다. 위조된 WasmArrayObject(numElements_ @+16, data_ @+24, 인라인 요소 @+40)를 만들며, 위조 창은 out-of-line(malloc) 배열에 유지되어 GC가 스캔 중에 이를 이동시킬 수 없습니다.0xfeedfacf) → XUL 베이스를 찾습니다.LC_SEGMENT_64/LC_SYMTAB/LC_DYSYMTAB + 간접 심볼 테이블)을 탐색하여 _posix_spawnp의 __stubs 엔트리를 찾습니다. 하드코딩된 오프셋은 없으며, 스텁은 첫 호출 시 지연 바인딩을 수행합니다.typeDef @+0x40은 실제 ref.func에서 복사, 호출 대상 @+0x38 = 스텁 주소)를 위조하고 call_ref를 통해 posix_spawnp(pid, "/bin/sh", 0, 0, [sh, -c, cmd], 0)로 호출합니다.152.0.6 이전 트리에서 빌드한 xpcshell(또는 js 셸/브라우저)로 테스트했습니다(여기서 참조 빌드는 Firefox 149.0a1, 2026-01-14입니다):
# minimal desync demo
xpcshell poc/poc-min.js
# full chain (regenerates chain.js from the template + generator)
(cd poc && sh build.sh)
rm -f /tmp/CVE-2026-15718-PWNED
xpcshell poc/chain.js # creates /tmp/CVE-2026-15718-PWNED
ls -l /tmp/CVE-2026-15718-PWNED
테스트 하네스는 javascript.options.wasm_optimizingjit=false를 설정하여 긴 Mach-O 스캔 동안 baseline 티어를 고정합니다(이 버그는 baseline 전용이며, 브라우저에서는 트리거가 티어 업 임계값보다 훨씬 낮은 수준에서 실행되므로 브라우저에서는 필요하지 않습니다). tools/xul_slots.py는 XUL의 명명된 스텁/GOT 슬롯을 나열하는 오프라인 검증 도구로, 런타임 탐색 코드를 교차 확인하는 데 사용됩니다.
오프셋은 macOS arm64, 이 정확한 트리/빌드 기준입니다 (WasmArrayObject { +16 numElements, +24 data_, +40 inline }, JSFunction 네이티브 @ +0x20, WasmFuncRef { +0x38/+0x48 call target, +0x40 typeDef }). 이들 모두 체인의 자체 테스트에 의해 런타임에 다시 파생되며, 구조 레이아웃만 가정합니다. PAC 문제는 없습니다(XUL은 arm64, non-arm64e 바이너리입니다). 지연 바인딩 스텁은 첫 호출 시 대상을 해석하므로 dyld 공유 캐시 오프셋이 필요하지 않습니다.
이 PoC는 샌드박스가 없는 셸 프로세스에서 실행됩니다. 실제 Firefox 공격에서는 체인이 콘텐츠 프로세스 내부에 도달하며, macOS 샌드박스가 posix_spawnp를 차단합니다. 따라서 전체를 장악하려면 별도의 OS 샌드박스 탈출 버그가 필요합니다(예: CVE-2026-2796-and-CVE-2026-2768-escape-the-wasm-box의 2단계 분석 참조).
javascript.options.wasm=false가 트리거 벡터를 차단합니다.보안 연구, 교육 및 방어적 테스트 전용입니다. 이 취약점은 현재 Firefox/Thunderbird 릴리스에서 패치되었습니다. 소유하지 않았거나 테스트에 대한 명시적 승인을 받지 않은 시스템에 사용하지 마십시오.