infiltr8 Lab
Home/ CVE/ 웹·CMS/ Elementor 온보딩 모듈 권한 우회
CVE · 웹·CMS · RCE · CVE-2022-1329
심층 분석 연구 분석

CVE-2022-1329 (Elementor 플러그인 권한 우회) 분석

500만 개 이상 사이트에 깔린 워드프레스(WordPress) 페이지 빌더 Elementor가, 로그인만 한 최하위 권한(Subscriber) 계정에게도 관리자 전용 기능을 통째로 열어준 사건입니다(CVE-2022-1329, CVSS 8.8). 문을 여는 열쇠는 새로 만든 게 아니라, 이미 모든 로그인 사용자에게 나눠준 열쇠(nonce)를 관리자용 열쇠로 착각해서 받아준 것이었습니다. 이 글은 원 발견자(Wordfence)의 분석을 재구성하고, Elementor의 실제 오픈소스를 고정 커밋 기준으로 직접 열어 근본원인·호출 흐름·패치를 분석합니다.

이 글의 코드 발췌(core/app/modules/onboarding/module.php의 Module::__construct()·maybe_handle_ajax()·upload_and_install_pro(), core/common/app.php의 App::init_components(), core/common/modules/ajax/module.php의 NONCE_KEY·create_nonce()·get_init_settings())는 elementor/elementor GitHub 저장소(벤더 본인이 운영하는 공식 배포 미러)의 커밋 e66df28f9c7c65b85e3262f4006821621233b876(v3.6.2 태그, 패치 전)·557eff68475cc9ff8e05880ff00301418902cddc(v3.6.3 태그, 패치 후)·d30e5a5860685f2d48ce7278f0d582624af1abee(v3.6.4 태그, 후속 강화)를 얕은 클론(shallow clone)으로 직접 받아 파일·라인 단위로 대조했습니다. NVD가 1차 패치 근거로 인용하는 WordPress.org 플러그인 SVN 체인지셋(2708766)은 봇 차단 챌린지(HTTP 403)로 직접 열람하지 못했으나, elementor/elementor GitHub 태그 간 diff는 동일한 버전 범프(3.6.2→3.6.3)를 반영한 벤더 자체 배포본이므로 이 한계를 안고 코드 사실을 기술했습니다. CVSS·CWE·영향 버전·공개일은 NVD API(services.nvd.nist.gov)에서 직접 조회했습니다. 근본원인(W1·W2) 자체는 Wordfence의 서술에 의존하지 않고 Elementor 자체 오픈소스(위 커밋들)를 직접 읽어 저희가 독립적으로 재구성·확증한 것입니다 — core/common/app.php의 Ajax 모듈 무조건 로드 vs Finder 모듈 조건부 로드 대비(W2)는 저희가 코드를 직접 대조해 처음 짚어낸 지점으로, Wordfence 블로그가 명시적으로 언급한 부분이 아닙니다. 반면 Wordfence 블로그·자사 threat-intel 페이지 원문은 봇 차단(자동화 접근 차단)으로 저희가 직접 열람하지 못했습니다 — 그 사이트가 서술한다고 알려진 내용(nonce가 모든 인증 사용자에게 노출된다는 설명, 발견자 이름, 자체 CVSS 9.9 산정)은 여러 검색엔진 결과·2차 집계 사이트(SentinelOne 등)의 교차 인용으로만 확인했으며, 원문 byte-level 대조는 하지 못했습니다 — 그중 CVSS 9.9 수치는 이 한계 때문에 §CVSS 절에서 미검증으로 명시하고 본문에서는 NVD 확인값(8.8)만 공식값으로 씁니다. 실제 공격에 쓰이는 zip 파일 구조(elementor-pro 폴더명·플러그인 헤더 요건)는 공개 PoC(exploit-db #50882)에 대한 설명을 Elementor 소스코드(upload_and_install_pro())와 교차 대조한 것이며, 저희가 그 PoC를 직접 실행해 재현하지는 않았습니다 — 이 한계는 §패치 완전성 평가·본문 배지에도 명시했습니다.

infiltr8 team 직접 분석 #Elementor#WordPress#MissingAuthorization#ArbitraryFileUpload
무엇Elementor의 온보딩(onboarding) 모듈이 관리자 전용 AJAX 액션 7종을 실행하기 전에 current_user_can() 권한 검사를 아예 하지 않아, 로그인만 한 최하위 권한(Subscriber) 사용자도 이 액션들을 그대로 실행할 수 있었습니다.
원리검사한 건 신원이 아니라 wp_verify_nonce()뿐이었는데, 이 nonce(elementor_ajax)는 권한과 무관하게 모든 로그인 사용자에게 매 관리자 페이지마다 JS 설정값으로 뿌려지고 있었습니다 — 사실상 '로그인만 하면 통과'와 같았습니다.
핵심그 액션 중 하나(elementor_upload_and_install_pro)가 업로드된 zip 파일을 Plugin_Upgrader로 그대로 설치·활성화하기 때문에, 위조 플러그인 zip 안에 심은 PHP 코드가 서버에서 그대로 실행됩니다(RCE). CVSS 8.8(NVD, Wordfence의 CNA 제출값도 동일), CWE-862 + CWE-434.

01개요

Elementor는 워드프레스(WordPress)용 드래그앤드롭 페이지 빌더 플러그인으로, 2022년 기준 500만 개 이상의 사이트에 설치돼 워드프레스 플러그인 생태계에서 가장 널리 쓰이는 축에 속합니다. 3.6.0 버전부터 Elementor는 플러그인을 처음 설치한 관리자를 위한 '온보딩(onboarding) 마법사' — 사이트 이름·로고 설정, 테마 활성화, Elementor Pro 업그레이드 등을 안내하는 초기 설정 화면 — 을 추가했습니다.

이 온보딩 마법사가 서버와 통신하는 방식(AJAX 액션 처리)에 문제가 있었습니다. 온보딩 모듈은 요청에 담긴 nonce(위조요청방지 토큰, CSRF 토큰)가 유효한지만 확인하고, 그 요청을 보낸 사용자가 애초에 이 기능을 쓸 권한이 있는지는 확인하지 않았습니다. 두 검사는 목적이 다릅니다 — nonce는 '이 요청이 우리가 렌더링한 페이지에서 온 것이 맞는가'를 증명할 뿐, '이 사용자가 관리자인가'는 증명하지 않습니다. 그런데 이 nonce가 권한과 무관하게 모든 로그인 사용자에게 뿌려지고 있었기 때문에, 사실상 두 검사가 하나로 뭉개진 셈이 됐습니다.

그 결과, 사이트에 계정만 있으면(예: 오픈 회원가입, 댓글 작성용 계정, WooCommerce 고객 계정 등으로 만들어지는 최하위 권한 Subscriber) 사이트 이름·로고를 마음대로 바꾸는 것을 넘어, elementor_upload_and_install_pro 액션으로 임의의 zip 파일을 '플러그인'으로 위장해 업로드·설치·활성화시킬 수 있었습니다. 업로드한 zip 안에 PHP 코드를 심어두면 그 코드가 곧바로 서버에서 실행되므로, 최하위 권한 계정 하나로 사이트 전체를 장악하는 원격 코드 실행(RCE)까지 이어집니다.

CVSS: 8.8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) — NVD의 공식 산정이며, 흥미롭게도 Wordfence 본인이 CNA(CVE 번호 발급 기관) 자격으로 NVD에 제출한 값도 동일하게 8.8/S:U입니다(NVD API로 직접 확인). 다만 Wordfence가 자사 소비자용 threat-intel 취약점 데이터베이스 페이지에서는 이 CVE를 9.9(CRITICAL)로 별도 표기하고 있다고 여러 2차 소스(SentinelOne 등)가 인용합니다 — 저희는 봇 차단(자동화 접근 차단)으로 그 페이지 원문을 직접 열람·대조하지 못했고, 인용된 벡터 문자열(S:C 추정)도 byte-level로 확인하지 못했습니다 — 이 9.9 수치는 미검증 2차 인용으로 남겨둡니다. 이 글은 NVD가 직접 확인해 주는 8.8을 공식값으로 씁니다. CWE-862(Missing Authorization) + CWE-434(Unrestricted Upload of File with Dangerous Type).

이 근본원인은 온보딩 모듈이 처음 도입된 3.6.0(GitHub 태그 커밋 기준 2022-03-21)부터 존재했고, 3.6.3(2022-04-12, GitHub 태그 커밋 557eff68475cc9ff8e05880ff00301418902cddc 기준)에서 패치됐습니다. NVD 등록일은 2022-04-19입니다. 신고·발견은 Wordfence Threat Intelligence(연구원 Ramuel Gall)가 했고, 비슷한 시기 별도 업체 Plugin Vulnerabilities도 독립적으로 같은 문제를 공개했습니다(§References). 이 근본원인을 별도로 쪼갠 variant CVE는 확인되지 않았습니다 — 이 CVE 하나가 대표합니다.

02영향받는 버전

대상취약 버전안전 버전
Elementor Website Builder(무료판, wordpress.org 배포)3.6.0 ~ 3.6.2 (온보딩 모듈이 도입된 버전부터)3.6.3 이상
Elementor Pro이 CVE의 취약 대상 자체는 아님 — 다만 upload_and_install_pro 액션이 흉내 내는 설치 대상 이름이 elementor-pro라, 이 취약점의 실제 공격 벡터(위장 플러그인 zip)에 이름이 등장합니다.해당 없음

03배경 — nonce는 신원증명이 아니다

워드프레스에는 검증 목적이 서로 다른 두 가지 보안 장치가 있습니다. nonce(wp_verify_nonce())는 위조요청방지(CSRF) 토큰으로, '이 요청이 정말 우리가 방금 렌더링한 페이지에서 보낸 것인가'만 증명합니다 — 페이지를 볼 수 있는 사람이면 누구나 그 페이지 안에 박힌 nonce를 손에 넣습니다. 반면 capability 검사(current_user_can())는 '이 사용자가 이 작업을 할 권한이 실제로 있는가'를 확인합니다. 워드프레스 공식 문서와 개발 가이드는 두 검사를 항상 함께 쓰라고 명시합니다 — nonce만으로는 '누가' 요청했는지 전혀 알 수 없기 때문입니다.

Elementor 3.6.0의 온보딩 모듈은 이 관례에서 벗어난 방식으로 AJAX 요청을 처리했습니다. 워드프레스 표준 방식은 wp_ajax_{action} 훅에 핸들러를 등록하는 것인데(이 경우에도 개발자가 직접 capability를 확인해야 하는 건 동일합니다), 온보딩 모듈은 대신 admin_init 훅에 익명 함수를 걸어 두고 그 함수 안에서 $_POST['action']과 nonce를 직접 검사한 뒤, 통과하면 자체 스위치문으로 여러 기능을 라우팅하는 방식을 썼습니다. 이 자체는 문제가 아닙니다 — 문제는 이 관문에서 확인한 게 nonce 하나뿐이었다는 점입니다.

이 nonce가 어떻게 발급·배포되는지가 다음 절의 단서입니다. Elementor는 Ajax라는 공용(common) 모듈을 통해 nonce를 만들고, 이 모듈은 특정 권한을 가진 사용자에게만이 아니라 로그인한 모든 사용자에게 조건 없이 로드됩니다.

04취약점 발생 원인

이 취약점은 '누가 요청했는가'와 '요청한 사람이 이걸 할 자격이 있는가'를 같은 질문으로 취급한 데서 시작합니다. 관리자 전용 기능의 관문에 권한 검사가 아예 없었던 것(W1)과, 그 관문을 여는 유일한 열쇠(nonce)가 애초에 모든 로그인 사용자에게 조건 없이 배포되고 있었던 것(W2)이 만나 완성됩니다.

1"관리자 전용 기능인데, nonce만 확인하고 신원(권한)은 확인하지 않았다"

핵심

온보딩 모듈의 admin_init 훅 콜백이 wp_doing_ajax()·$_POST['action'] 존재·wp_verify_nonce() 세 조건만 통과하면 maybe_handle_ajax()를 호출해, 사이트 이름 변경부터 임의 플러그인 zip 설치까지 이어지는 스위치문 전체를 열어줬습니다.

Module::__construct()는 admin_init 액션에 익명 함수를 등록합니다. 이 함수는 요청이 AJAX 요청인지, action·_nonce POST 파라미터가 있는지, 그리고 그 nonce가 Ajax::NONCE_KEY('elementor_ajax') 기준으로 유효한지만 확인합니다. 이 네 조건이 전부 참이면 곧바로 $this->maybe_handle_ajax()를 호출합니다 — 요청자가 관리자인지, 심지어 Elementor를 편집할 권한이라도 있는지는 어디에서도 묻지 않습니다.

add_action( 'admin_init', function() { if ( wp_doing_ajax() && isset( $_POST['action'] ) && isset( $_POST['_nonce'] ) && wp_verify_nonce( $_POST['_nonce'], Ajax::NONCE_KEY ) ) { $this->maybe_handle_ajax(); } } );

Elementor 실제 소스 — core/app/modules/onboarding/module.php · Module::__construct() (패치 전, GitHub 커밋 e66df28f9c7c65b85e3262f4006821621233b876 = v3.6.2 태그). 438행이 nonce만 확인하고 끝나는 지점입니다 — current_user_can() 호출이 없습니다.

maybe_handle_ajax()는 $_POST['action'] 문자열 하나로 사이트 이름 변경(elementor_update_site_name)·로고 변경(elementor_update_site_logo·elementor_upload_site_logo)·테마 활성화(elementor_activate_hello_theme)·그리고 Elementor Pro 업로드·설치(elementor_upload_and_install_pro)까지 7가지 액션 중 하나로 라우팅합니다. 이 라우팅 지점에도 권한 재확인은 없습니다 — 앞의 관문 하나만 통과하면 7개 액션 전부가 열립니다.

왜 이렇게 됐나nonce 검증 함수의 이름(wp_verify_nonce) 자체에 '검증'이 들어 있다 보니, 코드를 훑어볼 때 '요청이 검증됐다'는 인상을 줍니다. 하지만 이 함수가 증명하는 건 요청의 출처일 뿐 요청자의 자격이 아닙니다. 이 코드는 그 둘을 구분하는 두 번째 질문(권한이 있는가)을 아예 던지지 않았습니다.

2"그 nonce 자체가, 권한과 무관하게 모든 로그인 사용자에게 뿌려지고 있었다"

핵심

elementor_ajax nonce를 발급하는 Ajax 공용 모듈은 App::init_components()에서 어떤 조건도 없이 추가되는 반면, 관리자 권한이 필요한 Finder 모듈은 명시적으로 current_user_can( 'manage_options' ) 뒤에서만 추가됩니다 — 같은 함수 안에서 Elementor 개발자 스스로 '이 모듈은 권한 게이트가 필요하다'와 '이 모듈은 필요 없다'를 갈랐는데, Ajax가 후자로 분류된 것입니다.

Ajax 모듈의 get_init_settings()는 'nonce' => $this->create_nonce()를 반환하고, 이 값은 매 wp-admin 페이지 로드마다 elementorCommon이라는 이름의 JS 설정 객체로 페이지 소스에 그대로 박혀 브라우저로 전달됩니다. 이 로직 자체는 로그인 여부만 확인하면 되는 기능(예: 검색 자동완성)을 염두에 뒀을 수 있지만, W1의 관문이 바로 이 nonce 하나로 관리자 전용 기능까지 열어주면서 문제가 커졌습니다.

public function init_components() { $this->add_component( 'ajax', new Ajax() ); if ( current_user_can( 'manage_options' ) ) { if ( ! is_customize_preview() ) { $this->add_component( 'finder', new Finder() ); } } $this->add_component( 'connect', new Connect() ); $this->add_component( 'event-tracker', new Event_Tracker() ); }

Elementor 실제 소스 — core/common/app.php · App::init_components() (v3.6.2 태그, 커밋 e66df28f9c7c65b85e3262f4006821621233b876). 58행(Ajax, 무조건 추가)과 60~64행(Finder, manage_options 조건부 추가)의 대비가 핵심입니다.

실제로 App::init_components()를 보면, Elementor 개발자들은 이미 '권한이 필요한 모듈'과 '필요 없는 모듈'을 구분해 코드를 짜 놓았습니다. Ajax 모듈은 조건 없이 추가되고, 그 바로 다음 줄의 Finder(검색) 모듈만 current_user_can( 'manage_options' ) 뒤에 조건부로 추가됩니다. 즉 '권한 게이트가 필요한 자산은 이렇게 막는다'는 패턴이 같은 함수 안에 이미 있었는데, 온보딩 모듈이 기대는 Ajax의 nonce에는 그 패턴이 적용되지 않았습니다.

왜 이렇게 됐나이 코드는 '이 자산이 새어나가면 위험한가'라는 질문에 Finder에는 그렇다고 답하고 Ajax에는 아니라고 답한 결과입니다. Ajax 모듈이 처음 설계될 당시(2.0.0, 2018년경)에는 이 nonce로 열리는 기능들이 대부분 저권한 사용자가 써도 무방한 것들이었을 가능성이 있습니다 — 하지만 이후 온보딩 모듈처럼 이 nonce 하나로 관리자 전용 기능까지 열어주는 코드가 얹히면서, 애초의 '이건 안전하다'는 전제가 조용히 깨졌습니다.

분석 결론

무게 중심은 W1(온보딩 모듈이 관리자 전용 기능의 관문에 권한 검사를 두지 않은 것)입니다 — 이게 없었다면 W2는 무해했습니다.

W2(Ajax 공용 nonce가 모든 로그인 사용자에게 배포되는 것)는 그 자체로는 설계상 결함이 아닙니다 — Ajax 모듈이 처음 만들어질 때 전제했던 위험도로는 합리적이었을 수 있고, 이 nonce에 기대는 다른 코드들은 실제로 저권한 사용자가 써도 되는 기능일 수 있습니다. W1이 이 nonce의 권한 범위를 온보딩의 관리자 전용 기능까지 확장해서 쓰지만 않았다면 문제가 되지 않았습니다.

한 문장으로 묶으면 이렇습니다. '요청이 위조되지 않았다(nonce)'와 '요청자가 이걸 할 자격이 있다(capability)'는 서로 다른 질문인데, 이 코드는 앞의 질문에 대한 답만으로 뒤의 질문까지 답했다고 처리했습니다. 그 틈에서 최하위 권한 계정이 관리자 전용 기능의 문을 그대로 열었습니다.

05공격 과정

5.1 공격 과정 요약

공격 흐름도
공격 흐름도 (직접 작성)

5.2 상세 공격 과정

가장 낮은 진입장벽은 nonce를 손에 넣는 단계입니다. Elementor의 Ajax 모듈이 만드는 elementor_ajax nonce는 관리자 화면을 렌더링할 때마다 elementorCommon이라는 JS 전역 설정 객체 안에 평문으로 들어가는데, 이 객체는 로그인한 사용자의 권한 등급과 무관하게 페이지 소스에 그대로 박힙니다. 즉 공격자는 별도의 취약점 없이, 자기 계정으로 /wp-admin/ 아무 페이지나 열어 소스 보기만 하면 이 값을 얻습니다.

이 nonce를 손에 넣은 뒤에는 /wp-admin/admin-ajax.php로 action=elementor_upload_and_install_pro, _nonce=<획득한 값>, fileToUpload=<zip 파일>을 담아 POST 요청을 보냅니다(공개된 PoC 구조를 개념적으로만 서술 — 실제 요청 형식·구체 파라미터 조합은 이 글에 싣지 않습니다). 이 요청이 admin_init 훅에 도달하면, W1에서 짚은 대로 nonce 검증만 통과하면 그대로 maybe_handle_ajax()가 호출되고, $_POST['action'] 값에 따라 upload_and_install_pro()로 라우팅됩니다.

upload_and_install_pro()는 업로드된 파일을 검증 없이 Plugin_Upgrader 인스턴스의 $upgrader->install( $_FILES['fileToUpload']['tmp_name'], [ 'overwrite_package' => false ] )에 넘깁니다. 이 함수는 워드프레스가 '정식 플러그인 zip 설치'에 쓰는 표준 API로, zip 압축을 풀어 wp-content/plugins/ 아래에 배치합니다. 뒤이어 코드는 그 결과가 성공이면 activate_plugin( WP_PLUGIN_DIR . '/elementor-pro/elementor-pro.php', false, false, true )를 호출해 정확히 이 경로의 파일을 즉시 활성화합니다. 다시 말해 공격자가 만든 zip이 elementor-pro/elementor-pro.php라는 경로 구조와, 워드프레스가 플러그인으로 인식하는 데 필요한 최소한의 헤더 주석만 갖추면, 그 안의 임의 PHP 코드가 활성화 시점에 서버에서 실행됩니다.

이 지점부터는 일반적인 워드프레스 플러그인 백도어 기법(웹셸, 관리자 계정 생성 등)과 다르지 않습니다 — 그 후속 활용 절차는 이 글에서 다루지 않습니다(무해화).

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W2 — Ajax 공용 nonce가 모든 로그인 사용자에게 배포nonce 획득 단계(§8.2 1문단, 다이어그램 A→B)권한 등급과 무관하게 유효한 elementor_ajax nonce 확보W1의 유일한 관문을 통과할 재료 확보
W1 — admin_init 관문이 nonce만 확인하고 capability는 확인 안 함onboarding admin_init 훅 도달 시점(§8.2 2문단, 다이어그램 C→D)관리자 전용 스위치문(maybe_handle_ajax) 전체에 도달elementor_upload_and_install_pro 액션까지 라우팅
(귀결) 검증 없는 플러그인 설치·활성화upload_and_install_pro()의 Plugin_Upgrader::install()·activate_plugin() 호출(§8.2 3문단, 다이어그램 E)위조 zip이 정식 플러그인 경로로 배치·활성화서버에서 임의 PHP 코드 실행(RCE)

06취약점 패치

core/app/modules/onboarding/module.php·Module::__construct() 내부 admin_init 익명 콜백·GitHub elementor/elementor v3.6.3 태그(커밋 557eff68475cc9ff8e05880ff00301418902cddc, 2022-04-12) · NVD가 인용하는 1차 근거는 WordPress.org 플러그인 SVN 체인지셋 2708766(봇 차단으로 직접 열람은 못했으나 동일 버전 범프를 반영한 벤더 배포본으로 대조)
add_action( 'admin_init', function() { if ( wp_doing_ajax() && isset( $_POST['action'] ) && isset( $_POST['_nonce'] ) && wp_verify_nonce( $_POST['_nonce'], Ajax::NONCE_KEY ) wp_verify_nonce( $_POST['_nonce'], Ajax::NONCE_KEY ) && current_user_can( 'manage_options' ) ) { $this->maybe_handle_ajax(); } } );

왜§7 W1에서 지목한 자리 — 관리자 전용 스위치문 전체로 통하는 단 하나의 관문 — 를 정확히 겨냥한 수정입니다. nonce 검증 조건 뒤에 && current_user_can( 'manage_options' )를 추가해, 이 관문을 통과하려면 이제 관리자 권한이 있어야만 하도록 만들었습니다.

어떻게패치는 7개 온보딩 액션마다 개별 권한을 매핑하는 대신, 이 모든 액션이 공유하는 단 하나의 관문에 가장 강한 권한(manage_options, 관리자급) 하나를 요구하는 방식을 택했습니다 — 사이트 이름 바꾸기처럼 상대적으로 가벼운 기능까지도 전부 관리자만 쓸 수 있게 됩니다. 정교함보다 확실함을 택한, 빠르고 거친(coarse) 수정입니다.

비고바로 다음 버전인 3.6.4(GitHub 태그 커밋 d30e5a5860685f2d48ce7278f0d582624af1abee)에서 Elementor는 이 관문을 각 액션에 맞는 개별 capability로 다시 나눴습니다 — 로고 변경은 edit_theme_options, 테마 활성화는 switch_themes, Pro 설치는 install_plugins와 activate_plugins 둘 다로. 또한 사이트 로고 변경 로직에 첨부 ID 존재 확인(wp_get_attachment_url())과 정수 강제(absint())를 추가하는 등 입력 검증도 함께 다듬었습니다. 이건 3.6.3의 수정이 이 CVE를 못 막았다는 뜻이 아니라 — manage_options는 Subscriber를 포함한 모든 저권한 등급을 이미 배제하므로 CVE-2022-1329의 공격 경로는 3.6.3에서 완전히 막힙니다 — 급하게 막은 방어선을 다음 릴리스에서 제품 품질 수준으로 다듬은 후속 조치로 읽는 게 정확합니다.

07대응방안

가장 확실한 조치는 Elementor를 3.6.3 이상으로 업데이트하는 것입니다(§영향받는 버전 표). 이후 릴리스(3.6.4)는 세분화된 권한 검사와 입력 검증을 추가로 포함하므로, 가능하면 3.6.4 이상을 권장합니다.

08왜 이게 반복되는가

이 사건이 흥미로운 이유는, 취약해진 코드가 새로 짠 복잡한 로직이 아니라 워드프레스에서 가장 기본적인 관용구(nonce + capability)의 절반만 가져다 썼다는 점입니다. 저희가 Jenkins CLI 임의 파일 읽기(CVE-2024-23897)에서 짚은 것과 뿌리가 비슷합니다 — 거기서는 '로컬 신뢰를 전제로 설계된 인자 파서'가 원격 노출 컨텍스트로 옮겨지며 인가 검사가 다시 검토되지 않았고, 여기서는 'CSRF 방지용 nonce'가 '신원·권한 증명'으로 슬쩍 격상돼 쓰였습니다. WSO2 API Manager의 JWT 서명검증 우회 사례에서도 '검증하는 코드가 있다'는 사실 자체가 '검증이 실제로 인가 기능을 한다'는 보장이 아니었다는 같은 계열의 교훈을 다뤘습니다 — 세 사건 모두 검증 로직이 존재는 했지만, 그 검증이 실제로 막아야 할 질문을 막지 못했습니다.

패치완전성 관점에서도 이 사건은 배울 점이 있습니다. 3.6.3의 수정은 정교하지 않았습니다 — 7개 액션을 하나의 최고 권한으로 뭉뚱그렸으니까요. 하지만 그 거칢이 오히려 안전한 방향의 거칢이었습니다(과잉 차단이지 과소 차단이 아니었습니다). 다음 릴리스에서 세분화하며 코드 품질을 다듬은 것은 '급한 불부터 확실하게 끄고, 그다음에 정교하게 다듬는다'는 순서가 실제로 안전하게 작동한 사례로 볼 수 있습니다.

이 한 틈은 고쳐졌지만, '검증 코드가 있다'와 '그 검증이 우리가 막으려던 질문을 실제로 막는다'는 서로 다른 문장이라는 확인은 다른 플러그인, 다른 프레임워크에서도 계속 나옵니다 — 그래서 저희는 이 커밋 하나가 아니라 이 질문을 남깁니다.

09결론

Elementor 온보딩 모듈의 admin_init 관문이 nonce 검증만 하고 current_user_can() 권한 검사를 하지 않은 것이 근본원인입니다(W1). 그 nonce 자체가 권한과 무관하게 모든 로그인 사용자에게 배포되고 있었던 것(W2)이 이 결함의 진입장벽을 최하위 권한 계정 수준까지 낮췄습니다. 두 미흡점이 만나 최하위 권한 Subscriber 계정이 elementor_upload_and_install_pro 액션으로 위조 플러그인을 설치·활성화해 서버에서 임의 PHP 코드를 실행할 수 있었습니다. CVSS 8.8(NVD), CWE-862 + CWE-434이며, 3.6.3 패치는 이 공격 경로를 완전히 닫은 것으로 확인됩니다.

이 CVE의 표준판(요약본)은 아직 별도로 작성되지 않았습니다.

참고 자료

이 글의 코드 발췌(core/app/modules/onboarding/module.php의 Module::__construct()·maybe_handle_ajax()·upload_and_install_pro(), core/common/app.php의 App::init_components(), core/common/modules/ajax/module.php의 NONCE_KEY·create_nonce()·get_init_settings())는 elementor/elementor GitHub 저장소(벤더 본인이 운영하는 공식 배포 미러)의 커밋 e66df28f9c7c65b85e3262f4006821621233b876(v3.6.2 태그, 패치 전)·557eff68475cc9ff8e05880ff00301418902cddc(v3.6.3 태그, 패치 후)·d30e5a5860685f2d48ce7278f0d582624af1abee(v3.6.4 태그, 후속 강화)를 얕은 클론(shallow clone)으로 직접 받아 파일·라인 단위로 대조했습니다. NVD가 1차 패치 근거로 인용하는 WordPress.org 플러그인 SVN 체인지셋(2708766)은 봇 차단 챌린지(HTTP 403)로 직접 열람하지 못했으나, elementor/elementor GitHub 태그 간 diff는 동일한 버전 범프(3.6.2→3.6.3)를 반영한 벤더 자체 배포본이므로 이 한계를 안고 코드 사실을 기술했습니다. CVSS·CWE·영향 버전·공개일은 NVD API(services.nvd.nist.gov)에서 직접 조회했습니다. 근본원인(W1·W2) 자체는 Wordfence의 서술에 의존하지 않고 Elementor 자체 오픈소스(위 커밋들)를 직접 읽어 저희가 독립적으로 재구성·확증한 것입니다 — core/common/app.php의 Ajax 모듈 무조건 로드 vs Finder 모듈 조건부 로드 대비(W2)는 저희가 코드를 직접 대조해 처음 짚어낸 지점으로, Wordfence 블로그가 명시적으로 언급한 부분이 아닙니다. 반면 Wordfence 블로그·자사 threat-intel 페이지 원문은 봇 차단(자동화 접근 차단)으로 저희가 직접 열람하지 못했습니다 — 그 사이트가 서술한다고 알려진 내용(nonce가 모든 인증 사용자에게 노출된다는 설명, 발견자 이름, 자체 CVSS 9.9 산정)은 여러 검색엔진 결과·2차 집계 사이트(SentinelOne 등)의 교차 인용으로만 확인했으며, 원문 byte-level 대조는 하지 못했습니다 — 그중 CVSS 9.9 수치는 이 한계 때문에 §CVSS 절에서 미검증으로 명시하고 본문에서는 NVD 확인값(8.8)만 공식값으로 씁니다. 실제 공격에 쓰이는 zip 파일 구조(elementor-pro 폴더명·플러그인 헤더 요건)는 공개 PoC(exploit-db #50882)에 대한 설명을 Elementor 소스코드(upload_and_install_pro())와 교차 대조한 것이며, 저희가 그 PoC를 직접 실행해 재현하지는 않았습니다 — 이 한계는 §패치 완전성 평가·본문 배지에도 명시했습니다.