## https://sploitus.com/exploit?id=09381E37-66E9-52A2-AF7E-670A9638A774
# wp2shell-go
A Go port of the wp2shell PoC. It targets the unauthenticated WordPress REST batch route-confusion SQL injection from Searchlight Cyber's wp2shell advisory. Everything runs through one endpoint: `POST /wp-json/batch/v1`.
This is not Searchlight's official checker and it does not reproduce any undisclosed wp2shell internals. `check` confirms the SQLi path, `read` pulls data out over the blind injection, and `shell` is an optional post-auth helper that uploads a plugin webshell once you already have admin credentials.
Standard library only. No third-party Go dependencies, nothing to `go get`.
## Affected versions
| Version range | Status |
| ------------- | ------ |
| [options]
```
### check
Starts passive. It prints WordPress markers and public version hints, then sends a benign batch marker probe. A vulnerable build answers with HTTP 207 and the full marker pattern: `parse_path_failed`, `block_cannot_read`, and `rest_batch_not_allowed`.
The probe comes straight from the core fix. The malformed `///` request creates `parse_path_failed`. A `/wp/v2/posts` request acts as a batch-allowed spacer. The `/wp/v2/block-renderer/...` route is not batch-allowed but returns `block_cannot_read` if its handler gets reached anonymously, and `/batch/v1` gives `rest_batch_not_allowed`. On a vulnerable build the parse error shifts the handler arrays out of step, so the spacer request runs under the block-renderer handler. A fixed build keeps the arrays aligned, so this exact three-code pattern should not show up for the crafted probe.
By default `check` stops there and sends no SQL payload. Add `--confirm-sqli` for the active paired timing confirmation. That step reads nothing and changes nothing. It sends three baseline/delayed pairs and decides on the median delta, which keeps network jitter from producing a false positive.
```
./wp2shell check http://target
./wp2shell check http://target --confirm-sqli
```
### read
```
./wp2shell read http://target # server fingerprint
./wp2shell read http://target --preset users # logins and password hashes
./wp2shell read http://target --query "SELECT @@version"
```
The extractor reads one character at a time with a binary search on the byte value, so a short string costs a handful of requests per character. It is slow and chatty, but it works blind.
### shell
Optional post-exploitation helper, and not a pre-auth RCE step. It needs valid admin credentials. If `read --preset users` gave you a hash, crack it offline and pass the plaintext here.
```
./wp2shell shell http://target --user admin --password '' --cmd id
./wp2shell shell http://target --user admin --password '' -i # interactive
```
It uploads a plugin webshell behind a random path and a per-run token, then prints the path. Clean up when you are done. Pass `--cleanup` to have it delete itself after running, or remove the plugin by hand.
## Options
| Option | Applies to | Description |
| ------ | ---------- | ----------- |
| `--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. |
| `--preset` | read | `fingerprint` or `users`. |
| `--query` | read | A scalar SQL expression to read. |
| `--prefix` | read | Database table prefix (default `wp_`). |
| `--max-length N` | read | Cap on characters read per value (default 128). |
| `--user` / `--password` | shell | Admin credentials, plaintext, recovered from the hash. |
| `--cmd` | shell | Command to run (omit when using `-i`). |
| `-i` / `--interactive` | shell | Open an interactive shell after deploying. |
| `--cleanup` | shell | Delete the webshell from the target when finished. |
| `--keep` | shell | Do not warn about removing the webshell. |
## Layout
```
main.go CLI entry point
internal/cli/ argument parsing and command dispatch
internal/wp/ the primitive: client, sqli, shell, version
internal/integration/ live-target tests (build tag: integration)
test/ docker-compose target and integration runner
```
## Tests
Unit tests need nothing external:
```
go test ./...
```
The integration tests run against a real, vulnerable WordPress 7.0.1 container. The runner brings up WordPress and MariaDB, installs the site, runs the Go integration tests and the CLI binary against it, then tears the stack down:
```
make integration
# or: ./test/run-integration.sh
# KEEP=1 ./test/run-integration.sh # leave the stack running
```
That suite walks the whole chain against the live target: detection and version fingerprint, the route-confusion marker probe, a blind read of the MariaDB version, the time-based oracle, recovery of the admin login and password hash out of `wp_users`, and the post-auth webshell deploy, run, and cleanup. Last run recovered `1|admin|$wp$2y$10$...` and ran `id` as `www-data`.
## Remediation
Update to WordPress 7.0.2, or 6.9.5 if you are on the 6.9 branch. Until you can, block both `/wp-json/batch/v1` and the `rest_route=/batch/v1` query parameter at the edge, or require authentication for the batch endpoint through the `rest_pre_dispatch` filter.
## Legal
For authorized security testing only. Use it against systems you own or have explicit written permission to test. No warranty, and no liability for misuse.
## References
- WordPress 7.0.2 release announcement:
- Searchlight Cyber wp2shell advisory:
- The Python PoC this is ported from: