
Reproducible Docker-based proof-of-concept for CVE-2026-19949, a second-order SQL injection in All-in-One WP Migration <= 7.109 that leaks the ai1wm_secret_key via anonymous REST and escalates to remote code execution.
Unauthenticated second-order SQL injection in All-in-One WP Migration and Backup (WordPress)
that escalates to leaking the ai1wm_secret_key and, with it, to remote code execution (RCE).
Read this in: English · Español
| CVE | CVE-2026-19949 |
| Plugin | All-in-One WP Migration and Backup (ServMask), ≤ 7.109 |
| Patch | 7.110 (August 20, 2026) |
| CVSS | 8.8 (High) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| Prerequisite | An administrator exporting and restoring the site (a routine action with this plugin) |
| Researcher | Jack Taylor (Wordfence bug bounty program) |
Educational, training and defensive use only. This material is a resource for internal training in self-owned, isolated labs: classrooms, academies, CTFs and security teams practicing on their own infrastructure. The PoC attacks a WordPress site running on your own machine, inside Docker containers; run it only against that lab or against systems for which you hold explicit written authorization. Do not use it against third-party systems: that is illegal and not authorized.
Scope of use and warning — read this first.
This repository is lab training material. It is meant for studying the bug, reproducing it and verifying the patch on your own machine. Anything beyond that context is the sole responsibility of whoever runs it, and this project neither endorses nor authorizes it.
| Permitted use | Prohibited use |
|---|---|
| Reproducing the PoC in the Docker lab bundled with this repository. | Running the PoC against any site that is not yours. |
| Practicing in classrooms, academies, CTFs and controlled training environments. | Using it against clients, third parties or production systems without written authorization. |
| Auditing your own infrastructure, with explicit permission. | Scanning or exploiting Internet ranges "just to see if they fall". |
| Verifying 7.110 and building detections with the indicators in §13. | Redistributing it as an attack tool or integrating it into an exploitation panel. |
This software is dual-use: the same chain that teaches you to defend also
attacks. That is why the invasive CLI mode requires --acepto-responsabilidad
and an explicit domain confirmation, and why the chain has only ever been tested
against local containers. No warranty is provided and the author disclaims any
liability for misuse, resulting damage or legal consequences (see §16).
If you are after the defensive part, go straight to §13 Detection and mitigation.
The plugin's export/import flow dumps the database to SQL (database.sql
inside the .wpress) and, on import, rewrites every statement with
Ai1wm_Database::replace_table_values() to substitute URLs and table
prefixes. To locate string literals it uses the regex:
// 7.109 (vulnerable) — class-ai1wm-database.php:1637
preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
The problem is the negative lookbehind (?<!\\): it inspects a single byte
before each candidate closing quote instead of counting the full run of
backslashes. In a MySQL dump, data ending in \ is written as ...\\' — a
closing quote preceded by an even number of backslashes (a real string
ending in a backslash) — but the regex believes it is escaped and
over-captures the next literal. The callback then runs
unescape_mysql → replace_serialized_values → escape_mysql over the
over-captured value, and that cycle re-emits an unbalanced quote/backslash
sequence that flips the MySQL string boundary in the resulting statement,
promoting attacker-controlled data into executable SQL.
With the trackback planted (see §4), the row in database.sql reads:
INSERT INTO `…_comments` VALUES (2,4,'Jack Blogs\\\\','','/*payload*/…','172.18.0.1',…);
The author ends in \ at the data level → the dump writes it as \\ → the
closing quote is preceded by an even run. The regex over-captures up to
the opening quote of the next field and the callback re-escapes the whole set:
'Jack Blogs\\\\',' ← over-captured content
unescape → 'Jack Blogs\\',' ← strtr collapses the pairs
escape → 'Jack Blogs\\\\\', ← a NEW escaped quote has appeared
output → 'Jack Blogs\\\\\','…' ← the `\ , '` pair flips the string boundary
From that point on, everything that followed in the statement is re-tokenized with inverted parity (data that was a string becomes code and vice versa). The patched 7.110 regex processes the same line and returns it unchanged.
You can watch it byte by byte, without exploiting anything, with
make demo-flip (exploit/04_demo_flip.php), which runs the REAL 7.109 and
7.110 plugin code over the same dump line.
- $input = preg_replace_callback( "/'(.*?)(?<!\\\\)'/S", array( $this, 'replace_table_values_callback' ), $input );
+ $input = preg_replace_callback( "/'((?:[^'\\\\]++|\\\\.)*+)'/sS", array( $this, 'replace_table_values_callback' ), $input );
The new pattern tokenizes MySQL literals correctly: either a character that is
not a quote/backslash, or an escape pair \x, with possessive quantifiers. An
even backslash run no longer confuses the literal's closing quote.
Lab only. The steps below run against the WordPress Docker instance you bring up yourself at
localhost:8080. Do not reproduce them against someone else's site: doing so is unauthorized-access crime.
The chain published by Wordfence (Sept. 2026) has four steps: