
wp2shell (CVE-2026-63030 & CVE-2026-60137) - 완전한 RCE 체인
Searchlight Cyber의 wp2shell 권고와 관련된 인증되지 않은 WordPress REST 배치 경로 혼동(route-confusion) SQL 삽입에 대한 독립적인 개념 증명(PoC)입니다.
이 저장소는 Searchlight Cyber의 공식 점검 도구가 아닙니다. check는 SQLi 경로를 확인하고, read는 데이터베이스 읽기를 시연하며, shell은 제공된 관리자 자격 증명을 사용하거나 먼저 SQLi-to-admin 브리지를 실행하여 플러그인 기반 명령 셸을 엽니다.
Searchlight Cyber 권고에 나열된 wp2shell RCE 노출 범위는 다음과 같습니다.
| 버전 범위 | 상태 |
|---|---|
| <= 6.8.5 | 영향을 받지 않음 |
| 6.9.0 – 6.9.4 | 영향 받음 |
| 7.0.0 – 7.0.1 | 영향 받음 |
REST 배치 엔드포인트(/batch/v1)는 인증이 없으며, 하나의 호출에서 여러 하위 요청을 실행하고 각 하위 요청이 자체적으로 검증 및 권한 검사를 받도록 합니다.
serve_batch_request_v1()은 두 개의 병렬 배열, 즉 $matches(하위 요청별 일치하는 핸들러)와 $validation(하위 요청별 검증 결과)을 만든 다음 디스패치할 때 동일한 오프셋으로 둘 다 인덱싱합니다. wp_parse_url()에 실패하는 경로를 가진 하위 요청은 $validation에는 추가되지만 $matches에는 추가되지 않으므로 배열이 어긋나게 되고, 하위 요청이 다른 하위 요청의 핸들러 아래에서 디스패치됩니다. 이것이 경로 혼동입니다.
PoC는 이 프리미티브를 두 번 중첩합니다:
requests 본문을 가진 POST /wp/v2/posts 요청은 배치 핸들러 자체 아래에서 디스패치됩니다. posts 요청으로 검증되었기 때문에 requests 목록은 배치 스키마와 대조하여 검사되지 않으므로 하위 요청은 GET을 사용할 수 있습니다. 메서드 허용 목록이 우회됩니다.GET /wp/v2/posts/999999 항목-라우트 요청은 author_exclude, orderby, per_page 같은 posts 컬렉션 쿼리 매개변수를 전달합니다. 999999 ID는 존재할 필요가 없습니다. 단지 컬렉션 전용 매개변수를 검증하지 않는 항목 라우트와 일치시키기 위한 흔하지 않은 포스트 ID일 뿐입니다. 그런 다음 이 desync로 인해 동일한 요청이 posts get_items() 아래에서 디스패치되고, 여기서 author_exclude는 WP_Query의 author__not_in 쿼리 변수에 매핑되며, 취약한 빌드는 이를 SQL에 문자열로 삽입합니다.그 결과 인증 전에 도달할 수 있는 boolean 및 시간 기반 블라인드 SQL 삽입이 발생합니다. 이 PoC에는 SQLi-to-admin 체인에서 사용되는 UNION 가짜 포스트 프리미티브도 포함되어 있습니다.
여기서 구현된 RCE 경로는 다음과 같습니다:
wp_posts 행을 사용하여 posts 컬렉션을 통해 공격자 제어 콘텐츠를 렌더링합니다. 렌더 브리지는 /wp/v2/posts/999999 항목-라우트 소스를 사용합니다. SQLi 읽기가 get_items()에 도달하기 위해 사용하는 것과 동일한 라우트입니다.POST /wp/v2/users에 도달하여 생성된 관리자를 만들게 합니다.1~5단계는 인증 전이며, 명령 실행 단계는 인증된 관리자 플러그인 업로드입니다.
Python 3.8+ 및 표준 라이브러리. 타사 종속성은 없습니다.
저장소 디렉터리에서 실행합니다:
./wp2shell.py <command> <url> [options]
또는 pip install .를 실행하여 PATH에 wp2shell 명령을 추가할 수 있습니다.
먼저 수동적 WordPress 마커와 공개 버전 힌트를 출력한 다음, 무해한 배치 마커 프로브를 보냅니다. 취약한 배치 구현은 경로 혼동 마커 패턴인 parse_path_failed, block_cannot_read, rest_batch_not_allowed와 함께 HTTP 207을 반환합니다.
마커 프로브는 WordPress 코어 수정을 기반으로 합니다. 잘못된 /// 요청은 parse_path_failed를 만들고, /wp/v2/posts 요청은 배치 허용 스페이서 역할을 하며, /wp/v2/block-renderer/... 라우트는 배치가 허용되지 않지만 핸들러에 익명으로 도달하면 block_cannot_read를 반환하고, /batch/v1은 rest_batch_not_allowed를 반환합니다. 취약한 빌드에서는 구문 분석 오류로 인해 배치 핸들러 배열이 어긋나므로 스페이서 요청이 블록 렌더러 핸들러 아래에서 디스패치됩니다. 수정된 빌드는 배열을 일치된 상태로 유지하므로, 제작된 프로브에 대해 이 세 가지가 모두 나타나는 패턴은 나타나지 않아야 합니다.
기본적으로 check는 여기서 멈추고 SQLi 페이로드를 보내지 않습니다. 활성 SQLi 확인도 원한다면 --confirm-sqli를 사용하십시오. 확인은 먼저 UNION 읽기 프리미티브를 시도하고, UNION 반사가 불가능하면 짝을 이루는 타이밍 프로브로 대체합니다.
신호는 서로 독립적입니다. 버전 힌트는 힌트일 뿐이며, 마커 패턴은 경로 혼동을 보여주고, --confirm-sqli는 페이로드가 데이터베이스에 도달했음을 보여줍니다. WAF가 페이로드를 차단할 수 있으므로 확인 실패가 버그가 없음을 증명하지는 않습니다.
./wp2shell.py check http://target
./wp2shell.py check targets.txt # scan every URL in the file
./wp2shell.py read http://target # server fingerprint
./wp2shell.py read http://target --preset users # user logins and password hashes
./wp2shell.py read http://target --query "SELECT @@version"
기본적으로 추출은 --technique auto를 사용하며, 다음 순서로 사용 가능한 방법을 시도합니다:
UNION을 통해 가짜 WP_Post 행을 만들고 REST 응답에서 제목을 ||HEX(value)||로 다시 읽습니다. 페이로드는 orderby=none 및 per_page=500과 함께 동일한 /wp/v2/posts/999999 소스 라우트를 사용하므로 가짜 행이 렌더링된 포스트로 유지됩니다. 값당 요청 1회.WP_DEBUG_DISPLAY 켜짐) 요청당 약 15바이트를 유출하는 EXTRACTVALUE/UPDATEXML.X-WP-Total 헤더를 참/거짓 신호로 읽으며 반영된 값이 필요 없습니다.--technique union|error|blind로 하나를 강제로 지정할 수 있습니다. 이러한 읽기 경로는 데이터베이스 행을 쓰지 않습니다.
--user 및 --password를 사용하면 shell은 제공된 관리자 자격 증명으로 로그인하고 WordPress 플러그인 업로드 동작을 사용합니다.
자격 증명이 없으면 shell은 먼저 사전 인증 SQLi-to-admin 브리지를 실행하고 생성된 관리자로 로그인한 다음 플러그인 셸을 업로드합니다.
./wp2shell.py shell http://target --user admin --password '<recovered>' --cmd id
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # interactive shell
./wp2shell.py shell http://target --cmd id # pre-auth bridge
./wp2shell.py shell http://target -i # pre-auth interactive
shell은 플러그인 웹셸(임의 경로와 실행별 토큰으로 잠김)을 업로드하고 해당 경로를 출력합니다. 업로드된 웹셸은 자동으로 제거됩니다. 사전 인증 브리지가 관리자를 생성한 경우 해당 생성 계정은 셸 세션이 끝난 후 자동으로 제거됩니다.
WordPress 7.0.2로 업데이트하거나, 사이트가 6.9 브랜치에 있다면 6.9.5로 업데이트하십시오. 그때까지는 /wp-json/batch/v1과 rest_route=/batch/v1 쿼리 매개변수를 모두 엣지에서 차단하거나, rest_pre_dispatch 필터를 통해 배치 엔드포인트에 인증을 요구하십시오.
공인된 보안 테스트 전용입니다. 소유하고 있거나 명시적인 서면 허가를 받은 시스템에 대해서만 사용하십시오. 어떠한 보증도 제공되지 않으며 오용에 대한 책임도 지지 않습니다.
| 옵션 | 적용 대상 | 설명 |
|---|
--proxy URL | 모두 | HTTP 프록시(예: Burp)를 통해 트래픽을 라우팅합니다. |
--timeout N | 모두 | 요청 제한 시간(초)입니다. |
--sleep N | check | --confirm-sqli의 타이밍 폴백에서 사용하는 지연 시간입니다. |
--samples N | check | --confirm-sqli의 타이밍 폴백에서 사용하는 타이밍 쌍 수입니다. |
--confirm-sqli | check | 활성 SQLi 확인 페이로드도 전송합니다. |
--preset | read | fingerprint 또는 users. |
--technique | read | auto(기본값), union(인밴드, 가짜 포스트를 위조), error(인밴드, DB 오류 표시 필요), blind 중 하나입니다. |
--query | read | 읽을 스칼라 SQL 표현식입니다. |
--prefix | read | 데이터베이스 테이블 접두사(기본값 wp_). |
--max-length N | read | 값당 읽을 최대 문자 수(기본값 128). |
--user / --password | shell | 선택적 관리자 자격 증명. 둘 다 생략하면 사전 인증 브리지를 사용합니다. |
--cmd | shell | 실행할 명령(-i 사용 시 생략). |
-i / --interactive | shell | 배포 후 대화형 셸을 엽니다. |