Skip to main content

FTPクライアントへの攻撃:想定以上のファイルを取得するMGET

2018年4月4日

0 分で読めます

はじめに

WebブラウザーなどのHTTPクライアントの脆弱性が、悪意のあるWebコンテンツによって悪用されるという話はよく耳にします。これは目新しいことではありません。しかし、FTPクライアント自体にも脆弱性があり、悪用される可能性があることをご存じでしょうか。FTPクライアントは、接続先の悪意あるサーバーから攻撃を受ける可能性があります。

この記事では、2017年11月に発見し、影響を受ける複数のベンダーに責任ある形で開示した、興味深いパストラバーサル脆弱性をご紹介します。この脆弱性は複数のアプリケーションやライブラリに影響し、悪意のあるFTPサーバーによってローカルファイルシステム上の任意の場所にファイルを作成したり、上書きしたりできる可能性があります。以下で詳しく見ていくように、この脆弱性の原因は検証の欠如です。影響するのはFTPクライアントだけではなく、Javaやnpmなど、さまざまなエコシステムの多くのアプリケーションやライブラリにも及びます。

脆弱性

では、問題を詳しく見ていきましょう。リモートのFTPフォルダーの内容をローカルにダウンロードする関数を実装するとします。ご存じのとおり、FTPプロトコルにはフォルダーをダウンロードするコマンドはありませんが、ほかのいくつかのコマンドを組み合わせれば目的を達成できます。

次のように処理します。

  1. リモートフォルダー内のファイルをすべて一覧表示する(FTPのLISTまたはNLSTコマンド)

  2. 上記の一覧にある各ファイルについて、ファイルをダウンロードしてローカルフォルダーに保存する(FTPのGETまたはMGETコマンド)

Apache commons-netライブラリを使ってこの処理を行うJavaコードの例は、次のようになります。

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

上記のコードは、サーバーから返された各ファイルを順に処理し、ローカルの保存先フォルダーにダウンロードします。たとえば、リモートフォルダーにある最初のファイルの名前がpasswdで、ローカルの保存先が/var/data/sync/の場合、ファイルは/var/data/sync/passwdにダウンロードされます。

しかし、FTPサーバーが悪意のあるものに変わり、LISTコマンドに対してpasswdではなく、ファイル名として../../../../etc/passwdを返したらどうなるでしょうか。上記のコードはファイルを/var/data/sync/../../../../etc/passwdに保存し、結果として/etc/passwdをダウンロードしたファイルで上書きしてしまいます。

../../../../etc/passwdは有効なファイル名ではない、と思うかもしれません。確かにそうですが、RFCでは無効だとは定められていません。技術的にはファイルシステムに関して規定しておらず、クライアントとサーバーそれぞれの判断に委ねています。たとえば、WindowsベースのFTPサーバーがLISTコマンドに応答する形式はこのようになります。

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

一方、Unixベースのサーバーではこのようになります。

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

実際には、ほかにもさまざまなファイルシステム形式があります。apache commons-netライブラリがサポートする形式の一覧をご覧ください。

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

一般的なFTPクライアントはファイル名を検証せず、開発者が検証できるようにそのまま返します。当然ながら、検証は見落とされがちです。GitHubで公開されているプロジェクトや、StackOverflowやCodeJavaのような各種コードスニペット・サンプルサイトを見ると、これは明らかです。

ケーススタディ:Apache HIVE

Apache Hiveは、データの要約、クエリ、分析を提供するためにApache Hadoop上に構築されたデータウェアハウスソフトウェアです。Hiveは、Hadoopと連携するさまざまなデータベースやファイルシステムに保存されたデータをクエリするための、SQLに似たインターフェースを提供します。機能の一つとして、COPY-FROM-FTPコマンドを使った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

コードを確認すると、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;
}

retrieveFileList関数の中では、サーバーから返されたファイル名が検証されずにディレクトリ名へ追加されていることがわかります(name = dir + name;)。ファイルはキューに追加され、後でダウンロードされます。

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

その後、ダウンローダースレッドでキューからファイルが取り出され、サーバーからダウンロードされます。

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

考えられる攻撃の一つは、rootユーザーのSSH認証鍵ファイルauthorized_keysを上書きし、後からrootとしてログインできるようにすることです。Apache Hiveのインスタンスが、商取引データを毎日ダウンロードするために攻撃者のFTPサーバーへ接続すると仮定しましょう。この攻撃を実行するには、FTPサーバーを変更して、クライアントに悪意のあるパストラバーサルを含むファイル名を返すようにします。たとえば、LISTコマンドへの応答として../../../../../../../home/root/.ssh/authorized_keysを返すことができます。

Hiveがこのステートメントを実行すると(rootとして実行されている場合)、rootのauthorized_keys SSHファイルが、攻撃者が用意した内容で上書きされます。

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

上記の脆弱性はApache Foundationに責任ある形で開示しました。対応の経緯は以下のとおりです。

日付

イベント

2017年11月2日

Snyk Security Researchが脆弱性を発見

2017年11月8日

影響を受けるApache製品の一覧をFoundationに開示

2018年2月5日

Apacheから、2月末までに修正版をリリースする予定との連絡

2018年4月4日

記事を公開

Apache Hiveプロジェクトについて、詳細は2018年4月4日にCVEデータベースにも公開されました。CVE-2018-1315:FTPサーバーが侵害されている場合、HPL/SQLの「COPY FROM FTP」ステートメントによって任意の場所に書き込みが可能:

  • 深刻度:中

  • ベンダー:The Apache Software Foundation

  • 影響を受けるバージョン:Hive 2.1.0~2.3.2

  • 説明:HiveのHPL/SQL拡張を使用して「COPY FROM FTP」ステートメントを実行すると、侵害された、または悪意のあるFTPサーバーにより、コマンドを実行したクラスタ上の任意の場所にファイルを書き込まれる可能性があります。HPL/SQLのFTPクライアントコードが、ダウンロードしたコードの保存先を検証していないためです。HPL/SQLは別のコマンドラインスクリプトであり、個別に起動する必要があるため、hivecliユーザーおよびhiveserver2ユーザーには影響しません。

  • 緩和策:Hive 2.1.0~2.3.2でHPL/SQLを使用している場合は、2.3.3にアップグレードしてください。また、ほかの方法でHPL/SQLの使用を無効にすることもできます。

まとめ

一部のFTPクライアントアプリやライブラリに存在する脆弱性は、FTPサーバーからのデータが適切に検証されないことに起因する点を説明しました。同様の問題は過去にも見つかっています。たとえば2002年には、MITREの主任情報セキュリティエンジニアであるSteve Christeyが、Linux標準のFTPクライアントやwgetを含む複数のFTPクライアントでこの問題を発見しました。

セキュリティのためだけでなく、入力を検証することは不可欠です。開発者は、一般的なユーザーがAPIをどう使うかを考えがちですが、攻撃者による予想外の入力にも備えることが同じくらい重要です。FTPサーバーから受け取ったディレクトリ一覧を処理する際は、/で始まるファイル名や..を含むファイル名を拒否するなど、必ず検証してください。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップで、CTFチャレンジの解き方を学びましょう。

カテゴリー:

続きを読む

Blog

フロンティアモデルは脆弱性を発見した。攻撃者だけがエクスプロイトチェーンを見つけた。

静的解析で欠陥は見つかりましたが、ライブ攻撃テストで侵害につながる連鎖を実証できたのは唯一でした。Evo COS、Claude Security、Claude Code Securityを比較します。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

Blog

脆弱性のバックログはもはや技術的負債ではなく、攻撃対象領域です

増え続ける脆弱性のバックログは、単なる技術的負債ではありません。攻撃対象領域そのものです。古いリスクの前提、攻撃の自動化、そして連鎖する検出結果によって、なぜ新たなアプローチが求められているのかを解説します。