Sécurité Web 13 Septembre 2022

4 points de sécurisation nginx méconnus

Image de présentation pour

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’application Flask 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 :

Retrouvez cet article en vidéo

Merci d'avoir lu cet article !

Si vous avez des commentaires ou des retours sur cet article, n'hésitez pas à me contacter. Venez aussi me dire bonjour sur Twitter ou sur Linkedin.