Hacking APIs: the bugs that live between endpoints
Modern web challenges are increasingly a REST or GraphQL API and a token. The four vulnerabilities that dominate API CTF - broken object-level auth, broken function-level auth, mass assignment, and GraphQL introspection - and how to test each one.
A growing share of web challenges are not websites at all. They are a JSON API with a login endpoint, a bearer token, and a flag sitting behind an authorization check that was written slightly wrong. There is no HTML to inspect, no forms to fill, just requests and responses - which is good news, because API bugs are more mechanical than classic web bugs. The whole game is spotting where the server trusts something it should have verified.
Four vulnerability classes account for most of it, and they map neatly onto the OWASP API Security Top 10. Learn to test these four and you can work almost any API challenge methodically instead of poking at random.
Map the API before attacking it
You cannot attack endpoints you have not found. Spend the first few minutes building a picture of the surface - the same job as web recon, with a spec file instead of a sitemap.
- Look for the spec.
/openapi.json,/swagger.json,/api-docs,/graphqlwith introspection. A published schema hands you every route, parameter, and type for free - and CTF authors leave it enabled more often than you would think. - Watch the client. If there is a frontend, its JavaScript bundle calls the API. Read the requests it makes; the routes and the token format are all in there.
- Brute the obvious.
/api/v1/users,/api/v1/admin,/api/internal. Versioned prefixes are a tell, and an older version left mounted alongside the current one often skips a check the new one added. - Read the token. If it is a JWT, decode it. The claims tell you what the server thinks about you - your user id, your role - and those are exactly the values the next four attacks try to change.
1. Broken object-level authorization (BOLA / IDOR)
The most common API bug, full stop. An endpoint takes an object id - GET /api/orders/1337 - and returns that object without checking that the object belongs to you. Change the id, get someone else's data. When that someone is the admin or the flag-holder, you win.
The test is A-B testing: create or observe two accounts, note an object id that belongs to account A, then request it while authenticated as account B. If B gets A's object, the endpoint is broken.
GET /api/v2/users/1/documents/42 HTTP/1.1
Authorization: Bearer <token-for-user-2>
# You are user 2. If document 42 (user 1's) comes back, that is BOLA.
# Then walk the id space: 41, 40, 39 ... one of them holds the flag.2. Broken function-level authorization (BFLA)
BOLA is about objects you should not see; BFLA is about actions you should not perform. A regular user finding the admin-only endpoint and being allowed to call it. The endpoint checks that you are logged in, but forgets to check that you are an admin.
The test is A-B-A: do an action as an admin (or infer the admin request shape from the docs), then replay it as a low-privileged user, then confirm as the admin that it took effect. The two things to vary are the HTTP method and the route.
- Method swapping.
GET /api/users/5works for you; tryPUT,PATCH, andDELETEon the same route. Read access does not imply the write methods were locked down. - Guessing the admin route. If
/api/user/profileexists, try/api/admin/users,/api/users(the collection),/api/user/5/promote. The docs or the JS often name these even when the UI hides them. - Verb tampering. Some frameworks honour
X-HTTP-Method-Override: DELETEon a POST, sneaking a blocked method past a filter that only inspects the real verb.
3. Mass assignment
When an API binds a JSON body straight onto a database model, you can often set fields the form never showed you. The registration endpoint takes {username, password} - but the model also has an is_admin or role field, and the framework will happily set it if you include it.
POST /api/register HTTP/1.1
Content-Type: application/json
{"username":"me","password":"pw","role":"admin","is_admin":true,"balance":999999}You discover the field names by reading them off a GET response for your own object - whatever the API shows you about yourself is a menu of fields to try setting. Combined with BFLA, mass assignment is a two-step chain: find the admin update endpoint you should not be able to call, then use it to set your own role to admin.
4. GraphQL: introspection and its consequences
GraphQL challenges have a distinctive shape: one endpoint, usually /graphql, that answers any query you can express against its schema. The first move is always the same - ask the schema to describe itself.
{"query":"{ __schema { types { name fields { name args { name } } } } }"}With the schema in hand, the same authorization bugs apply, often more sharply. GraphQL resolvers frequently check auth at the query level but not per field, so a user(id: 2){ email passwordHash } may return fields for an object you were never meant to read. Mutations are the BFLA surface: look for deleteUser, promoteUser, updateRole, and try calling them as a low-privileged user.
The checklist
- Map the surface: spec files, the client's requests, versioned prefixes, and the token's claims.
- BOLA: change object ids across accounts; harvest ids from side channels.
- BFLA: swap methods and hit admin routes as a low-privileged user.
- Mass assignment: send extra fields (role, is_admin) you read off your own object.
- GraphQL: run introspection, then apply BOLA and BFLA per field and per mutation.
- Chain them: BFLA to reach an update endpoint, mass assignment to make yourself admin.