CVE-2026-18351 (Elementor Forms 업로드 플러그인 무인증 파일 업로드 RCE) 분석

분류: CVE · 웹·서버 · RCE · CVE-2026-18351 · [표준]

파일 업로드 필터는 대개 “이 확장자는 허용, 저 확장자는 금지”라는 목록 하나에 기댑니다. 문제는 그 목록을 만드는 방식이 느슨할 때 생깁니다. WordPress용 “Drag and Drop File Upload for Elementor Forms” 플러그인은 어떤 확장자를 허용할지를 요청 파라미터로 받아 목록에 넣었고(공식 advisory 기준 type 파라미터가 MIME 허용목록 키로 쓰이는 점이 근본), 그 목록을 만드는 절차의 빈틈이 겹쳐 검사를 우회할 수 있었습니다. 그 결과 로그인하지 않은 공격자가 PHP 파일을 올려 서버에서 코드를 실행할 수 있었습니다.

개요

“Drag and Drop File Upload for Elementor Forms”는 Elementor 폼에 드래그앤드롭 파일 첨부 기능을 붙여 주는 WordPress 플러그인입니다(벤더 슬러그 addonsorg). 이번 취약점은 방문자가 첨부 파일을 올릴 때 파일 종류가 안전한지 판단하는 is_file_type_valid() 함수의 검증이 불충분한 데서 비롯됩니다. 무인증(pre-auth) 원격 공격자가 실행 가능한 파일(PHP)을 업로드해 서버에서 임의 코드를 실행할 수 있어, 사이트 전체 장악으로 이어집니다.

근본은 위험한 타입의 파일을 제한 없이 업로드(CWE-434, Unrestricted Upload of File with Dangerous Type)하는 결함입니다. ⚠️미확인(팩트체크 대상) — CWE 매핑은 리서치 재료(집계 사이트) 기준이며 NVD·Wordfence 원본 대조 대상입니다.

  • 영향 범위: 1.6.0 이하 전체(<= 1.6.0). ⚠️미확인 — 공개 excerpt·집계 사이트 기준이나, NVD·Wordfence 원본 대조가 필요합니다.
  • 수정 버전: 1.6.1. ⚠️미확인 — 특히 재확인 필요. 이 값은 집계 텍스트와 WordPress SVN에 tags/1.6.1 태그가 존재한다는 사실로 뒷받침되지만, NVD·Wordfence 원문 페이지에서 직접 읽어 확인되지는 않았습니다(리서치 단계에서 두 원본 모두 로드 실패). 패치 changeset은 r3628051로 확인됩니다(⚠️미확인).
  • CVSS: 3.1 기준 9.8 (Critical), 벡터 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. ⚠️미확인 — 이 값은 CNA인 Wordfence([email protected]) 산정값으로 보이며(NVD 자체 산정값이 아님), 집계 사이트를 거쳐 옮겨졌으므로 CNA 원문과의 직접 대조가 필요합니다.
  • 취약 함수/파일: is_file_type_valid() (파일 fields/file_upload.php), 진입점은 AJAX 핸들러 elementor_file_upload. ⚠️미확인 — 파일·함수명은 리서치 재료가 WordPress.org SVN 원문에서 확인했다고 보고했으나, 본 워커는 네트워크가 없어 직접 대조하지 않았습니다. 정확한 라인번호는 출처마다 달라(일부는 :833, 다른 곳은 500번대) 이 글은 특정 라인번호를 단정하지 않습니다.
  • 인증 요구: 없음(pre-auth), 원격 — 공식 설명도 무인증(PR:N) 공격 가능으로 봅니다. 핸들러가 nonce를 확인하지만, CVSS가 PR:N인 점은 이 nonce가 공개 폼 페이지에서 얻어지는(이 부류 취약점의 전형) 것과 정합적입니다. ⚠️미확인 — 정확한 nonce 노출 경로는 소스에서 직접 확인되지 않았으므로, 메커니즘은 소스 대조 전까지 개념 수준으로만 서술합니다.
  • 크레딧: CNA는 Wordfence Threat Intelligence이며, 발견자(finder)는 집계 출처(CVE.report·OpenCVE·Patchstack)상 Adam Rayyan Aryasatya로 표기됩니다. ⚠️미확인 — 발견자 이름은 집계 출처 기준이라 CNA 원문 크레딧과의 직접 대조가 필요합니다.

한 줄 요약: “허용 확장자 목록을 공격자 입력으로 만들면서, 목록 구성 절차의 빈틈(리서치 재료가 본 1.6.0 소스 기준으로는 부분 문자열 매칭·octet-stream 기본값·검사 순서)이 겹치면, 블랙리스트에 든 .php도 검문을 통과할 수 있다” — 검증의 결론이 아니라 목록을 만드는 절차 자체가 오염된 것이 핵심입니다. (공식 advisory는 이를 type 파라미터가 MIME 허용목록 키로 쓰이는 결함으로 서술 — 아래 §정직한 한계 참조.)

기술 배경 — WordPress에서 파일 업로드를 지키는 방식

WordPress에서 임의 파일 업로드가 곧바로 서버 장악(RCE)으로 이어지는 이유는, 업로드된 파일이 대개 웹으로 접근 가능한 디렉터리(wp-content/uploads)에 저장되기 때문입니다. 공격자가 .php 파일을 그곳에 올려두면, 그 URL을 직접 요청하는 것만으로 서버가 그 코드를 실행합니다. 그래서 “무엇을 저장하게 두느냐”가 결정적입니다.

이를 막는 표준 장치는 두 가지입니다. 확장자·MIME 허용 목록(allowlist)과 위험 확장자 차단 목록(blacklist)입니다. 플러그인은 보통 허용된 타입만 통과시키고, 그중에서도 .php·.phtml·.htaccess 같은 실행 가능/위험 확장자는 별도 블랙리스트로 한 번 더 거릅니다. 여기에 WordPress 내장 함수 wp_check_filetype_and_ext()(파일 내용·확장자로 실제 MIME을 판정)와 sanitize_file_name()(파일명 정규화)이 보조로 쓰입니다.

이 장치들은 “허용 목록이 좁고 정확하며, 블랙리스트 검사가 최종적으로 저장될 파일명 기준으로 이뤄질 때”만 제대로 동작합니다. 이번 취약점은 그 두 전제가 모두 흔들린 경우입니다.

CVE 분석 요약 — 어느 판단이 원인인가

이 플러그인은 wordpress.org의 공개 오픈소스입니다(SVN 저장소). 리서치 재료가 취약 버전(tags/1.6.0)과 패치 버전(tags/1.6.1)의 fields/file_upload.php를 원문으로 확보했다고 보고했습니다. 다만 본 워커는 네트워크가 없어 그 원문을 직접 대조하지 못했으므로, 아래 서술과 코드는 리서치 재료가 전한 내용의 재구성이며 라인번호를 단정하지 않습니다(⚠️미확인 — 팩트체커가 SVN 원문에서 byte-level 대조).

리서치 재료 기준, is_file_type_valid()의 허용 목록 구성 절차에 여러 빈틈이 겹칩니다.

  • 허용 확장자를 요청 파라미터로 받는다. 어떤 타입을 허용할지가 요청의 type 값(그리고 폼 설정)에서 오고, 이 문자열이 그대로 허용 목록의 씨앗이 됩니다. 공격자가 허용 집합에 영향을 줄 수 있는 구조입니다.
  • 매칭이 느슨해 목록이 부풀 수 있다(⚠️미확인·상충). 리서치 재료는 요청한 확장자를 WordPress의 MIME 목록과 대조할 때 정확 일치가 아니라 strpos()(부분 포함) 검사를 쓴다고 전했습니다(짧은 조각이 여러 MIME 키에 부분 매칭되어 의도보다 넓은 타입이 섞여 들어가는 구조). 반면 공식 advisory는 type 파라미터가 MIME 허용목록의 “정규식 키”로 쓰인다고 서술합니다 — 두 설명은 상충하므로 어느 쪽이든 SVN 원문 대조 전까지 단정하지 않습니다. 공통분모는 “매칭이 느슨해 의도보다 넓은 타입이 허용 목록에 섞인다”는 방향뿐입니다.
  • 모르는 확장자를 octet-stream으로 눙쳐서 통과시킨다. MIME 목록에서 못 찾은 확장자는 거부되는 대신 application/octet-stream이라는 기본값을 붙여 허용 목록에 그대로 추가됩니다. 즉 “모르는 타입 = 거부”가 아니라 “모르는 타입 = 일단 허용”에 가깝게 동작합니다.
  • 블랙리스트 검사 시점의 빈틈. .php 등은 블랙리스트로 거르지만, 검사는 wp_check_filetype_and_ext()가 돌려준 확장자를 기준으로 합니다. 한편 실제 저장 파일명은 sanitize_file_name()으로 정규화됩니다. 이 “검사한 확장자”와 “정규화 뒤 최종 확장자”가 어긋날 수 있는 지점이, 블랙리스트를 지나쳐 실행 가능한 파일이 저장되는 통로가 됩니다.

⚠️정직한 한계 — 정확한 익스플로잇 프리미티브는 1차출처로 고정되지 않았습니다. 공식 advisory 텍스트는 type 파라미터가 MIME 허용목록의 “정규식 키”로 쓰인다고 표현합니다. 반면 리서치 재료는 1.6.0 코드의 허용 목록 구성 루프가 정규식(preg_match)이 아니라 부분 문자열(strpos) 검사였다고 전했으나(정규식은 이중 확장자 검사 /\.[a-z0-9]+\.([a-z0-9]+)$/i에 등장한다고 보고), 본 워커는 SVN 원문을 직접 대조하지 못했고 두 설명이 상충하므로 어느 쪽도 확정하지 않습니다. 나아가 위에서 서술한 application/octet-stream 기본 허용 폴백·검사 순서·원본↔정규화 파일명 불일치 사슬은 공개 CNA/Wordfence 설명에는 등장하지 않는 코드 세부로, 오직 리서치 재료의 재구성일 뿐 SVN diff로 확인되지 않았습니다. 어떤 type 값 + 어떤 파일명 조합이 최종적으로 저장된 .php로 귀결되는지의 정확한 사슬은 개념적으로만 서술하며, 팩트체커가 SVN 원문에서 확정해야 합니다. 완성형 페이로드는 싣지 않습니다.

아래는 그 구조를 개념적으로 재구성한 것입니다. 실제 소스가 아니며 라인번호를 주장하지 않습니다.

// 개념용 단순화, 실제 페이로드/원본 코드 아님 (SVN 원문 미대조, 라인번호 미주장)
function is_file_type_valid($file_types, $file) {
    // (1) 허용 확장자 목록의 씨앗이 요청/폼 설정에서 온다
    $allowed = explode(',', strtolower($file_types));
    $custom_allowed_mimes = [];

    foreach ($allowed as $ext) {
        $found = false;
        foreach (get_allowed_mime_types() as $mime_ext => $mime_type) {
            // (2) '정확히 일치'가 아니라 '부분 포함' → 목록이 의도보다 넓어짐
            if (strpos($mime_ext, $ext) !== false) {
                $custom_allowed_mimes[$mime_ext] = $mime_type;
                $found = true;
            }
        }
        // (3) 못 찾은 확장자도 거부하지 않고 octet-stream으로 허용에 추가
        if (!$found) {
            $custom_allowed_mimes[$ext] = 'application/octet-stream';
        }
    }

    // (4) 블랙리스트 검사는 wp_check_filetype_and_ext() 결과 기준.
    //     실제 저장명은 sanitize_file_name()으로 정규화되므로,
    //     '검사한 확장자'와 '최종 확장자'가 어긋날 수 있다 → 우회 통로
    $checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $custom_allowed_mimes);
    return !in_array(strtolower($checked['ext']), get_blacklist_file_ext(), true);
}

패치(요지, ⚠️미확인 — SVN r3628051 = 1.6.1로 보고됨): 리서치 재료가 전한 수정의 방향은 이 빈틈들을 하나씩 막습니다. (a) 부분 문자열 strpos()를 정확한 토큰 일치(in_array)로 교체해 목록 오염을 차단하고, (b) 요청 확장자에 영숫자만 허용하는 필터를 걸어 메타문자 주입을 막고, (c) 원본 파일명과 sanitize_file_name()으로 정규화한 파일명 양쪽을 모두 블랙리스트와 대조하며, (d) application/octet-stream 기본 허용 폴백을 제거해 모르는 확장자가 조용히 목록에 끼지 못하게 합니다. 네 갈래 모두 “허용 목록을 만드는 절차”를 조이는 방향으로, (이 요약이 맞다면) 근본원인이 목록 구성 로직에 있었음을 시사합니다. 다만 이 네 가지 변경 요약 자체가 r3628051 diff를 직접 대조해 확인된 것이 아니라 리서치 재료가 전한 재구성이므로, 패치의 실제 내용은 팩트체커의 diff 대조 전까지 확정이 아닙니다.

공격 시나리오 · 영향도 + 대응방안

공격 시나리오 (단계 압축, 완성형 페이로드 없음):

  1. 공격자가 이 플러그인의 드래그드롭 업로드 필드를 쓰는 Elementor 폼을 찾습니다(로그인 불필요 — CVSS PR:N). 핸들러는 nonce를 요구하지만 무인증 공격이 가능한 것으로 평가되며, 그 nonce가 구체적으로 어떤 경로로 확보되는지는 소스에서 확인되지 않아 여기서 단정하지 않습니다(⚠️미확인).
  2. 파일 종류 검증(is_file_type_valid())의 빈틈을 노려, 허용 목록 구성이 느슨해지도록 요청을 구성한 뒤 서버에서 실행 가능한 파일(예: PHP 스크립트)을 실어 업로드합니다.
  3. 파일이 웹 접근 가능한 업로드 디렉터리에 저장되면, 공격자는 그 파일 URL을 직접 요청해 서버에서 코드를 실행합니다.

영향도: 확인된 성격은 무인증 원격 공격자의 임의 코드 실행(RCE)입니다(CVSS 기밀성·무결성·가용성 모두 High). 웹셸이 심기면 사이트 콘텐츠·데이터베이스·서버 파일에 대한 폭넓은 제어가 가능하므로 사실상 전체 침해로 이어집니다. 다만 실제 노출은 사이트가 이 플러그인의 업로드 필드를 실제로 게시했는지에 달려 있습니다. 한 집계 출처가 “활발한 악용 정황”을 언급했으나 CISA KEV 등 1차 트래커로 확인되지 않았으므로 사실로 단정하지 않습니다(⚠️미확인). 공개 PoC/익스플로잇 저장소가 존재하는 것으로 확인됩니다(References [7]·[8] — 저장소의 존재만 확인하며 페이로드는 반입하지 않습니다). 다만 저장소 존재가 곧 대규모 악용을 뜻하지는 않으며, 실제 관측 여부는 본 리서치 범위에서 확인되지 않았습니다(⚠️미확인).

대응방안:

  1. 근본: 플러그인을 수정 버전으로 업데이트합니다. 리서치 재료 기준 수정 버전은 1.6.1로 보이나(⚠️미확인·재확인 필요), 업데이트 전 벤더 공지·Wordfence advisory에서 정확한 안전 버전을 확인하는 것이 안전합니다.
  2. 차선(즉시 업데이트 불가 시):
    • 해당 플러그인의 드래그드롭 업로드 필드가 있는 폼을 일시 비활성화하거나 플러그인을 잠시 끕니다.
    • 웹 서버·WAF 규칙으로 업로드 디렉터리(wp-content/uploads)에서 PHP 실행을 차단합니다(디렉터리 내 .php·.phtml 요청 거부). 파일이 올라가도 실행되지 않으면 RCE로 이어지지 않습니다.
    • 업로드 처리 엔드포인트(AJAX elementor_file_upload)로 향하는 요청에 접근 제어·레이트리밋을 걸어 노출을 줄입니다.
  3. 탐지 관점: 업로드 디렉터리에 새로 생긴 .php(및 실행 가능 확장자) 파일과, 업로드 직후 그 파일 URL로 곧장 들어오는 요청을 감시합니다. 후자는 웹셸 실행 시도의 신호일 수 있습니다. 업로드 엔드포인트로 향하는 비정상 멀티파트 요청·비정상 type 파라미터 값도 함께 봅니다.

결론

업로드 필터의 진짜 급소는 “무엇을 거부하나”라는 목록이 아니라, 그 목록을 누가·어떻게 만드느냐입니다. 허용 목록의 씨앗을 공격자에게 받아 부분 매칭으로 부풀리고 모르는 것까지 눙쳐 넣으면, 아무리 꼼꼼한 블랙리스트도 그 위에서 헛돕니다.

이 부류의 사고가 반복되는 이유는 분명합니다. 파일 타입 검증이 “결론(이 파일은 안전한가)“이 아니라 “절차(허용 목록을 어떻게 구성하는가)“에서 무너지기 때문입니다. 부분 문자열 매칭, “모르면 일단 허용” 기본값, 그리고 “검사한 이름”과 “저장할 이름”의 불일치는 각각은 사소해 보여도 겹치면 무인증 RCE의 조건이 됩니다. 이 결함은 우리가 다룬 다른 무제한 파일 업로드(CWE-434) 계열, 그리고 안전하지 않은 기본값·정규화 전후 불일치(검사 대상과 실제 사용 대상이 다른 TOCTOU류) 계열과 같은 뿌리를 공유합니다. 정석은 허용 목록을 정확 일치로 좁게 유지하고, 모르는 타입은 기본 거부하며, 최종적으로 저장될 파일명 기준으로 블랙리스트를 검사하고, 저장 위치에서 실행 자체를 막는 다층 방어입니다. 이 플러그인의 한 틈은 업데이트로 닫히겠지만, “허용 목록을 만드는 절차를 공격자에게 내주지 말라”는 교훈은 다음 업로드 처리기에서 또 시험대에 오릅니다. 그래서 우리는 도구가 아니라 방법을 남깁니다.

References / 참고자료

  1. NVD, “CVE-2026-18351 Detail” — NVD 페이지. 다만 취약점 설명·영향 버전·CWE·CVSS의 1차 산정 주체는 CNA인 Wordfence이며, NVD는 그 값을 수신·표시하는 성격입니다(NVD 자체 산정 CVSS가 아님). 즉 1차 정본은 아래 [5] Wordfence advisory 쪽이고, 이 페이지는 그 미러/재게시로 봅니다. 리서치 단계에서 본문 로드에 실패해 미획득, 팩트체크 재확인 대상. https://nvd.nist.gov/vuln/detail/CVE-2026-18351
  2. WordPress.org Plugin SVN — 취약 버전 원문(tags/1.6.0/fields/file_upload.php) — is_file_type_valid()의 취약 로직(부분 매칭·octet-stream 폴백) 근거. 라인번호·byte-level 대조는 팩트체크 대상. https://plugins.svn.wordpress.org/drag-and-drop-file-upload-for-elementor-forms/tags/1.6.0/fields/file_upload.php
  3. WordPress.org Plugin SVN — 패치 버전 원문(tags/1.6.1/fields/file_upload.php) — 수정 방향(정확 일치·영숫자 필터·원본/정규화 이중 검사·폴백 제거) 근거. https://plugins.svn.wordpress.org/drag-and-drop-file-upload-for-elementor-forms/tags/1.6.1/fields/file_upload.php
  4. WordPress Plugin Trac — Changeset r3628051 — 패치 changeset(1.6.1로 배포된 것으로 보고됨). diff 규모·내용 대조는 팩트체크 대상. https://plugins.trac.wordpress.org/changeset/3628051/drag-and-drop-file-upload-for-elementor-forms
  5. Wordfence Threat Intelligence, CVE-2026-18351 advisory — CNA/어사이너 원문(영향/수정 버전·근본원인·크레딧 정본). 리서치 단계에서 403으로 미획득, 팩트체크 시 직접 대조 필요. https://www.wordfence.com/threat-intel/vulnerabilities/id/CVE-2026-18351
  6. Strix AI, “CVE-2026-18351” 분석 페이지 — CVSS 벡터·changeset·파일 경로 교차 확인용 2차 분석. https://www.strix.ai/cve/CVE-2026-18351
  7. 공개 PoC 저장소 (존재 확인용, 페이로드 미반입) — GitHub: JohenLastGen-JLG/CVE-2026-18351. https://github.com/JohenLastGen-JLG/CVE-2026-18351
  8. 공개 익스플로잇 저장소 (존재 확인용, 페이로드 미반입) — GitHub: ChiefYoru/Exploit-CVE-2026-18351. https://github.com/ChiefYoru/Exploit-CVE-2026-18351

이 글은 위 원 자료를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차는 원문을 참고하세요. 이 플러그인은 wordpress.org 공개 오픈소스지만, 본 초안 워커는 네트워크 접근이 없어 SVN 원문을 직접 대조하지 못했습니다 — 코드는 리서치 재료가 전한 내용의 개념 재구성이며 파일/라인 단정을 하지 않았습니다. ⚠️미확인 표시가 붙은 사실 주장(특히 영향/수정 버전·CWE·CVSS 벡터·근본원인·인용 코드·패치 changeset·크레딧)은 발행 전 독립 팩트체크로 1차출처(NVD·Wordfence·SVN r3628051) 대조가 끝나야 확정됩니다. 정확한 익스플로잇 사슬과 nonce 노출 경로, “활발한 악용” 주장은 확증되지 않아 개념 수준으로만 서술했습니다.