Skip to main content

Atacando um cliente FTP: o MGET pode trazer mais do que você esperava

Escrito por

4 de abril de 2018

0 minutos de leitura

Introdução

É comum ouvirmos falar de vulnerabilidades em clientes HTTP, como navegadores, geralmente exploradas por conteúdo malicioso na Web. Nada de novo nisso. Mas você sabia que os próprios clientes FTP também podem ter vulnerabilidades exploráveis? Servidores maliciosos aos quais os clientes se conectam podem atacá-los.

Nesta publicação, vou mostrar uma vulnerabilidade interessante de travessia de caminho que identificamos e divulgamos de forma responsável a vários fornecedores afetados em novembro de 2017. Essa vulnerabilidade pode afetar diversos aplicativos e bibliotecas, permitindo que um servidor FTP malicioso crie ou sobrescreva arquivos em qualquer lugar do sistema de arquivos local. Como você verá nos detalhes a seguir, o problema decorre da falta de validação e afeta não apenas clientes FTP, mas também muitos outros aplicativos e bibliotecas de diferentes ecossistemas, como Java, npm e outros.

A vulnerabilidade

Certo, vamos ao problema! Queremos criar uma função que baixe o conteúdo de uma pasta FTP remota para uma pasta local. Como a maioria de nós já sabe, o próprio protocolo FTP não oferece um comando para baixar pasta, mas podemos combinar vários outros comandos para atingir esse objetivo.

Podemos:

  1. Listar todos os arquivos da pasta remota (com os comandos FTP LIST ou NLST)

  2. Para cada arquivo listado acima: baixar o arquivo e salvá-lo em uma pasta local (com os comandos FTP GET ou MGET)

Um exemplo de código Java que faz isso usando a biblioteca Apache commons-net poderia ser assim:

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

O código acima percorre cada arquivo retornado pelo servidor e o baixa para uma pasta de destino local. Por exemplo, se o primeiro arquivo na pasta remota se chamar passwd e nossa pasta de destino local for /var/data/sync/, o arquivo será baixado para /var/data/sync/passwd.

Mas e se o servidor FTP se tornar malicioso e, em vez de responder ao comando LIST com passwd, retornar ../../../../etc/passwd como nome do arquivo? O código acima acabará colocando o arquivo em /var/data/sync/../../../../etc/passwd, praticamente sobrescrevendo /etc/passwd com o arquivo recém-baixado.

Você pode dizer que ../../../../etc/passwd não é um nome de arquivo válido — e de fato não é. Mas a RFC não diz que ele não é válido. Tecnicamente, ela não se posiciona sobre o sistema de arquivos e deixa que o cliente e o servidor decidam como lidar com isso. Por exemplo, a resposta de um servidor FTP baseado em Windows a um comando LIST pode ser assim:

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

Já um sistema baseado em Unix tem esta aparência:

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

Na verdade, há muitos outros formatos de sistema de arquivos. Veja a lista dos formatos compatíveis com a biblioteca apache commons-net:

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

Assim, o cliente FTP típico não valida os nomes de arquivo e os retorna como estão, deixando a validação a cargo dos desenvolvedores. Nem é preciso dizer que essa validação acaba sendo esquecida. Isso fica evidente ao analisar projetos hospedados no GitHub ou vários sites com exemplos e trechos de código, como StackOverflow e CodeJava.

Estudo de caso: Apache HIVE

O Apache Hive é um projeto de software de data warehouse desenvolvido sobre o Apache Hadoop para resumir, consultar e analisar dados. O Hive oferece uma interface semelhante a SQL para consultar dados armazenados em vários bancos de dados e sistemas de arquivos integrados ao Hadoop. Entre outros recursos, ele permite copiar dados de servidores FTP usando o 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

Ao analisar o código, encontramos a chamada 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 da função retrieveFileList, vemos que o nome de arquivo retornado pelo servidor é anexado ao nome do diretório sem nenhuma validação (name = dir + name;). O arquivo é adicionado a uma fila para ser baixado mais 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);
          }
        }

Mais tarde, na thread de download, o arquivo é retirado da fila e baixado do 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*/);
}

Um possível ataque consiste em sobrescrever o arquivo ssh authorized_keys do usuário root, permitindo fazer login como root posteriormente. Vamos supor que uma instância do Apache Hive se conecte ao nosso servidor FTP todos os dias para baixar dados de comerciantes. Para executar esse ataque, modificaríamos o servidor FTP para enviar ao cliente nomes de arquivo maliciosos com travessia de caminho. Por exemplo, poderíamos responder a um comando LIST com ../../../../../../../home/root/.ssh/authorized_keys.

Quando o Hive executar esse comando (supondo que esteja rodando como root), o arquivo authorized_keys do root será sobrescrito por um arquivo conhecido pelo invasor.

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

A vulnerabilidade acima foi divulgada de forma responsável à Apache Foundation. Veja a linha do tempo:

Data

Evento

11/2/2017

Vulnerabilidade descoberta pela equipe de pesquisa de segurança da Snyk

11/8/2017

Lista dos produtos Apache afetados divulgada à fundação.

2/5/2018

A Apache nos informou que planejava lançar uma versão corrigida até o fim de fevereiro.

4/4/2018

Publicação do artigo.

Os detalhes também foram publicados no banco de dados CVE em 4/4/2018 para o projeto Apache Hive.CVE-2018-1315: a instrução ‘COPY FROM FTP’ em HPL/SQL pode gravar em qualquer local se o servidor FTP estiver comprometido:

  • Gravidade: Moderada

  • Fornecedor: The Apache Software Foundation

  • Versões afetadas: Hive 2.1.0 a 2.3.2

  • Descrição: Quando a instrução ‘COPY FROM FTP’ é executada usando a extensão HPL/SQL do Hive, um servidor FTP comprometido ou malicioso pode fazer com que o arquivo seja gravado em qualquer local do cluster em que o comando é executado. Isso acontece porque o código do cliente FTP no HPL/SQL não verifica o local de destino do código baixado. Isso não afeta os usuários hivecli e hiveserver2, pois hplsql é um script de linha de comando separado e precisa ser invocado de outra forma.

  • Mitigação: Quem usa HPL/SQL com Hive 2.1.0 a 2.3.2 deve atualizar para a versão 2.3.3. Como alternativa, é possível desativar o uso de HPL/SQL por outros meios.

Resumo

Mostramos como uma vulnerabilidade presente em alguns aplicativos e bibliotecas de clientes FTP é causada pela falta de validação adequada dos dados recebidos do servidor FTP. Problemas semelhantes já foram identificados. Por exemplo, em 2002, Steve Christey, engenheiro principal de segurança da informação na MITRE, descobriu que o problema existia em vários clientes FTP, incluindo o cliente FTP nativo do Linux e o wget.

Validar entradas é essencial — e não apenas por segurança. Como desenvolvedor, é muito fácil pensar em como uma pessoa comum usaria nossas APIs, mas também é fundamental prever entradas inesperadas que possam vir de invasores. Ao processar listagens de diretórios recebidas de servidores FTP, valide-as: bloqueie nomes de arquivo que comecem com / ou contenham ...

Comece a participar de desafios de Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.