Contact CTF writeups Notes

[PicoCTF 2018] - web - Artisanal Handcrafted HTTP 3

This is one of my writeups for PicoCTF 2018

Problem

We found a hidden flag server hiding behind a proxy, but the proxy has some... interesting ideas of what qualifies someone to make HTTP requests. Looks like you'll have to do this one by hand. Try connecting via nc 2018shell3.picoctf.com 41367, and use the proxy to send HTTP requests to flag.local. We've also recovered a username and a password for you to use on the login page: realbusinessuser/potoooooooo.

hints:

  1. Be the browser. When you navigate to a page, how does your browser send HTTP requests? How does this change when you submit a form?

Solution

When we connect to the server, we are greeted by a superb ASCII-art captcha :

 _____            ___
|____ |          /   |  ______
    / / __  __  / /| | |______|
    \ \ \ \/ / / /_| |  ______
.___/ /  >  <  \___  | |______|
\____/  /_/\_\     |_/




>

Yep, no way my browser can handle that : as the problem description states, we're going to have to write HTTP requests by hand.

Writing HTTP requests

Mozilla has a pretty good guide to get started on the protocol : "An overview of HTTP".

An HTTP request has the following content:

{METHOD} {PATH} {PROTOCOL_VERSION}
{HEADERS}

{BODY}

The headers and the body are separated by a newline, the body being optional.

Here's a basic example GET request :

GET /index.html HTTP 1.1

And here's an example POST request :

POST /login.php HTTP 1.1
Content-Length: 30

username=iodbh&password=secret

The Content-Length header is mandatory if the request has a body, as it tells the server how many bytes are part of the body. (So in the example above, if I passed a Content-Length of 30, the server would read username=iodbh&password=secre - missing the last byte/character).

So let's try getting the root page :

Request:

GET / HTTP 1.1

Response:

HTTP/1.1 400 Missing Host header
Date: Mon, 15 Oct 2018 14:00:08 GMT
Connection: keep-alive
Transfer-Encoding: chunked

0

Hmmm, looks like the Host header is missing.

The Host header

As you probably know, the DNS protocol helps us resolve hostnames to IP addresses. But if several domains are hosted on the same machine (or, like in this problem, behind the same proxy), how does the server know what resource to return ? It reads the host header.

Note : This has very interesting security implications, I highly recommend the talk "cracking the lens : Targeting HTTP's Hidden Attack-Surface" (text version) by James Kettle on the subject.

The host we want is given in the task description : flag.local. So let's try with the header :

Request:

GET / HTTP 1.1
Host: flag.local

Response:

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 321
etag: W/"141-LuTf9ny9p1l454tuA3Un+gDFLWo"
date: Mon, 15 Oct 2018 14:21:53 GMT
connection: close
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="main.css" />
    </head>
    <body>
        <header>
            <h1>Real Business Internal Flag Server</h1>
            <a href="/login">Login</a>
        </header>
        <main>
            <p>You need to log in before you can see today's flag.</p>
        </main>
    </body>
</html>

This time we have some HTML back, with a link to /login.

Handling forms

If we follow the link, we get a form :

Request:

GET /login HTTP 1.1
Host: flag.local

Response:

GET /login HTTP 1.1
Host: flag.local

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 498
etag: W/"1f2-UE5AGAqbLVQn1qrfKFRIqanxl9I"
date: Mon, 15 Oct 2018 14:26:34 GMT
connection: close
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="main.css" />
    </head>
    <body>
        <header>
            <h1>Real Business Internal Flag Server</h1>
            <a href="/login">Login</a>
        </header>
        <main>
            <h2>Log In</h2>

            <form method="POST" action="login">
                <input type="text" name="user" placeholder="Username" />
                <input type="password" name="pass" placeholder="Password" />
                <input type="submit" />
            </form>
        </main>
    </body>
</html>

There are several things to look for here :

  • the method attribute on the form tag : it tells us what methode to use. Here, it's POST.
  • the action attribute on the form tag : it tells us what path we should send the data to. Here, it's login.
  • the name attributes on input tags. They tell us what fields we're supposed to send. Here, it's user and pass.

With that and the credentials provided in the task we can construct the next request. Don't forget the Content-Length header since our request has a body !

We also need a Content-Type header indicating what format the body is in. Here, it's application/x-www-form-urlencoded.

Request:

POST /login HTTP 1.1
Host: flag.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 38

user=realbusinessuser&pass=potoooooooo

Response:

HTTP/1.1 302 Found
x-powered-by: Express
set-cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D; Path=/
location: /
vary: Accept
content-type: text/plain; charset=utf-8
content-length: 23
date: Mon, 15 Oct 2018 14:44:27 GMT
connection: close

Found. Redirecting to /

Great, we successfully logged in !

Managing cookies

So, we got a redirect response to /. If we were using a browser, it would automatically follow the redirect and take us to /. So we have to do that by hand. But it's not the only thing the browser would by doing for us.

HTTP is a stateless protocol, meaning the server is not keeping any state between the requests at the protocol level - if we just requested /, there would be no trace of our previous login.

The state is instead referenced to by a cookie. We can see it in the previous response's Set-Cookie header. Our browser would automatically save that cookie, and automatically send it in the request when the host and path match those of the cookie.

So the last thing we need to do is request / again, this time with the cookie. How do we attach a cookie ? Just use the Cookie header :

Request:

GET / HTTP 1.1
Host: flag.local
Cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D

Response:

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 438
etag: W/"1b6-eYJ8DUTdkgByyfWFi6OJJSjopFg"
date: Mon, 15 Oct 2018 15:02:11 GMT
connection: close
<html>
    <head>
        <link rel="stylesheet" type="text/css" href="main.css" />
    </head>
    <body>
        <header>
            <h1>Real Business Internal Flag Server</h1>
            <div class="user">Real Business Employee</div>
            <a href="/logout">Logout</a>
        </header>
        <main>
            <p>Hello <b>Real Business Employee</b>!  Today's flag is: <code>picoCTF{0nLY_Us3_n0N_GmO_xF3r_pR0tOcol5_2e14}</code>.</p>
        </main>
    </body>
</html>

And our flag is in the response : picoCTF{0nLY_Us3_n0N_GmO_xF3r_pR0tOcol5_2e14}