Contact CTF writeups Notes

[PicoCTF 2018] - web - The Vault

This is one of my writeups for PicoCTF 2018

Problem

TThere is a website running at http://2018shell3.picoctf.com:53261. Try to see if you can login!

Solution

When browsing to the target URL, we are presented with a login form and a link to the login source code :

<?php
  ini_set('error_reporting', E_ALL);
  ini_set('display_errors', 'On');

  include "config.php";
  $con = new SQLite3($database_file);

  $username = $_POST["username"];
  $password = $_POST["password"];
  $debug = $_POST["debug"];
  $query = "SELECT 1 FROM users WHERE name='$username' AND password='$password'";

  if (intval($debug)) {
    echo "<pre>";
    echo "username: ", htmlspecialchars($username), "\n";
    echo "password: ", htmlspecialchars($password), "\n";
    echo "SQL query: ", htmlspecialchars($query), "\n";
    echo "</pre>";
  }

  //validation check
  $pattern ="/.*['\"].*OR.*/i";
  $user_match = preg_match($pattern, $username);
  $password_match = preg_match($pattern, $username);
  if($user_match + $password_match > 0)  {
    echo "<h1>SQLi detected.</h1>";
  }
  else {
    $result = $con->query($query);
    $row = $result->fetchArray();

    if ($row) {
      echo "<h1>Logged in!</h1>";
      echo "<p>Your flag is: $FLAG</p>";
    } else {
      echo "<h1>Login failed.</h1>";
    }
  }
?>

It is apparent from the code that we're looking at an SQL injection, this time with a regex-based filter to circumvent.

We can also see that the script takes a debug parameter that will echo the SQL query.

The regex used for validation is:

/.*['\"].*OR.*/i

That translates to (any character, 0 or more times) followed by (a single quote) followed by ("OR") followed by (any character, 0 or more times)

This filter would prevent using the basic ' OR 1=1 -- injection.

The filtering bug

If we look closely at the code, we can notice a mistake :

$user_match = preg_match($pattern, $username);
$password_match = preg_match($pattern, $username);

Instead of checking the validation regex against the username and password fields, the username field is checked twice ! This means we can use our basic injection on the password field.

Thanks to the debug parameter, we know that the query is constructed as follows:

SELECT 1 FROM users WHERE name='username' AND password='password'

So if we were to inject our ' OR 1=1 ; -- payload in the password field, we would end up with the following query :

SELECT 1 FROM users WHERE name='username' AND password='' OR 1=1 ; --'

That circumvents password verification, but we still need a valid username for the query to return at least one row.

So I took a wild guess and tried a username of admin and a password of ' OR 1=1 ; --. Bingo : picoCTF{w3lc0m3_t0_th3_vau1t_23495366}

A better solution

Let's assume for a moment that the filter was correctly used (i.e. if the username and password fields were correctly checked). Would that mean that we couldn't leverage an SQL injection ? Certainly not.

Here's an alternative solution I thought about when writing this up : if you pass '/* as username and */ OR 1=1 -- as password, you get the flag and would get it without the bug exploited in the previous solution.

The reason for that is that the two components of the injection (the ' escaping the quoted block and the always true OR 1=1 condition) that are checked against are split in different fields, preventing the regex from matching :

  • '/* has a single quote but no OR : the username field doesn't match
  • */ OR 1=1 -- contains OR but no single quote before : the password field doesn't match

The /* */ is pretty much the same as in javascript : it declares an inline comment, meaning that anything between /* and */ will be ignored.

So with that request, this query is constructed

SELECT 1 FROM users WHERE name=''/*' AND password='*/ OR 1=1 --'

Removing the commented-out parts, this is executed :

SELECT 1 FROM users WHERE name='' OR 1=1

This is a better solution for 2 reasons :

  1. It would work even if the filter checking were fixed
  2. It doesn't require to guess a valid username.