Skip to main content

Attaquer un client FTP : obtenir plus que prévu avec MGET

Écrit par

4 avril 2018

0 minutes de lecture

Introduction

On entend souvent parler de vulnérabilités dans les clients HTTP, comme les navigateurs Web, généralement exploitées par du contenu Web malveillant : rien de nouveau jusque-là. Mais saviez-vous que les clients FTP eux-mêmes peuvent aussi présenter des vulnérabilités exploitables ? Des serveurs malveillants auxquels les clients se connectent peuvent les cibler.

Dans cet article, je vais vous présenter une vulnérabilité intéressante de traversée de répertoires que nous avons identifiée et signalée de manière responsable à plusieurs fournisseurs concernés en novembre 2017. Cette vulnérabilité peut toucher plusieurs applications et bibliothèques, et permettre à un serveur FTP malveillant de créer ou d’écraser des fichiers n’importe où dans le système de fichiers local. Comme vous le verrez dans les détails ci-dessous, cette vulnérabilité découle d’une absence de validation. Elle concerne non seulement les clients FTP, mais aussi de nombreuses autres applications et bibliothèques dans différents écosystèmes, notamment Java, npm et d’autres.

La vulnérabilité

D’accord, examinons le problème ! Nous voulons coder une fonction qui télécharge le contenu d’un dossier FTP distant vers un dossier local. Comme la plupart d’entre nous le savent déjà, le protocole FTP ne propose pas de commande de téléchargement de dossier, mais nous pouvons combiner plusieurs autres commandes pour atteindre notre objectif.

Nous pouvons :

  1. Lister tous les fichiers du dossier distant (commandes FTP LIST ou NLST)

  2. Pour chaque fichier de la liste ci-dessus : télécharger le fichier et l’enregistrer dans un dossier local (commandes FTP GET ou MGET)

Voici à quoi pourrait ressembler un exemple de code Java effectuant cette opération avec la bibliothèque Apache commons-net :

private void downloadDirectory(FTPClient ftpClient, String remoteDir, String localDir) throws IOException
{
  FTPFile[] subFiles = ftpClient.listFiles(remoteDir);
  for (FTPFile aFile : subFiles)
  {
    if (!aFile.isDirectory())
    {
       String remoteFile = ftpClient.printWorkingDirectory() + File.separator + aFile.getName();
       String localFile = localDir + File.separator + aFile.getName();

       OutputStream downloadedStream = new BufferedOutputStream(new FileOutputStream(new File(localFile)));
       boolean success = ftpClient.retrieveFile(remoteFile, downloadedStream);
       outputStream.close();
    }
  }
}

Le code ci-dessus parcourt chaque fichier renvoyé par le serveur et le télécharge dans un dossier de destination local. Par exemple, si le premier fichier du dossier distant s’appelle passwd et que notre dossier de destination local est /var/data/sync/, le fichier sera téléchargé sous /var/data/sync/passwd.

Mais que se passe-t-il si le serveur FTP devient malveillant et, au lieu de répondre à la commande LIST avec passwd, renvoie ../../../../etc/passwd comme nom de fichier ? Le code ci-dessus placera alors le fichier dans /var/data/sync/../../../../etc/passwd, écrasant de fait /etc/passwd avec le fichier nouvellement téléchargé.

Vous pourriez dire que ../../../../etc/passwd n’est pas un nom de fichier valide, et c’est vrai. Mais la RFC ne dit pas qu’il ne l’est pas. En théorie, elle reste neutre vis-à-vis du système de fichiers et laisse au client comme au serveur le soin de déterminer eux-mêmes ce qui est valide. Par exemple, la réponse d’un serveur FTP Windows à une commande LIST pourrait ressembler à :

"05-26-95  10:57AM               143712 $LDR$",
"05-20-97  03:31PM                  681 .bash_history",
"12-05-96  05:03PM       <DIR>          absoft2",
"11-14-97  04:21PM                  953 AUDITOR3.INI",
"05-22-97  08:08AM                  828 AUTOEXEC.BAK",
"01-22-98  01:52PM                  795 AUTOEXEC.BAT",
"05-13-97  01:46PM                  828 AUTOEXEC.DOS",
"12-03-96  06:38AM                  403 AUTOTOOL.LOG",
"12-03-96  06:38AM       <DIR>          123xyz",
"01-20-97  03:48PM       <DIR>          bin",
"05-26-1995  10:57AM               143712 $LDR$",

Tandis que celle d’un système Unix ressemble à :

"zrwxr-xr-x   2 root     root         4096 Mar  2 15:13 zxbox",
"dxrwr-xr-x   2 root     root         4096 Aug 24  2001 zxjdbc",
"drwxr-xr-x   2 root     root         4096 Jam  4 00:03 zziplib",
"drwxr-xr-x   2 root     99           4096 Feb 23 30:01 zzplayer",
"drwxr-xr-x   2 root     root         4096 Aug 36  2001 zztpp",
"-rw-r--r--   1 14       staff       80284 Aug 22  zxJDBC-1.2.3.tar.gz",
"-rw-r--r--   1 14       staff      119:26 Aug 22  2000 zxJDBC-1.2.3.zip",
"-rw-r--r--   1 ftp      no group    83853 Jan 22  2001 zxJDBC-1.2.4.tar.gz",
"-rw-r--r--   1ftp       nogroup    126552 Jan 22  2001 zxJDBC-1.2.4.zip",
"-rw-r--r--   1 root     root       111325 Apr -7 18:79 zxJDBC-2.0.1b1.tar.gz",
"drwxr-xr-x   2 root     root         4096 Mar  2 15:13 zxbox",

Il existe en réalité de nombreux autres formats de systèmes de fichiers. Voici la liste de ceux pris en charge par la bibliothèque apache commons-net :

OS400, AS400, L8, MVS, NETWARE, NT, OS2, UNIX, VMS, MACOS_PETER

Le client FTP classique ne valide donc pas les noms de fichiers et les renvoie tels quels, laissant aux développeurs le soin de les valider. Inutile de dire que cette validation est souvent oubliée. Cela saute aux yeux quand on consulte des projets hébergés sur GitHub ou divers sites proposant des extraits de code et des exemples, comme StackOverflow et CodeJava.

Étude de cas : Apache HIVE

Apache Hive est un projet d’entrepôt de données reposant sur Apache Hadoop, qui permet de résumer, d’interroger et d’analyser des données. Hive propose une interface de type SQL pour interroger des données stockées dans diverses bases de données et divers systèmes de fichiers intégrés à Hadoop. Il permet notamment de copier des données depuis des serveurs FTP à l’aide de la commande COPY-FROM-FTP.

COPY FROM FTP host [USER user [PWD password]] [DIR directory] [FILES files_wildcard]
  [TO [LOCAL] target_directory] [options]

options:
  OVERWRITE | NEW
  SUBDIR
  SESSIONS num

En examinant le code, nous voyons l’appel retrieveFileList().

/**
* Run COPY FROM FTP command
*/Integer run(HplsqlParser.Copy_from_ftp_stmtContext ctx) {
  trace(ctx, "COPY FROM FTP");
  initOptions(ctx);
  ftp = openConnection(ctx);
  if (ftp != null) {
    Timer timer = new Timer();
    timer.start();
    if (info) {
      info(ctx, "Retrieving directory listing");
    }
    retrieveFileList(dir);
    timer.stop();
    if (info) {
      info(ctx, "Files to copy: " + Utils.formatSizeInBytes(ftpSizeInBytes) + ", " + Utils.formatCnt(fileCnt, "file") + ", " + Utils.formatCnt(dirCnt, "subdirectory", "subdirectories") + " scanned (" + timer.format() + ")");
    }
    if (fileCnt > 0) {
      copyFiles(ctx);
    }
  }  
  return 0;
}

Dans la fonction retrieveFileList, le nom de fichier renvoyé par le serveur est ajouté au nom du répertoire sans aucune validation (name = dir + name;). Le fichier est ajouté à une file d’attente pour être téléchargé ultérieurement.

void retrieveFileList(String dir) {
  if (info) {
    if (dir == null || dir.isEmpty()) {
      info(null, "  Listing the current working FTP directory");
    }
    else {
      info(null, "  Listing " + dir);
    }
  }
  try {
    FTPFile[] files = ftp.listFiles(dir);
    ArrayList<FTPFile> dirs = new ArrayList<FTPFile>();
    for (FTPFile file : files) {
      String name = file.getName();
      if (file.isFile()) {
        if (filePattern == null || Pattern.matches(filePattern, name)) {
          if (dir != null && !dir.isEmpty()) {
            if (dir.endsWith("/")) {
              name = dir + name;
            }
            else {
              name = dir + "/" + name;
            }
          }
          if (!newOnly || !isTargetExists(name)) {
            fileCnt++;
            ftpSizeInBytes += file.getSize();
            filesQueue.add(name);
            filesMap.put(name, file);
          }
        }

Plus tard, dans le thread de téléchargement, le fichier est extrait de la file d’attente et téléchargé depuis le serveur.

java.io.File targetLocalFile = null;
File targetHdfsFile = null;
if (local) {
  targetLocalFile = new java.io.File(targetFile);
  if (!targetLocalFile.exists()) {            
    targetLocalFile.getParentFile().mkdirs();            
    targetLocalFile.createNewFile();
  }
  out = new FileOutputStream(targetLocalFile, false /*append*/);
}

Une attaque possible consiste à remplacer le fichier authorized_keys SSH de l’utilisateur root, afin de pouvoir se connecter ultérieurement en tant que root. Imaginons qu’une instance Apache Hive se connecte à notre serveur FTP pour télécharger chaque jour des données marchandes. Pour mener cette attaque, nous modifierions notre serveur FTP afin qu’il renvoie au client des noms de fichiers malveillants comportant des chemins traversant les répertoires. Par exemple, nous pourrions répondre à une commande LIST avec ../../../../../../../home/root/.ssh/authorized_keys.

Lorsque Hive exécute cette instruction (en supposant qu’il s’exécute en tant que root), le fichier SSH authorized_keys de root sera remplacé par un fichier connu de l’attaquant.

COPY FROM FTP remote.merchant.domain.com
USER 'foo' PWD '***'
DIR data/sales/in FILES  '.*'
TO /data/sales/raw OVERWRITE

La vulnérabilité ci-dessus a été signalée de manière responsable à l’Apache Foundation. Voici la chronologie :

Date

Événement

2/11/2017

Vulnérabilité découverte par Snyk Security Research

8/11/2017

Liste des produits Apache concernés communiquée à la fondation.

5/2/2018

Apache nous a informés de son intention de publier une version corrigée d’ici la fin du mois de février.

4/4/2018

Publication de l’article.

Les détails ont également été publiés dans la base de données CVE le 4/4/2018 pour le projet Apache Hive.CVE-2018-1315 : l’instruction « COPY FROM FTP » dans HPL/SQL peut écrire à n’importe quel emplacement si le serveur FTP est compromis :

  • Gravité : Modérée

  • Fournisseur : The Apache Software Foundation

  • Versions concernées : Hive 2.1.0 à 2.3.2

  • Description : Lorsqu’une instruction « COPY FROM FTP » est exécutée avec l’extension HPL/SQL de Hive, un serveur FTP compromis ou malveillant peut faire écrire le fichier à n’importe quel emplacement du cluster depuis lequel la commande est exécutée. En effet, le code du client FTP dans HPL/SQL ne vérifie pas l’emplacement de destination du code téléchargé. Cela ne concerne pas les utilisateurs hivecli et hiveserver2, car hplsql est un script en ligne de commande distinct qui doit être appelé différemment.

  • Mesures d’atténuation : Les utilisateurs de HPL/SQL avec Hive 2.1.0 à 2.3.2 doivent effectuer la mise à niveau vers la version 2.3.3. Il est également possible de désactiver HPL/SQL par d’autres moyens.

Résumé

Nous avons montré comment une vulnérabilité présente dans certains clients et bibliothèques FTP est due à l’absence de validation correcte des données provenant du serveur FTP. Des problèmes similaires ont déjà été observés. Par exemple, en 2002, Steve Christey, Principal Information Security Engineer chez MITRE, a découvert que le problème touchait plusieurs clients FTP, notamment le client FTP natif de Linux et wget.

Il est essentiel de valider les entrées, et pas seulement pour des raisons de sécurité. En tant que développeur, il est facile de penser à la façon dont une personne ordinaire pourrait utiliser nos API, mais il est tout aussi important de s’attendre aux entrées imprévues que pourraient envoyer des attaquants. Lorsque vous traitez des listes de répertoires provenant de serveurs FTP, veillez à les valider en bloquant les noms de fichiers qui commencent par / ou qui contiennent ...

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.

Lire la suite

Blog

Les modèles de pointe ont trouvé les vulnérabilités. Seul l’attaquant a trouvé les chaînes d’exploitation.

L’analyse statique a détecté les failles, mais seuls des tests d’attaque en conditions réelles ont prouvé comment elles pouvaient être enchaînées pour provoquer des compromissions. Comparaison d’Evo COS, de Claude Security et de Claude Code Security.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

Blog

Votre backlog de vulnérabilités n’est plus une dette technique, c’est une surface d’attaque

Un backlog de vulnérabilités qui s’allonge est plus qu’une dette technique : c’est une surface d’attaque. Découvrez pourquoi les anciennes hypothèses de risque, les attaquants automatisés et les vulnérabilités en chaîne exigent une nouvelle approche.