[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-93484":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":16,"stars7d":17,"stars30d":17,"stars90d":15,"forks30d":15,"starsTrendScore":18,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":21,"hasPages":21,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":25,"readmeContent":26,"aiSummary":10,"trendingCount":15,"starSnapshotCount":15,"syncStatus":14,"lastSyncTime":27,"discoverSource":28},93484,"wp2shell-poc","Icex0\u002Fwp2shell-poc","Icex0","wp2shell (CVE-2026-63030) - full RCE chain","",null,"Python",478,112,2,0,52,313,417,96.06,"MIT License",false,"main",[24,5],"wp2shell","2026-07-22 04:02:09","# wp2shell-poc\n\nIndependent proof-of-concept for the unauthenticated WordPress REST batch route-confusion\nSQL injection associated with Searchlight Cyber's wp2shell advisory.\n\nThis repository is not Searchlight Cyber's official checker. `check` confirms the SQLi path,\n`read` demonstrates database read, and `shell` opens a plugin-backed command shell either with\nsupplied administrator credentials or by first exercising the SQLi-to-admin bridge.\n\n![wp2shell — the `shell` command exercising the pre-auth SQLi-to-admin bridge](docs\u002Fshell.svg)\n\n## Affected versions\n\nSearchlight Cyber's advisory lists these wp2shell RCE exposure ranges:\n\n| Version range | Status |\n| ------------- | ------ |\n| \u003C= 6.8.5 | Not affected |\n| 6.9.0 – 6.9.4 | Affected |\n| 7.0.0 – 7.0.1 | Affected |\n\n## How it works\n\nThe REST batch endpoint (`\u002Fbatch\u002Fv1`) is unauthenticated and runs several sub-requests in one\ncall, relying on each sub-request being validated and permission-checked on its own.\n\n`serve_batch_request_v1()` builds two parallel arrays — `$matches` (the matched handler per\nsub-request) and `$validation` (the validation result per sub-request) — then indexes both by\nthe same offset when dispatching. A sub-request whose path fails `wp_parse_url()` is appended to\n`$validation` but not to `$matches`, so the arrays fall out of step and a sub-request is\ndispatched under a **different** sub-request's handler. That is the route confusion.\n\nThe PoC nests the primitive twice:\n\n1. A `POST \u002Fwp\u002Fv2\u002Fposts` request that carries a `requests` body is dispatched under the batch\n   handler itself. Having been validated as a posts request, its `requests` list is never checked\n   against the batch schema, so its sub-requests may use `GET` — the method allow-list is\n   bypassed.\n2. Inside that inner batch, a `GET \u002Fwp\u002Fv2\u002Fposts\u002F999999` item-route request carries posts collection\n   query params such as `author_exclude`, `orderby`, and `per_page`. The `999999` ID does not need\n   to exist; it is just an unlikely post ID used to match the item route, whose schema does not\n   validate those collection-only params. The desync then dispatches the same request under posts\n   `get_items()`, where `author_exclude` maps to the `WP_Query` `author__not_in` query var, which\n   the vulnerable build interpolates into SQL as a string.\n\nThe result is a boolean- and time-based blind SQL injection reachable pre-authentication. This PoC\nalso includes the UNION fake-post primitive used by the SQLi-to-admin chain.\n\nThe RCE path implemented here is:\n\n1. Use UNION fake `wp_posts` rows to render attacker-controlled content through a posts collection.\n   The render bridge uses the `\u002Fwp\u002Fv2\u002Fposts\u002F999999` item-route source — the same route the SQLi read\n   uses to reach `get_items()`.\n2. Use that render to make WordPress create real oEmbed cache posts.\n3. Recover those real cache post IDs through the SQLi.\n4. In one poisoned batch request, recast those IDs as a customizer changeset, navigation item, and\n   request hook shape.\n5. Let the same request reach `POST \u002Fwp\u002Fv2\u002Fusers`, creating a generated administrator.\n6. Log in as that generated administrator and use plugin upload behavior to run a command.\n\nSteps 1–5 are pre-authentication; the command-execution step is authenticated admin plugin upload.\n\n## Requirements\n\nPython 3.8+ and the standard library. No third-party dependencies.\n\n## Usage\n\nRun it from the repository directory:\n\n```\n.\u002Fwp2shell.py \u003Ccommand> \u003Curl> [options]\n```\n\nOr `pip install .` to get a `wp2shell` command on your `PATH`.\n\n### check — non-destructive vulnerability check\n\nPrints passive WordPress markers and public version hints first, then sends a benign batch marker\nprobe. A vulnerable batch implementation returns HTTP 207 with the route-confusion marker pattern\n`parse_path_failed`, `block_cannot_read`, and `rest_batch_not_allowed`.\n\nThe marker probe is based on the WordPress core fix. The malformed `\u002F\u002F\u002F` request creates\n`parse_path_failed`; a `\u002Fwp\u002Fv2\u002Fposts` request acts as a batch-allowed spacer; the\n`\u002Fwp\u002Fv2\u002Fblock-renderer\u002F...` route is not batch-allowed but returns `block_cannot_read` if its\nhandler is reached anonymously; `\u002Fbatch\u002Fv1` gives `rest_batch_not_allowed`. On vulnerable builds\nthe parse error shifts the batch handler arrays out of step, so the spacer request is dispatched\nunder the block-renderer handler. Fixed builds keep the arrays aligned, so this exact all-three\npattern should not appear for the crafted probe.\n\nBy default, `check` stops there and does not send an SQLi payload. Use `--confirm-sqli` when you\nalso want an active SQLi confirmation. The confirmation tries the UNION read primitive first and\nfalls back to paired timing probes if UNION reflection is unavailable.\n\nThe signals are independent: a version hint is only a hint, the marker pattern shows route\nconfusion, and `--confirm-sqli` shows a payload reached the database. A WAF can block the payload,\nso a failed confirmation doesn't prove the bug is absent.\n\n```\n.\u002Fwp2shell.py check http:\u002F\u002Ftarget\n.\u002Fwp2shell.py check targets.txt          # scan every URL in the file\n```\n\n### read — extract data through SQL injection\n\n```\n.\u002Fwp2shell.py read http:\u002F\u002Ftarget                      # server fingerprint\n.\u002Fwp2shell.py read http:\u002F\u002Ftarget --preset users       # user logins and password hashes\n.\u002Fwp2shell.py read http:\u002F\u002Ftarget --query \"SELECT @@version\"\n```\n\nBy default extraction is `--technique auto`, which tries the available methods in this order:\n\n1. **union** — forges a fake `WP_Post` row via `UNION` and reads its title back from the REST\n   response as `||HEX(value)||`. The payload uses the same `\u002Fwp\u002Fv2\u002Fposts\u002F999999` source route with\n   `orderby=none` and `per_page=500` so the fake row survives as a rendered post. One request per\n   value.\n2. **error** — `EXTRACTVALUE`\u002F`UPDATEXML` leak ~15 bytes per request, when the target reflects\n   MySQL errors (e.g. `WP_DEBUG_DISPLAY` on).\n3. **blind** — boolean binary search, ~8 requests per character; reads the posts collection\n   `X-WP-Total` header as the true\u002Ffalse signal and needs no reflected value.\n\nForce one with `--technique union|error|blind`. These read paths do not write database rows.\n\n### shell — command execution\n\nWith `--user` and `--password`, `shell` logs in with supplied administrator credentials and uses\nWordPress plugin upload behavior.\n\nWithout credentials, `shell` first runs the pre-auth SQLi-to-admin bridge, logs in as the generated\nadministrator, then uploads the plugin shell.\n\n```\n.\u002Fwp2shell.py shell http:\u002F\u002Ftarget --user admin --password '\u003Crecovered>' --cmd id\n.\u002Fwp2shell.py shell http:\u002F\u002Ftarget --user admin --password '\u003Crecovered>' -i   # interactive shell\n.\u002Fwp2shell.py shell http:\u002F\u002Ftarget --cmd id                                   # pre-auth bridge\n.\u002Fwp2shell.py shell http:\u002F\u002Ftarget -i                                         # pre-auth interactive\n```\n\n`shell` uploads a plugin webshell (locked behind a random path and a per-run token) and prints its\npath. The uploaded webshell is removed automatically. When the pre-auth bridge creates an\nadministrator, that generated account is removed automatically after the shell session finishes.\n\n## Options\n\n| Option              | Applies to | Description                                                           |\n| ------------------- | ---------- | -------------------------------------------------------------------- |\n| `--proxy URL`       | all        | Route traffic through an HTTP proxy (for example, Burp).             |\n| `--timeout N`       | all        | Request timeout in seconds.                                          |\n| `--sleep N`         | check      | Delay used by the timing fallback for `--confirm-sqli`.              |\n| `--samples N`       | check      | Timing pairs used by the timing fallback for `--confirm-sqli`.       |\n| `--confirm-sqli`    | check      | Also send an active SQLi confirmation payload.                       |\n| `--preset`          | read       | `fingerprint` or `users`.                                            |\n| `--technique`       | read       | `auto` (default), `union` (in-band, forges a fake post), `error` (in-band, needs visible DB errors), or `blind`. |\n| `--query`           | read       | A scalar SQL expression to read.                                     |\n| `--prefix`          | read       | Database table prefix (default `wp_`).                               |\n| `--max-length N`    | read       | Maximum characters read per value (default 128).                     |\n| `--user` \u002F `--password` | shell  | Optional admin credentials; omit both to use the pre-auth bridge.   |\n| `--cmd`             | shell      | Command to run (omit when using `-i`).                              |\n| `-i` \u002F `--interactive` | shell   | Open an interactive shell after deploying.                           |\n\n## Remediation\n\nUpdate to WordPress 7.0.2, or 6.9.5 if the site is on the 6.9 branch. Until then,\nblock both `\u002Fwp-json\u002Fbatch\u002Fv1` and the `rest_route=\u002Fbatch\u002Fv1` query parameter at\nthe edge, or require authentication for the batch endpoint via the\n`rest_pre_dispatch` filter.\n\n## Legal\n\nFor authorized security testing only. Use it exclusively against systems you own or have explicit\nwritten permission to test. No warranty is provided and no liability is accepted for misuse.\n\n## References\n\n- WordPress 7.0.2 release announcement — \u003Chttps:\u002F\u002Fwordpress.org\u002Fnews\u002F2026\u002F07\u002Fwordpress-7-0-2-release\u002F>\n- Searchlight Cyber wp2shell advisory — \u003Chttps:\u002F\u002Fslcyber.io\u002Fresearch-center\u002Fwp2shell-pre-authentication-rce-in-wordpress-core\u002F>\n- sergiointel\u002Fwp2shell-poc SQLi-to-admin bridge — \u003Chttps:\u002F\u002Fgithub.com\u002Fsergiointel\u002Fwp2shell-poc>\n","2026-07-19 02:30:06","CREATED_QUERY"]