La vulnérabilité à détecter pour ce challenge était une divulgation de fichiers locaux due à une limitation des chemins d’accès incorrect. La résolution du challenge ne demande pas de connaissance particulière, mais simplement de lire la documentation du cadriciel web Actix afin de comprendre le comportement des méthodes.

Note : Cet article est aussi disponible en anglais 🇬🇧. Le challenge a été annoncé dans ce tweet 🐦.

Explication

Une application peut servir des pages dynamiques, mais également des fichiers statiques (CSS, JavaScript, images, etc.).
Ici, l’application affiche l’image /public/static/polygons.svg sur la page d’accueil. Cette image est servie grâce à la fonction r#static() sous la route /public/. Cependant, la fonction r#static() n’effectue aucun filtrage et se contente de retourner le fichier passer en argument. Au niveau de la route, un filtrage pourrait être positionné à l’aide d’une expression régulière pour correspondre à certains types de fichier seulement. Néanmoins, ce n’est pas le cas, l’expression régulière .* est utilisé et autorise donc tout type de fichiers. Il n’y a alors aucune restriction sur le type de fichier et un attaquant peut ainsi demander tout fichier sur le système qui est lisible avec les permissions de l’application.

Il est, par exemple, possible de remonter l’arborescence pour lire des fichiers à l’extérieur de la racine du serveur web.

➜ curl 'http://127.0.0.1:8888/public/../../../../../../etc/os-release' --path-as-is
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/"
LOGO=archlinux-logo

Ou encore de lire le code source de l’application.

➜ curl 'http://127.0.0.1:8888/public/examples/app-vuln.rs'
use actix_files::NamedFile;
use actix_web::{get, HttpRequest, HttpResponse, Responder, Result};
use std::path::PathBuf;

#[get("/")]
async fn index() -> impl Responder {
  let html = "<!doctype html><html><body><h1>Polygons!</h1><img src=\"/public/static/polygons.svg\"></body></html>";
  HttpResponse::Ok().body(html)
}

async fn r#static(req: HttpRequest) -> Result<NamedFile> {
  let path: PathBuf = req.match_info().query("filename").parse().unwrap();
  Ok(NamedFile::open(path)?)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
  use actix_web::{web, App, HttpServer};

  HttpServer::new(||
    App::new()
      .service(index)
      .route("/public/{filename:.*}", web::get().to(r#static))
    )
    .bind(("127.0.0.1", 8888))?
    .run()
    .await
}

La documentation d’Actix, averti du danger de ce genre de case d’usage :

Matching a path tail with the [.*] regex and using it to return a NamedFile has serious security implications. It offers the possibility for an attacker to insert ../ into the URL and access every file on the host that the user running the server has access to.

Cependant, utiliser une expression régulière plus restrictive n’est pas suffisant. Si l’on change /public/{filename:.*} en /public/{filename:static/.+\.svg} cela améliore sensiblement les choses en empêchant de requêter tous types de fichier.

  • curl 'http://127.0.0.1:8888/public/static/polygons.svg' (commence par static/ et fini par .svg)
  • curl 'http://127.0.0.1:8888/public/examples/app-vuln.rs' (commence par examples/ et fini par .rs)

À première vue, on pourrait se dire que cela suffit, mais il est toujours possible de remonter l’arborescence de fichier pour lire en dehors de la racine du serveur web d’autres fichiers du même type.

  • curl 'http://127.0.0.1:8888/public/static/../../../../../../home/noraj/Pictures/logo_acceis_black.svg' --path-as-is (commence par static/ et fini par .svg)

Dans une application réelle, l’expression régulière aurait peut-être fait une liste blanche des extensions courantes de fichiers statiques : .js, .css, .xml, .png, …

Le .js pourrait permettre de lire un fichier config.js d’une autre application, le .png des images privées des utilisateurs, car la route autorise certain nom de fichier ou non mais ne remplace pas un contrôle d’accès basé sur les permissions de l’utilisateur, le .xml qui permettrait de légitimement accéder au sitemap.xml permettrait aussi de récupérer des fichiers de configurations sensibles, etc.

Code corrigé

Voici donc le code corrigé :

Code corrigé

Utiliser une expression régulière comme règle de routage n’est définitivement pas la bonne manière de procéder pour implémenter du contrôle d’accès.

Dans le but de servir des fichiers statiques, il est plus simple et plus sécurisé de servir un dossier spécifique. App::service() s’assurera qu’il n’est pas possible de remonter en dehors de ce dossier.

Il s’agit toutefois de servir des fichiers publics sans contrôle d’accès (feuille de style, scripts, …). L’erreur à ne pas commettre serait alors de positionner un répertoire upload/, permettant aux utilisateurs de déposer leur fichier personnels, comme route statique puisque tout le monde y aurait accès. C’est malheureusement souvent le cas quand le développeur veut donner accès à des fichiers publics de l’entreprise (ex : livre-blanc.pdf, résultats-annuels.pdf) qui se trouvent dans upload/, ne se rendant pas compte qu’en donnant accès à ce dossier, il donne aussi accès à upload/users/toto/document-discussion-privée.odt. Il faut donc veiller à ce que les dossiers servis par des routes de fichier statiques soient tous publics et gérer les fichiers confidentiels avec un mécanisme de contrôle d’accès même si ceux-ci sont statiques.

Diff

Le code source est disponible sur le dépôt Github Acceis/vulnerable-code-snippets et sur le site web acceis.github.io/avcs-website.

À propos de l’auteur

Article écrit par Alexandre ZANNI alias noraj, Ingénieur en Test d’Intrusion chez ACCEIS.