Skip to main content

Ataque a un cliente FTP: MGET y mucho más de lo que esperabas

Escrito por

4 de abril de 2018

0 minutos de lectura

Introducción

A menudo escuchamos sobre vulnerabilidades en clientes HTTP, como los navegadores web, que suelen ser explotadas por contenido web malicioso. Esto no es nada nuevo. Pero ¿sabías que los propios clientes FTP también pueden tener vulnerabilidades que se pueden explotar? Los servidores maliciosos a los que se conectan los clientes pueden atacarlos.

En esta publicación, mostraré una interesante vulnerabilidad de recorrido de rutas que identificamos y divulgamos de forma responsable a varios proveedores afectados en noviembre de 2017. Esta vulnerabilidad puede afectar varias aplicaciones y bibliotecas, y permitir que un servidor FTP malicioso cree o sobrescriba archivos en cualquier lugar del sistema de archivos local. Como verás en los detalles a continuación, esta vulnerabilidad se debe a la falta de validación y afecta no solo a los clientes FTP, sino también a muchas otras aplicaciones y bibliotecas de diversos ecosistemas, como Java, npm y otros.

La vulnerabilidad

Bien, ¡veamos el problema! Queremos programar una función que descargue el contenido de una carpeta FTP remota a una carpeta local. Como la mayoría ya sabemos, el protocolo FTP no ofrece un comando para descargar una carpeta, pero podemos combinar varios comandos para lograrlo.

Podemos hacer lo siguiente:

  1. Enumerar todos los archivos de la carpeta remota (comandos FTP LIST o NLST)

  2. Para cada archivo de la lista anterior: descargarlo y guardarlo en una carpeta local (comandos FTP GET o MGET)

Un ejemplo de código Java que realiza esta operación con la biblioteca Apache commons-net podría verse así:

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

El código anterior recorre cada archivo que devuelve el servidor y lo descarga en una carpeta de destino local. Por ejemplo, si el primer archivo de la carpeta remota se llama passwd y nuestra carpeta de destino local es /var/data/sync/, el archivo terminaría descargándose en /var/data/sync/passwd.

Pero ¿qué ocurre si el servidor FTP se vuelve malicioso y, en lugar de responder al comando LIST con passwd, devuelve ../../../../etc/passwd como nombre de archivo? El código anterior terminaría colocando el archivo en /var/data/sync/../../../../etc/passwd y, en la práctica, sobrescribiría /etc/passwd con el archivo recién descargado.

Podrías decir que ../../../../etc/passwd no es un nombre de archivo válido, y tienes razón. Pero el RFC no dice que no lo sea. Técnicamente, no toma partido sobre el sistema de archivos y deja que el cliente y el servidor lo resuelvan por su cuenta. Por ejemplo, una respuesta de un servidor FTP basado en Windows a un comando LIST podría verse así:

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

En cambio, una respuesta de uno basado en Unix se ve así:

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

De hecho, existen muchos otros formatos de sistemas de archivos. Aquí tienes una lista de los que admite la biblioteca apache commons-net:

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

Por lo general, los clientes FTP no validan los nombres de archivo: los devuelven tal cual para que los desarrolladores los validen. No hace falta decir que esta validación suele pasarse por alto. Esto es muy evidente al revisar proyectos alojados en github o distintos sitios con fragmentos y ejemplos de código, como StackOverflow y CodeJava.

Estudio de caso: Apache HIVE

Apache Hive es un proyecto de software de almacén de datos basado en Apache Hadoop que permite resumir, consultar y analizar datos. Hive ofrece una interfaz similar a SQL para consultar datos almacenados en diversas bases de datos y sistemas de archivos integrados con Hadoop. Entre otras funciones, permite copiar datos de servidores FTP mediante el comando 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

Al revisar el código, encontramos la llamada 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;
}

Dentro de la función retrieveFileList, vemos que el nombre de archivo que devuelve el servidor se agrega al nombre del directorio sin ninguna validación (name = dir + name;). El archivo se agrega a una cola para descargarlo más tarde.

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

Más adelante, en el hilo de descarga, se extrae el archivo de la cola y se descarga del servidor.

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

Un posible ataque consiste en sobrescribir el archivo authorized_keys de SSH del usuario root para poder iniciar sesión como root más adelante. Supongamos que una instancia de Apache Hive se conecta a nuestro servidor FTP para descargar datos de comerciantes a diario. Para llevar a cabo este ataque, modificaríamos nuestro servidor FTP para enviar al cliente nombres de archivo maliciosos con recorrido de rutas. Por ejemplo, podríamos responder a un comando LIST con ../../../../../../../home/root/.ssh/authorized_keys.

Cuando Hive ejecuta esta instrucción (suponiendo que se ejecuta como root), el archivo authorized_keys de SSH de root se sobrescribe con uno que conoce el atacante.

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

La vulnerabilidad anterior se divulgó de forma responsable a Apache Foundation. Cronología:

Fecha

Evento

2/11/2017

El equipo de investigación de seguridad de Snyk descubrió la vulnerabilidad.

8/11/2017

Se informó a la fundación la lista de productos Apache afectados.

5/2/2018

Apache nos informó que planeaba lanzar una versión corregida antes de fines de febrero.

4/4/2018

Se publicó la entrada.

Los detalles también se publicaron en la base de datos CVE el 4/4/2018 para el proyecto Apache Hive.CVE-2018-1315: la instrucción «COPY FROM FTP» de HPL/SQL puede escribir en cualquier ubicación si el servidor FTP está comprometido:

  • Gravedad: Moderada

  • Proveedor: The Apache Software Foundation

  • Versiones afectadas: Hive 2.1.0 a 2.3.2

  • Descripción: Cuando se ejecuta la instrucción «COPY FROM FTP» mediante la extensión HPL/SQL de Hive, un servidor FTP comprometido o malicioso puede hacer que el archivo se escriba en una ubicación arbitraria del clúster desde el que se ejecuta el comando. Esto se debe a que el código del cliente FTP de HPL/SQL no verifica la ubicación de destino del código descargado. Esto no afecta a los usuarios hivecli ni hiveserver2, ya que hplsql es un script de línea de comandos independiente que debe invocarse de otra manera.

  • Mitigación: Los usuarios de HPL/SQL con Hive 2.1.0 a 2.3.2 deben actualizar a la versión 2.3.3. Otra opción es desactivar el uso de HPL/SQL por otros medios.

Resumen

Hemos explicado cómo una vulnerabilidad presente en algunas aplicaciones y bibliotecas cliente FTP se debe a que los datos del servidor FTP no se validan correctamente. En el pasado se han encontrado problemas similares. Por ejemplo, en 2002, Steve Christey, ingeniero principal de seguridad de la información en MITRE, descubrió que el problema afectaba a varios clientes FTP, incluido el cliente FTP nativo de Linux y wget.

Validar las entradas es esencial, y no solo por motivos de seguridad. Como desarrollador, es muy fácil pensar en cómo usaría nuestras API una persona promedio, pero también es importante prever las entradas inesperadas que podrían provenir de atacantes. Al procesar listas de directorios de servidores FTP, asegúrate de validarlas y bloquear los nombres de archivo que comiencen con / o contengan ...

Empieza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.