Hacking GraphQL API Using Race Conditions
Introduction⌗
I have been using this platform for quite a few years. I will not be disclosing the platform’s name for obvious reasons. While using it, I discovered an interesting functionality related to in-game currency. Periodically, users are awarded a gift that, when claimed, grants them 10 coins. These coins can be used to acquire merchandise, premium subscriptions, and other benefits on the platform. I made a mental note of this feature and continued with my day, procrastinating. After a considerable amount of time, I decided to explore whether I could manipulate this functionality to potentially obtain additional coins.
Investigating the HTTP traffic⌗
I fired up Burp Suite and began analyzing the HTTP history tab to identify the specific HTTP request sent to the server when the “claim” button is clicked. This platform utilizes GraphQL, which is easily recognizable due to the POST request directed to the /graphql
endpoint.
GraphQL⌗
GraphQL is a query language for APIs, and it serves as a server-side runtime for executing these queries by utilizing a user-defined type system for data. Unlike traditional REST APIs, where clients request data from various endpoints, GraphQL allows clients to precisely request the required data, eliminating unnecessary data retrieval. This is achieved by transmitting a single query to the server, which then responds with a JSON containing the requested data. This approach minimizes over-fetching (receiving more data than necessary) and under-fetching (receiving insufficient data) from the server. You can learn more about GraphQL here.
The body of the GraphQL request resembled the following:
{"query":"\n mutation collectCoins {\n collectCoins {\n ok\n }\n}\n ","variables":{},"operationName":"collectCoins"}
Considering that I had already claimed my one-time gift, sending this request again shouldn’t have yielded any more coins, right?
I sent the request to the repeater and clicked the send button to replay the request, resulting in the following response:
{"data":{"collectCoins":{"ok":true}}}
Wait, did it work? I checked my coin count, but it remained the same, indicating the presence of internal validation. Therefore, this attempt was unsuccessful.
Let’s contemplate: What could be happening on the backend? What logic might underlie this functionality?
My assumption of its functioning is as follows:
- The user clicks the “get coins” button.
- A request is dispatched to the server.
- The server verifies whether the user has previously claimed the gift.
- If the user hasn’t previously claimed the gift:
- The server increases the user’s coin count.
- The server sets a flag indicating that the user has claimed the gift.
- If the user has previously claimed the gift:
- The server does not increase the coin count.
So, what options do we have here? What might occur if we submit another request before the server sets a flag?
Race Condition⌗
A race condition arises in computer programming when two or more threads or processes attempt to access shared resources—such as memory, files, or data structures—concurrently. This can lead to unforeseen or undesirable outcomes. The term “race” reflects the fact that the outcome depends on which thread or process “wins the race” to access the resource first.
I switched to my alternate account and, fortunately, it also had an unclaimed gift. This time, I needed to submit the request multiple times within a short timeframe. To do this, I intercepted the requests by enabling the intercept feature in Burp Suite. I forwarded the unwanted requests and sent the request containing the GraphQL mutation body to the repeater. I then dropped this request from the intercept tab. I sent this request to Turbo Intruder from the repeater and selected the “race.py” script. Turbo Intruder requires the presence of %s
in the request, typically used for replacing it with the words for fuzzing, even if no fuzzing is needed. I set a dummy header X-hacker: %s
and initiated the attack. After completion, I checked my coin count and was astonished to find that I had obtained multiple sets of coins. It worked!
I had gained six times the intended amount! I attempted to replicate the process to create a functional POC to present to the concerned party for resolution. During recording, I was able to claim double the number of coins. This aligns with the understanding that a race condition isn’t a foolproof attack, and its success can be inconsistent. Thus, multiple attempts are necessary, which can be somewhat frustrating. However, persistence yields results.
Disclaimer⌗
The information presented in this blog post is intended for educational and informative purposes only. The author discovered a vulnerability and responsibly reported it to the appropriate parties to help improve security measures. The author does not endorse or condone any unauthorized, unethical, or illegal use of the information provided in this article. Any actions taken by readers based on the information presented are solely their responsibility.