Skip to main content

FTP-Clients angreifen: Mit MGET mehr herunterladen als erwartet

Artikel von

4. April 2018

0 Min. Lesezeit

Einführung

Wir hören oft von Sicherheitslücken in HTTP-Clients wie Webbrowsern, die in der Regel durch bösartige Webinhalte ausgenutzt werden. Das ist nichts Neues. Aber wussten Sie, dass auch FTP-Clients Sicherheitslücken aufweisen können, die sich ausnutzen lassen? FTP-Clients können von bösartigen Servern angegriffen werden, mit denen sie sich verbinden.

In diesem Blogbeitrag stelle ich eine interessante Path-Traversal-Sicherheitslücke vor, die wir im November 2017 entdeckt und den betroffenen Anbietern verantwortungsvoll offengelegt haben. Die Sicherheitslücke kann mehrere Anwendungen und Bibliotheken betreffen und es einem bösartigen FTP-Server ermöglichen, Dateien an beliebiger Stelle im lokalen Dateisystem zu erstellen oder zu überschreiben. Wie die folgenden Details zeigen, ist die Ursache eine fehlende Validierung. Davon sind nicht nur FTP-Clients betroffen, sondern auch viele andere Anwendungen und Bibliotheken in verschiedenen Ökosystemen, etwa Java, npm und weitere.

Die Sicherheitslücke

Sehen wir uns das Problem genauer an! Wir möchten eine Funktion programmieren, die den Inhalt eines Remote-FTP-Ordners in einen lokalen Ordner herunterlädt. Wie die meisten von uns wissen, bietet das FTP-Protokoll selbst keinen Befehl zum Herunterladen eines Ordners. Wir können jedoch mehrere andere Befehle kombinieren, um unser Ziel zu erreichen.

Wir können:

  1. Alle Dateien im Remote-Ordner auflisten (FTP-Befehle LIST oder NLST)

  2. Für jede Datei in den obigen Listenergebnissen: Datei herunterladen und in einem lokalen Ordner speichern (FTP-Befehle GET oder MGET)

Ein Beispiel für Java-Code, der dieses Verhalten mithilfe der Bibliothek Apache commons-net umsetzt, könnte so aussehen:

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();
    }
  }
}

Der obige Code durchläuft jede vom Server zurückgegebene Datei und lädt sie in einen lokalen Zielordner herunter. Wenn beispielsweise die erste Datei im Remote-Ordner passwd heißt und unser lokaler Zielordner /var/data/sync/ lautet, wird die Datei nach /var/data/sync/passwd heruntergeladen.

Was aber, wenn der FTP-Server bösartig wird und als Antwort auf den LIST-Befehl nicht passwd, sondern ../../../../etc/passwd als Dateinamen zurückgibt? Der obige Code legt die Datei dann unter /var/data/sync/../../../../etc/passwd ab und überschreibt damit praktisch /etc/passwd mit der heruntergeladenen Datei.

Sie könnten einwenden, dass ../../../../etc/passwd kein gültiger Dateiname ist. Das stimmt. Doch im RFC steht nicht, dass dies kein gültiger Dateiname ist. Genau genommen macht das RFC keine Vorgaben zum Dateisystem und überlässt es Client und Server, dies selbst zu klären. Eine Antwort eines Windows-basierten FTP-Servers auf einen LIST-Befehl könnte beispielsweise so aussehen:

"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$",

Ein Unix-basierter Server liefert dagegen:

"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",

Tatsächlich gibt es viele weitere Dateisystemformate. Hier ist eine Liste der Formate, die von der Apache-commons-net-Bibliothek unterstützt werden:

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

Der typische FTP-Client validiert Dateinamen also nicht, sondern gibt sie unverändert zurück, damit Entwickler sie validieren. Die Validierung wird dabei häufig übersehen. Das wird besonders deutlich, wenn man sich Projekte auf GitHub oder verschiedene Snippet- und Beispiel-Websites wie StackOverflow und CodeJava ansieht.

Fallstudie: Apache HIVE

Apache Hive ist ein Data-Warehouse-Softwareprojekt, das auf Apache Hadoop aufbaut und Datenzusammenfassungen, Abfragen und Analysen ermöglicht. Hive bietet eine SQL-ähnliche Schnittstelle für Abfragen von Daten, die in verschiedenen Datenbanken und Dateisystemen gespeichert sind, die sich in Hadoop integrieren lassen. Unter anderem unterstützt Hive das Kopieren von Daten aus FTP-Servern über den Befehl 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

Im Code sehen wir den Aufruf 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;
}

In der Funktion retrieveFileList sehen wir, dass der vom Server zurückgegebene Dateiname ohne Validierung an den Verzeichnisnamen angehängt wird (name = dir + name;). Die Datei wird in eine Warteschlange gestellt und später heruntergeladen.

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);
          }
        }

Später wird im Downloader-Thread die Datei aus der Warteschlange entnommen und vom Server heruntergeladen.

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*/);
}

Ein möglicher Angriff besteht darin, die Datei authorized_keys des Root-Benutzers zu überschreiben und sich so später als Root anzumelden. Nehmen wir an, eine Apache-Hive-Instanz verbindet sich täglich mit unserem FTP-Server, um Händlerdaten herunterzuladen. Für diesen Angriff ändern wir unseren FTP-Server so, dass er dem Client bösartige Dateinamen mit Path Traversal zurückgibt. Beispielsweise könnten wir auf einen LIST-Befehl mit ../../../../../../../home/root/.ssh/authorized_keys antworten.

Wenn Hive diese Anweisung ausführt (und als Root läuft), wird die Datei authorized_keys des Root-Benutzers durch eine vom Angreifer kontrollierte Datei überschrieben.

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

Die oben beschriebene Sicherheitslücke wurde der Apache Foundation verantwortungsvoll offengelegt. Der zeitliche Ablauf:

Datum

Ereignis

2.11.2017

Sicherheitslücke von Snyk Security Research entdeckt

8.11.2017

Liste der betroffenen Apache-Produkte an die Foundation übermittelt.

5.2.2018

Apache teilte uns mit, dass bis Ende Februar eine korrigierte Version veröffentlicht werden soll.

4.4.2018

Beitrag veröffentlicht.

Details zum Apache-Hive-Projekt wurden am 4.4.2018 auch in der CVE-Datenbank veröffentlicht.CVE-2018-1315: Die Anweisung „COPY FROM FTP“ in HPL/SQL kann an beliebige Speicherorte schreiben, wenn der FTP-Server kompromittiert wurde:

  • Schweregrad: Mittel

  • Anbieter: The Apache Software Foundation

  • Betroffene Versionen: Hive 2.1.0 bis 2.3.2

  • Beschreibung: Wird die Anweisung „COPY FROM FTP“ mit der HPL/SQL-Erweiterung für Hive ausgeführt, kann ein kompromittierter/bösartiger FTP-Server bewirken, dass die Datei an einen beliebigen Speicherort auf dem Cluster geschrieben wird, auf dem der Befehl ausgeführt wird. Der Grund: Der FTP-Client-Code in HPL/SQL überprüft nicht, an welchem Speicherort die heruntergeladene Datei abgelegt wird. Die Benutzer hivecli und hiveserver2 sind davon nicht betroffen, da hplsql ein separates Kommandozeilenskript ist, das auf andere Weise aufgerufen werden muss.

  • Abhilfe: Benutzer, die HPL/SQL mit Hive 2.1.0 bis 2.3.2 einsetzen, sollten auf Version 2.3.3 aktualisieren. Alternativ lässt sich die Verwendung von HPL/SQL auf anderem Wege deaktivieren.

Zusammenfassung

Wir haben gezeigt, dass eine Sicherheitslücke in einigen FTP-Client-Anwendungen und -Bibliotheken darauf zurückzuführen ist, dass Daten vom FTP-Server nicht korrekt validiert werden. Ähnliche Probleme sind bereits früher aufgetreten. So stellte Steve Christey, Principal Information Security Engineer bei MITRE, im Jahr 2002 fest, dass das Problem bei mehreren FTP-Clients auftrat, darunter dem nativen Linux-FTP-Client und wget.

Eingaben müssen unbedingt validiert werden – und zwar nicht nur aus Sicherheitsgründen. Als Entwickler denkt man leicht daran, wie eine durchschnittliche Person unsere APIs nutzen könnte. Genauso wichtig ist es jedoch, mit unerwarteten Eingaben zu rechnen, die von Angreifern stammen könnten. Wenn Sie Verzeichnislisten von FTP-Servern verarbeiten, validieren Sie sie unbedingt: Dateinamen, die mit / beginnen oder .. enthalten, müssen blockiert werden.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.