Nginx est un serveur web et un reverse-proxy. Comme tout outils, une revue de configuration permet de mettre en avant des faiblesses liées à sa configuration.
L’éditeur donne quelques bonnes pratiques et sur internet il existe de nombreux sites présentant les points de configuration de base. Cet article détaille certains points de contrôle qui sont moins connus.
Pour la simplicité de l’article, je vous propose Dockerfile
pour faire vos propre tests. Il déploie une application Python
basée sur Flask
avec un service nginx
en tant que reverse-proxy. Quelque soit la manière dont vous utilisez le serveur, les constats restent pertinents.
Off-By-Slash : mauvaise configuration des fichiers statiques
Même avec une configuration d’un reverse-proxy, il est intéressant de servir directement les ressources statiques par le serveur plutôt que de passer la requête au serveur suivant. Cela permet de gérer finement le cache ou encore de réduire les journaux d’évènements.
Un exemple de mauvaise configuration est le suivant :
location /static {
alias /opt/app/static/;
}
Ici, le serveur va vérifier si l’URI commence par /static
, si c’est le cas alors il retourne le fichier correspondant dans le dossier /opt/app/static/
. L’attaquant va envoyer la requête suivante :
http://localhost/static../app.py
Cette URI commence bien par /static
, le serveur retourne le fichier /opt/app/static/../app.py
. Il y a ici un parcours de répertoire, l’attaquant peut lire les fichiers du répertoires de l’application /opt/app
.
Dans le configuration du serveur nginx
, il manque un /
(Off-By-Slash), elle devrait être :
location /static/ {
alias /opt/app/static/;
}
Avec cette configuration, l’URI n’est pas valide car /static..
n’est pas géré directement par nginx
, elle sera traitée par le block de configuration suivant.
Modification de l’attribut merge_slashes
L’attribut merge_slashes
indique au serveur qu’il doit combiner les caractères slashes (/
) consécutifs. Par exemple, si l’URL contient la suite de slashes ////
alors cette URL sera passée au serveur suivant avec un unique slash.
Par défaut la valeur est positionnée à on
, la directive est donc en place, mais il est possible de la positionner à off
. Dans ce cas, le serveur ne va pas faire le regroupement et il est possible que l’application suivante ne traite pas correctement les slashes et provoque une vulnérabilité de type Path Traversal
.
Exemple d’une mauvaise configuration :
from flask import Flask, send_file
app = Flask(__name__)
@app.route("/<path:path>")
def vuln(path):
return send_file(path)
if __name__ == "__main__":
app.run(debug=True, port=8000, host='127.0.0.1')
La requête suivante directement sur le serveur vulnerable retourne un fichier sur le serveur :
GET ////../../../../../../../../../etc/passwd HTTP/1.1
Host: 127.0.0.1:8000
Dans le cas d’un reverse-proxy avec la configuration à off, l’URL ne sera pas modifiée. Il sera donc possible d’exploiter la vulnérabilité. Dans le cas où la configuration est à on
(par défaut), l’URI envoyée au serveur vulnérable est la suivante :
GET /etc/paswd HTTP/1.1
Host: 127.0.0.1:8000
Le fichier n’est pas retourné.
Il convient donc de s’assurer que la directive merge_slashes
n’est pas présente dans un fichier de configuration ou, si elle l’est, qu’elle a bien la valeur on
.
Injection d’en-tête dans le cas d’une ré-écriture
L’avantage d’un reverse-proxy est qu’il peut avoir comme fonction la ré-écriture d’URI ou la terminaison SSL/TLS. C’est à dire que c’est ce serveur qui traitera le chiffrement entre l’utilisateur et l’entrée sur votre infrastructure (la DMZ par exemple).
Il convient donc de rediriger les applications de HTTP vers HTTPS de manière systématique. Exemple d’une mauvaise configuration :
location /crlf {
return 302 http://localhost/$uri;
}
La variable $uri
est interne à nginx
et comme la documentation l’indique, elle est “normalisée”.
Ceci implique que les caractères encodés dans l’URL (%20, …) seront décodés. Ainsi, lors de la redirection, l’URL retournée à l’utilisateur ne sera pas “url-encoded”. Dans le cas où l’URL demandée contient des retours à la ligne (CRLF %0D%0A
), ils seront présents décodés dans la réponse, ce qui correspond à une nouvelle ligne et donc un nouvel en-tête en HTTP.
Par exemple, lorsque l’utilisateur demande la page suivante :
http://localhost/crlf%0D%0AX-Ubh-Header:%20New-Header-Value
La réponse est la suivante :
HTTP/1.1 301 Moved Permanently
Server: nginx
...
Location: http://localhost//crlf
X-Ubh-Header: New-Header-Value
Un attaquant peut injecter un nouvel en-tête (comme Set-Cookie
) dans le navigateur de sa victime.
Note pour l’exploitation : pour surcharger un Cookie
déjà existant, pensez à mettre l’attribut Path
au plus près du chemin souhaité. En effet, la RFC6265 indique que le navigateur “devrait” (SHOULD) trier les cookies par longueur du paramètre Path. Si vous avez un Path
plus précis que le cookie envoyé par l’application, alors il sera pris en compte à la place de celui normalement utilisé par l’application.
2. The user agent SHOULD sort the cookie-list in the following
order:
* Cookies with longer paths are listed before cookies with
shorter paths.
* Among cookies that have equal-length path fields, cookies with
earlier creation-times are listed before cookies with later
creation-times.
Pour se prémunir contre se type d’attaque, il convient de bien utiliser la variable $request_uri
qui contient l’URI originale.
Récupération d’un fichier à l’aide de try_files
Une autre mauvaise configuration permet de lire des fichiers du répertoire de l’application. La directive try_files
permet de tester si un fichier existe et le retourne. Si ce n’est pas le cas, les prochaines valeurs de la directive sont testées.
Dans l’exemple suivant, le premier block location
contient une directive try_files
qui va tester une a une les valeurs indiquées :
$uri
: vérifie si le fichier existe sur le serveur et le retourne@flask
: appelle l’applicationFlask
avec le chemin
root /opt/app;
location / {
try_files $uri @flask;
}
location @flask {
proxy_pass http://127.0.0.1:8000;
}
L’attaquant récupère le code source de l’application en spécifiant le fichier Python
souhaité :
http://localhost/app.py
Le fichier app.py
contenant le code source Flask sera retourné par le serveur nginx
car il existe bien dans le répertoire de l’application.
Des exemples pour tester
Je met à disposition un Dockerfile sur mon Github pour tester les vulnerabilities présentées. Pour créer l’image et l’exécuter :
docker build --no-cache -t nginx-image:latest .
docker run -d -p 80:80 --name nginx-test-hardening nginx-image
Ensuite, sur le port 80 de votre machine (c’est le premier 80 dans la deuxième ligne), vous pouvez exploiter les vulnérabilités :
- Off-By-Slash :
http://localhost/static../app.py
- CRLF injection :
http://localhost/crlf%0d%0aSet-Cookie:%20auth=test
merge_slashes off
:http://localhost///////../../../../../etc/passwd
(depuis burp pour éviter la ré-écriture du navigateur)try_files
:http://localhost/app.py