
CVE-2026-18322에 대한 근본 원인 분석, 수동 버전 검사기, 그리고 실습용 PoC로, Supsystic의 Smart Popup WordPress 플러그인에서 발생하는 인증되지 않은 권한 상승 취약점입니다.
WordPress 플러그인 Smart Popup by Supsystic(popup-by-supsystic)의 인증되지 않은 권한 상승 취약점인 CVE-2026-18322에 대한 보안 연구입니다. 이 저장소에는 업스트림 소스 diff에서 도출한 근본 원인 분석, 추출된 패치, 영향을 받는 설치를 식별하기 위한 탐지 도구(수동 버전 핑거프린터), 그리고 익스플로잇 PoC를 위한 랩이 포함되어 있습니다.
이 취약점은 공개되었으며 패치되었습니다. 이 작업은 방어 목적으로 게시됩니다: 운영자가 영향을 받는 호스트를 찾아 수정할 수 있도록 돕기 위함입니다.
| 버전 범위 | 상태 |
|---|---|
< 1.13.0 | 취약함 |
>= 1.13.0 | 수정됨 |
1.13.0(2026년 7월 31일 릴리스)이 첫 번째 패치 릴리스이며, 아래에 설명된 체인의 세 고리를 모두 복구합니다. 분석 §1을 참조하세요.
세 가지 독립적인 약점이 결합하여 인증되지 않은 관리자 생성으로 이어집니다:
1. 권한 맵 충돌 — havePermissions()가 두 개의 권한 맵을 array_merge()로 결합했습니다. 둘 다 문자열 키 PPS_USERLEVELS('userlevels')를 사용하며, array_merge()는 문자열 키를 덮어쓰기하므로, 기본 컨트롤러의 짧은 기본 목록이 팝업 모듈의 목록을 통째로 대체했습니다 — 이로 인해 save 및 기타 9개 메서드에서 관리자 제한이 조용히 제거되었습니다:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
이 검사는 열린 상태로 실패합니다: 맵에 없는 액션은 결코 거부되지 않습니다.
2. 낯선 사람에게 메일로 발송되는 재사용 가능한 nonce — save는 여전히 pps_nonce를 요구했지만, 구독 확인 이메일에 바로 그 nonce 액션이 포함되어 있었습니다. 로그아웃된 사용자의 경우 WordPress nonce는 uid=0을 기준으로 키가 생성되므로, 익명 구독자를 위해 발급된 토큰이 모든 익명 공격자에게 유효합니다.
3. 서버 측 역할 허용 목록 없음 — createWpSubscriber()는 구성된 역할을 그대로 WP_User::set_role()에 전달했습니다. 플러그인의 안전한 역할 목록(administrator 제외)은 관리자 드롭다운을 렌더링할 때만 적용되었습니다 — UI 전용 제어였습니다.
연결하면: 구독하여 nonce 획득 → popup::save에 재전송하여 sub_wp_create_user_role=administrator 설정 → 구독 흐름 트리거 → 영구 관리자 계정 생성.
파일/줄 참조가 포함된 전체 설명: docs/ANALYSIS.md.
poc/cve_2026_18322_check.py는 사이트가 영향을 받는 버전을 실행 중인지 판별합니다. 익스플로잇을 시도하지 않습니다: 일반 HTTP GET을 보내고 설치가 스스로 공개하는 버전을 읽습니다.
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests는 권장되지만 선택 사항입니다 — 도구는 표준 라이브러리로 폴백합니다.
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
대상은 인수로 주거나 -f로 줄 수 있습니다(-는 stdin을 읽음). 호스트 이름만 주어지면 https://로 간주합니다.
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
종료 코드: 0 = 취약한 대상 없음, 1 = 최소 하나 취약함, 2 = 사용법 오류.
두 가지 수동 신호, 둘 다 공개 리소스에 대한 일반 HTTP GET입니다:
?ver=PPS_VERSION으로 enqueue하므로(classes/frame.php:410,473), 홈페이지가 종종 정확한 버전을 노출합니다.readme.txt — Stable tag:가 권위 있으며 잠재적으로 오래된 캐시된 에셋보다 우선합니다; 변경 로그는 폴백으로 사용됩니다.홈페이지 스캔은 또한 이름이 변경된 wp-content 디렉터리(예: Bedrock의 /app/)를 발견하므로 readme 조회가 사이트의 실제 레이아웃을 따릅니다.
탐지 도구는 익스플로잇을 시도하지 않습니다. 이 CVE에 대한 무기화된 익스플로잇은 여기에 게시되지 않습니다: 전체 체인을 실행하는 유일한 코드는 로컬 랩에 하드 게이트되어 있습니다(아래 참조).
수동 검사기는 버전 핑거프린팅만 수행합니다. 그 HttpClient는 GET 외의 HTTP 메서드를 노출하지 않습니다 — 구조적으로 강제되며 테스트 스위트에서 검증됩니다. 따라서 실수로도 상태를 변경하는 요청을 보낼 수 없습니다. popup::save를 호출하거나, sub_wp_create_user_role 파라미터를 보내거나, 구독 양식을 제출하거나, 확인 이메일을 트리거하거나, 사용자, 팝업 또는 설정을 생성하거나 수정하지 않습니다. 두 개의 공개 리소스 — 홈페이지와 readme.txt — 만 읽고 그 외에는 아무것도 하지 않습니다.
그 안전성에는 대가가 따르며, 솔직히 말하면: 판정은 호스트가 게시하는 버전 메타데이터만큼만 좋습니다. 트레이드오프와 실패 모드: §9.
lab/exploit_full_chain.py는 여기서 실제 공격을 수행하는 유일한 코드 조각이며, WordPress 관리자를 생성합니다. 이는 분석을 신뢰로 받아들이는 대신 재현할 수 있도록 게시됩니다 — 입증할 수 없는 권한 부여 버그에 대한 주장은 발견이 아니라 단언입니다. 이는 lab/의 일회용 스택을 위해 작성되었으며 그 외에는 아무것도 아닙니다:
--lab-confirm은 필수입니다. 두 검사 모두 첫 HTTP 요청 전에 assert_lab_target()에서 실행되므로, 거부된 호출은 대상을 전혀 건드리지 않습니다.http://localhost:8025/api/v1/…)를 통해 구독 확인 이메일에서 재사용 가능한 pps_nonce를 수집합니다. 그 이메일을 다른 곳에서 검색하는 코드 경로는 없으므로, 체인은 랩 외부의 호스트에 대해 첫 단계가 없습니다.lab/docker-compose.yml의 모든 서비스는 127.0.0.1에 바인딩됩니다.따라서 배포된 그대로 실제 사이트를 겨냥할 수 없습니다 — 요청을 보내기 전에 REFUSING TO RUN에서 멈춥니다:
$ python3 exploit_full_chain.py https://example.com --lab-confirm
REFUSING TO RUN: example.com resolves to non-local address(es) 104.20.23.154, ...
This script creates a WordPress administrator and is for the local lab only.
To test a real site, use the non-destructive checker in ../poc/.
이 가드들은 장식이 아니라 하중을 지탱합니다: 스크립트는 실제 체인이며, 이를 시연으로 유지하는 것은 일회용 로컬 스택 외부에서는 실행되지 않는다는 점입니다. 제거하지 마시고 SECURITY.md를 참조하세요 — 이를 약화시키는 변경이나 랩 외부에서 실행되도록 의도된 독립형 익스플로잇은 이 저장소의 범위를 벗어납니다. 실제 사이트가 영향을 받는지 확인하려면 poc/를 사용하세요 — 검사기는 아무것도 건드리지 않고 같은 질문에 답합니다.
통제하는 호스트에 대한 권위 있는 답변을 위해:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
출력이 있으면 호스트가 취약합니다 — 그 줄이 바로 1.13.0이 교체한 것입니다.
소유하거나 명시적으로 테스트 권한을 받은 시스템에만 사용하세요. SECURITY.md를 참조하세요.
lab/에는 취약점을 엔드투엔드로 실행하는 일회용 Docker 스택이 있어 분석을 신뢰로 받아들이는 대신 검증할 수 있습니다:
cd lab
./setup.sh # WordPress + plugin 1.12.0
../.venv/bin/python exploit_full_chain.py http://localhost:8080 --lab-confirm
./verify.sh # confirm from the database
./teardown.sh
가장 유용한 출력은 버전 비교입니다 — 세 릴리스에 대한 동일한 요청:
./setup.sh --plugin-version 1.11.2 # exploit succeeds
./setup.sh --plugin-version 1.12.0 # exploit succeeds
./setup.sh --plugin-version 1.13.0 # exploit fails at phase 2
랩은 의도적으로 취약하며 그 익스플로잇 스크립트는 WordPress 관리자를 생성합니다. 모든 것이
127.0.0.1에 바인딩되며, 스크립트는 로컬 랩 외에는 실행되지 않습니다 — 랩 익스플로잇은 랩에서만 실행됩니다를 참조하세요. 스캐너가 아닙니다; 실제 사이트를 확인하려면poc/를 사용하세요.
sub_wp_create_user_role이 권한 있는 역할로 설정된 팝업, 그리고 pl=pps&mod=popup&action=save를 포함하여 admin-ajax.php로 가는 POST 요청.쿼리 및 로그 grep: §10.
.
├── README.md
├── SECURITY.md Scope, authorized-use policy, disclosure
├── LICENSE MIT
├── requirements.txt
├── docs/
│ └── ANALYSIS.md Full root-cause analysis and fix rationale
├── patches/
│ ├── 01-frame-permission-merge.diff array_merge() → _mergePermissions()
│ ├── 02-subscribe-role-allowlist-and-nonce.diff Role allowlist + dedicated nonce
│ └── 03-v1.12.0-confirmation-page-hardening.diff Unrelated hardening added in 1.12.0
├── poc/
│ └── cve_2026_18322_check.py Passive version fingerprint (GET only)
├── lab/ Disposable Docker lab — INTENTIONALLY VULNERABLE
│ ├── docker-compose.yml MariaDB + WordPress 1.12.0 + Mailpit (loopback only)
│ ├── setup.sh / teardown.sh Bring the lab up / destroy it
│ ├── exploit_full_chain.py Full attack chain — CREATES AN ADMIN; lab-gated
│ └── verify.sh Independent confirmation from the database
├── scripts/
│ └── fetch_versions.sh Export plugin versions from SVN and regenerate diffs
└── tests/
└── test_check.py Offline unit tests for the passive checker
pip install -r requirements.txt pytest
python3 -m pytest tests/ -v
스위트는 완전히 오프라인입니다 — 기록된 픽스처를 사용하며 라이브 대상을 사용하지 않습니다. 1.13.0 경계를 넘는 버전 분류, 대상 정규화, 그리고 각 상태를 결정하는 증거 규칙을 다룹니다.
안전 속성은 문서화될 뿐만 아니라 검증됩니다: 전체 스캔이 상태를 변경하는 요청을 발행하지 않으며, HttpClient가 GET 외의 HTTP 메서드를 노출하지 않는다는 것입니다.
개발 중 발견된 두 가지 회귀가 고정되어 있습니다: 줄 앵커 파싱을 무력화하는 CRLF readme.txt, 그리고 HTTP 오류 페이지가 플러그인 증거로 잘못 계산되는 문제.
업스트림 소스에서 분석을 재현하려면:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — LICENSE를 참조하세요. 분석된 플러그인은 GPLv2-or-later이며 여기서 재배포되지 않습니다; scripts/fetch_versions.sh가 공식 WordPress SVN 저장소에서 가져옵니다.
| 상태 | 의미 |
|---|
VULNERABLE | 플러그인이 < 1.13.0에서 탐지됨 |
NOT_VULNERABLE | 플러그인이 >= 1.13.0에서 탐지됨 |
PLUGIN_DETECTED_VERSION_UNKNOWN | 플러그인 존재, 버전 확인 불가 — 수동으로 확인 |
PLUGIN_NOT_DETECTED | 플러그인의 증거 없음(부재의 증명은 아님) |
ERROR | 대상에 연결할 수 없거나 형식이 잘못됨 |