Hackvent 2019: Day 17

Hackvent 2019491

Challenge

HV19.17 Unicode Portal

Introduction
Buy your special gifts online, but for the ultimative gift you have to become admin.

Resources
http://whale.hacking-lab.com:8881/

Solution

We visit the unicode portal and are presented with a very cool website:

Hackvent 2019 - Day 17 - Unicode Portal

We have to login before we can view the symbols, source or admin page. We register an account (only username and password is needed). Upon logging in we see a symbols page, a source page and an admin page. Upon accessing the admin page we are told You need to be an admin in order to access this area!. The source page is very unique and shows us the source code of the user.php page.

The source code is:

<?php

if (isset($_GET['show'])) highlight_file(__FILE__);

/**
 * Verifies user credentials.
 */
function verifyCreds($conn, $username, $password) {
  $usr = $conn->real_escape_string($username);
  $res = $conn->query("SELECT password FROM users WHERE username='".$usr."'");
  $row = $res->fetch_assoc();
  if ($row) {
    if (password_verify($password, $row['password'])) return true;
    else addFailedLoginAttempt($conn, $_SERVER['REMOTE_ADDR']);
  }
  return false;
}

/**
 * Determines if the given user is admin.
 */
function isAdmin($username) {
  return ($username === 'santa');
}

/**
 * Determines if the given username is already taken.
 */
function isUsernameAvailable($conn, $username) {
  $usr = $conn->real_escape_string($username);
  $res = $conn->query("SELECT COUNT(*) AS cnt FROM users WHERE LOWER(username) = BINARY LOWER('".$usr."')");
  $row = $res->fetch_assoc();
  return (int)$row['cnt'] === 0;
}

/**
 * Registers a new user.
 */
function registerUser($conn, $username, $password) {
  $usr = $conn->real_escape_string($username);
  $pwd = password_hash($password, PASSWORD_DEFAULT);
  $conn->query("INSERT INTO users (username, password) VALUES (UPPER('".$usr."'),'".$pwd."') ON DUPLICATE KEY UPDATE password='".$pwd."'");
}

/**
 * Adds a failed login attempt for the given ip address. An ip address gets blacklisted for 15 minutes if there are more than 3 failed login attempts.
 */
function addFailedLoginAttempt($conn, $ip) {
  $ip = $conn->real_escape_string($ip);
  $conn->query("INSERT INTO fails (ip) VALUES ('".$ip."')");
}

?>

From the source code we can exploit the register functionality! We decide to work backwards from the isAdmin function.

The === comparison is very strong and thus won't be broken. The verifyCreds function sanitises input and simply selects a password from the database matching the username.
Therefore in the end we know we must login with the santa username but we do not know its password.

However, in the registerUser method we see that ON DUPLICATE KEY the password is updated! Great. However, the isUsernameAvailable check is called before this to stop us from just registering the username santa again and updating the password.

The key to this exploit is the usage of the LOWER and UPPER methods. We need to find some username input where LOWER("SANTA") = BINARY LOWER(username) and UPPER(username) = "SANTA".
We write a quick SQL query to test some inputs:

SET @username = "test";
SELECT LOWER("SANTA"), LOWER(@username), UPPER(@username), BINARY LOWER(@username), LOWER("SANTA") = BINARY LOWER(@username) as USERNAME_COUNT, "santa" = UPPER(@username) AS OVERWRITE_PASS;

We run the following Javascript in our browser to do some quick checks to see if any character to map to a letter in santa when transformed to upper case. We luckily start with the letter S:

highNumber = 1000;
seen = [];
for(i=0;i<highNumber;i++){
    c = String.fromCharCode(i);
    if (seen.includes(c))
        continue;
    seen.push(c);
    if (c.toUpperCase() == 'S')
        console.log(c + " " + c.charCodeAt(0));
}

We get the following output:

S 83
s 115
ſ 383

Interestingly, the lowercase ſ character (U+017F : LATIN SMALL LETTER LONG S) is mapped to converted to an uppercase S (U+0053 : LATIN CAPITAL LETTER S).
Protip: Use this useful website to check unicode input easily.

Therefore, we try the input ſanta with out local query and the test passed! It bypasses the isUsernameAvailable check and then in the registerUser method it triggers a ON DUPLICATE KEY on UPPER("ſanta") which equals SANTA. Thus, we have overridden the password for the santa user. Finally, we login using santa as the username and the pass we used before and are authenticated!

We visit the admin page and get our daily flag:

Hackvent 2019 - Day 17 - Admin Section Flag

Flag:

HV19{h4v1ng_fun_w1th_un1c0d3}

Leave a comment

(required)(will not be published)(required)

Comments

Showing 1 comment from 1 commenter.

  • Display picture for Mo Beigi
    Mo Beigi

    These are also a good read:

    https://unicode.org/faq/casemap_charprop.html#10

    https://hackernoon.com/%CA%BC-%C5%9B%E2%84%87%E2%84%92%E2%84%87%E2%84%82%CA%88-how-unicode-homoglyphs-will-break-your-custom-sql-injection-sanitizing-functions-1224377f7b51

    Reply