It was supposed to be an easy target. A static marketing website — no login, no signup, no user accounts. The kind of target most bug hunters scroll past because "where do you even start?" But I've learned something after years of doing this: the bugs are never where you expect them. They hide in the plumbing. Here's the full story of how a routine recon on a "boring" static site turned into a PII exposure affecting 501 customer records.

Phase 1: The Boring Part (Or So I Thought)

Every hunt starts the same way — mapping the attack surface. I ran passive subdomain enumeration and got back 52 subdomains. Most of them were exactly what you'd expect:

  • VPN gateways (blocked by Cloudflare)
  • Internal integration servers (BizTalk)
  • B2B gateways requiring client certificates

But a few stood out:

  • wordpress.[target].com — A WordPress backend
  • scim.[target].com — A 1Password SCIM bridge
  • portal.[target].com — Some kind of portal
  • staging1.[target].com — A staging environment

My thinking at this point: "Okay, there's actual infrastructure here. The main site might be static, but the backend isn't." I probed all subdomains with HTTPX to see what was actually alive. The results were… underwhelming:

  • Most returned 403 behind Cloudflare
  • scim.[target].com was properly secured (introspection disabled, auth required)
  • remote.[target].com was a SonicWALL VPN (out of scope)
  • ncbiztalk.[target].com was a default IIS page from 2020

But wordpress.[target].com? That returned a 200 with a full WordPress login page. "There you are," I thought.

Phase 2: The WordPress Rabbit Hole

WordPress is one of those targets where you can spend hours going down rabbit holes. I started with the basics:

What I Checked First

  • wp-config.php backup files (.bak, .save, .old) → All 403 (Cloudflare WAF)
  • .env files → 403
  • debug.log → 302 redirect (weird, but not exploitable)
  • xmlrpc.php → 302 redirect (disabled)
  • readme.html → Blocked

My thinking: "They've locked down the obvious stuff. Cloudflare is doing its job. Let me check the REST API."

The REST API Discovery

I hit the WordPress REST API:

curl -s "https://wordpress.[target].com/wp-json/wp/v2/users"

Response:

{"code":"aios_user_lists_forbidden","message":"Listing users is forbidden."}

"Interesting," I thought. "They're running All In One Security (AIOS). That plugin blocks user enumeration via REST API." I confirmed this by checking:

  • Author enumeration (?author=1) → No location header leak
  • Status parameter (?status=draft) → Forbidden

AIOS was doing its job. User enumeration was locked down. My thinking at this point: "Okay, WordPress is hardened. The security team knows what they're doing. Let me check if there's anything else interesting." But then I noticed something in the HTML source of the login page:

<p style="display: none;"> <label>Enter something special:</label> <input name="aio_special_field" type="text" class="aio_special_field" value="" /> </p>

This confirmed AIOS was active with honeypot fields. Good security. I also noticed the response headers from earlier:

access-control-expose-headers: X-WP-Total, X-WP-TotalPages, X-JWT-Refresh

Wait. JWT-Refresh? That header only appears when JWT Authentication is installed. And JWT auth in WordPress usually means… GraphQL.

Phase 3: The GraphQL Pivot

I tested the GraphQL endpoint:

curl -s "https://wordpress.[target].com/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { types { name } } }"}'

Response:

{"errors":[{"message":"GraphQL introspection is not allowed"}]}

Introspection was disabled. Good security practice. But the endpoint was alive. That was enough.

The User Enumeration That Wasn't

I tried the users query:

curl -s "https://wordpress.[target].com/graphql" \ -H "Content-Type: application/json" \ -d '{"query":"{ users { nodes { id name email slug } } }"}'

Response:

{ "data": { "users": { "nodes": [ {"id": "dXNlcjox", "name": "ad…", "email": null, "slug": "admin"}, {"id": "dXNlcjo3", "name": "Anr….", "email": null, "slug": "anna"}, {"id": "dXNlcjo0", "name": "Mirj….", "email": null, "slug": "mirjam"}, {"id": "dXNlcjoxNg==", "name": "Shala…", "email": null, "slug": "shalane-de-scande"}, {"id": "dXNlcjoxOA==", "name": "Vikto….", "email": null, "slug": "viktorija"} ] } } }

There it was. AIOS blocked the REST API, but GraphQL completely bypassed it. I found three separate enumeration vectors:

  1. Direct users query (listed all users)
  2. Login mutation error differentiation (incorrect_password vs invalid_username)
  3. Password reset mutation response differentiation (user object returned vs null)

I was excited. This was a clean bypass of their security controls. Then I checked the scope.

Phase 4: The "Oh No" Moment

Buried in the program's out-of-scope list:

"User enumeration without any impact"

My heart sank. I'd just spent an hour finding a textbook example of exactly what they excluded. My thinking at this point: "Well, that's a waste. The GraphQL bypass is clean, but they explicitly don't want it. Time to move on." But I didn't move on immediately. I've learned to do one final check before walking away — check what other routes exist in the REST API. This is a habit I developed after missing bugs early in my career. You never know what's hiding in custom endpoints.

Phase 5: The 30-Second Check That Changed Everything

I ran a simple command to list all registered REST API routes:

curl -s "https://wordpress.[target].com/wp-json/" | jq -r '.routes | keys[]' | grep -v 'wp/v2'

Most of it was standard WordPress stuff:

  • /oembed/1.0/embed
  • /batch/v1
  • /code-snippets/v1/snippets (returned 401 — properly secured)
  • /regenerate-thumbnails/v1 (admin functionality)
  • /yoast/v1/* (SEO plugin routes)

Then I saw it:

/contact-submission /form-submission

"Wait… what?" These weren't standard WordPress post types. These were custom. My thinking shifted: "Contact submission? Form submission? Those store actual customer data. Let me check if they're accessible."

curl -s "https://wordpress.[target].com/wp-json/wp/v2/form-submission"

The response came back:

[ { "id": 16680, "date": "2026–05–04T08:43:27", "title": {"rendered": "xyz— wiktoria@parrotplanet.xyz"}, "status": "publish" }, { "id": 16679, "date": "2026–05–04T08:13:57", "title": {"rendered": "xyz — kontakt@4hunting.xyz"}, "status": "publish" } ]

Real customer emails. Right there in the API response. No authentication required. I immediately checked the total count:

curl -s -I "https://wordpress.[target].com/wp-json/wp/v2/form-submission" | grep x-wp-total x-wp-total: 178

501 total customer submissions. Exposed. Unauthenticated.

Why This Worked (And What I Learned)

The Mistake The Developers Made

When they created custom post types for form submissions, they left show_in_rest set to true. This is actually the default in modern WordPress — it's designed to make it easy to build headless setups. The problem? They also set the post status to publish instead of private. Public status + REST API enabled = publicly accessible data.

Why The Security Controls Failed

  • AIOS only protects standard WordPress endpoints (users, author queries)
  • Cloudflare WAF blocks known malicious patterns, not custom API routes
  • The GraphQL security was focused on introspection and user queries
  • Nobody thought to check if the custom post types were exposed

The Bug Hunter's Mindset That Found This

  1. "Check everything, even when you think you're done" — I almost moved on after the GraphQL enumeration was deemed out of scope
  2. "Custom post types are always worth checking" — This is a lesson I've learned the hard way
  3. "The scope says user enumeration, not data exposure" — Understanding the difference between "listing usernames" and "leaking customer PII" was crucial
  4. "Always check /wp-json/ routes" — Standard recon, but it paid off

Technical Takeaways for Bug Hunters

1. Always Check Custom Post Types

# Get all registered routes curl -s "https://target.com/wp-json/" | jq -r '.routes | keys[]' # Look for anything that's not wp/v2, oembed, batch, or standard plugin routes

2. Test Every Custom Endpoint Without Auth

# For each custom route you find curl -s "https://target.com/wp-json/wp/v2/CUSTOM-TYPE" | head -50 curl -s -I "https://target.com/wp-json/wp/v2/CUSTOM-TYPE" | grep x-wp-total

3. Understand The Difference In Impact

None

4. Don't Stop At The First "Blocked"

If AIOS blocks /wp-json/wp/v2/users, don't assume everything is secured. Check:

  • GraphQL endpoints
  • Custom REST routes
  • Individual post IDs (try /wp-json/wp/v2/posts/1, /wp-json/wp/v2/pages/1)
  • Custom taxonomies (/wp-json/wp/v2/CUSTOM-TAXONOMY)
None