FTPクライアントのPassiveモード時の接続方法を調べてみた
しばたです。
仕事でFTPサーバーへの接続状況を調査する必要があり、その際にクライアント毎でPassiveモードでの接続方法(実装)が結構違うことに気が付きました。
本記事ではその内容を雑多に解説します。
FTPのPassiveモードについて
FTPのPassiveモードが何ぞやという点については本記事では触れません。
こちらのサイトがわかりやすいので参照してください。
本記事において主題となる点は以下となります。
- クライアントから
PASV
コマンドを発行した際サーバーは使用する IPアドレス とポート番号を返す - パブリックに公開されるFTPサーバーの場合
PASV
コマンドで返されるIPアドレスがPrivateにならない様にするための設定がある - 主にIPV6向けに拡張Passiveモードがあり、この場合
EPSV
コマンドを使うEPSV
コマンドではIPアドレスは返さず使用するポート番号のみ返す
FTPクライアントによってPASV
、EPSV
どちらを使うか、PASV
で返されるIPアドレスをどう取り扱うかが結構異なっていました。
確認してみた
ここから実際にいくつかのクライアントで確認した結果を記載していきます。
私が普段Windows環境を使うため、基本的にはWindows用クライアントを選んでいます。
また、選定したクライアントに関しては「私が普段よく使う。単純に使い慣れてる。」だけでそれ以外の意図は特にありません。
検証環境
検証環境は私の検証用AWSアカウント上にパブリックに公開する形でFTPサーバーを立てました。
FTPDにはProFTPDを選び、ざっくり以下の設定としています。
- 最新のAmazon Linux 2にProFTPDをよしなにインストール
- EPELリポジトリからVer.1.3.5eをインストール
- xinetdモード
- セキュリティグループはよしなに開けておく
PassivePorts
パラメーターを60000
~61000
に- 動作確認のため意図的に
MasqueradeAddress
パラメーターは設定せず
具体的な構築手順は割愛します。
図にすると以下の通りです。
今回割り当てたEIPは35.76.153.xx
(xxは伏字)だったため、このアドレスに接続する形となります。
また、FTP接続に使うユーザーはscott
、パスワードはtiger
としています。
(わかりやすいサンプルユーザーとパスワードを考えたらこれが出てきました...)
1. Telnetコマンドで接続する
まずは基本的な動作を確認するためTelnetコマンドで試してみます。
今回はWSL(Ubuntu 20.04)にあるコマンドを使っています。
最初にtelnet 35.76.153.xx 21
を実行するとサーバーから応答が返りますので、ここからFTPコマンドを打ってコネクションを確立していきます。
$ telnet 35.76.153.xx 21
Trying 35.76.153.xx...
Connected to 35.76.153.xx.
Escape character is '^]'.
220 FTP Server ready.
USER
、PASS
コマンドで認証を行い、PASV
コマンドを打つとサーバーから使用するIPアドレスとポート番号が返されます。
USER scott
331 Password required for scott
PASS tiger
230 User scott logged in
PASV
227 Entering Passive Mode (10,0,11,251,235,233).
今回MasqueradeAddress
パラメーターを指定してないので10,0,11,251
とPrivate IPが返されていることがわかります。
ポート番号は235,233
から235 x 256 + 233 = 60393
を使う様に指示されています。
サーバーの指示通り10.0.11.251
を使ってしまうと当然接続はできません。
ちなみにMasqueradeAddress
パラメーターを指定した場合はこんな感じでEIPのアドレスを返し、これにより無事接続できる様になります。
PASV
227 Entering Passive Mode (35,76,153,xx,236,2).
また、PASV
でなくEPSV
コマンドを打つとこんな感じでポート番号のみ返されます。
EPSV
229 Entering Extended Passive Mode (|||60541|)
Passiveモードに入ったあとの手順は割愛しますが、これで通信の基本的なところは理解できたと思います。
2. WinSCPで接続する
次に私が良く使うWinSCPで試します。
バージョンはVer.5.19.2を使っています。
下図の様な設定でPassiveモードで接続する様にしています。
この状態でFTPサーバーに接続を試みます。
前節のTelnetで試した様にPASV
コマンドの結果はPrivate IPを返す状態のため接続エラーになるはずなのですが、実際には無事接続できています。
この挙動が初見では全く理解できず悩んでいたのですが、デバッグログを取得してみると以下の様なログを吐いていました。
# ・・・前略・・・
< 2021-09-23 09:24:14.611 220 FTP Server ready.
> 2021-09-23 09:24:14.612 USER scott
< 2021-09-23 09:24:14.638 331 Password required for scott
> 2021-09-23 09:24:14.638 PASS *****
< 2021-09-23 09:24:14.684 230 User scott logged in
# ・・・中略・・・
> 2021-09-23 09:24:15.068 PASV
< 2021-09-23 09:24:15.093 227 Entering Passive Mode (10,0,11,251,235,44).
. 2021-09-23 09:24:15.093 Server sent passive reply with unroutable address 10.0.11.251, using host address instead.
> 2021-09-23 09:24:15.093 MLSD
. 2021-09-23 09:24:15.093 35.76.153.xx:60204 に接続中...
< 2021-09-23 09:24:15.151 150 Opening ASCII mode data connection for MLSD
. 2021-09-23 09:24:15.154 modify=20210923001720;perm=flcdmpe;type=cdir;unique=CA01UA3C75;UNIX.group=1002;UNIX.mode=0700;UNIX.owner=1002; .
. 2021-09-23 09:24:15.154 modify=20210923001720;perm=flcdmpe;type=pdir;unique=CA01UA3C75;UNIX.group=1002;UNIX.mode=0700;UNIX.owner=1002; ..
. 2021-09-23 09:24:15.154 modify=20200715055811;perm=adfrw;size=18;type=file;unique=CA01UA3C78;UNIX.group=1002;UNIX.mode=0644;UNIX.owner=1002; .bash_logout
. 2021-09-23 09:24:15.154 modify=20200715055811;perm=adfrw;size=193;type=file;unique=CA01UA3C79;UNIX.group=1002;UNIX.mode=0644;UNIX.owner=1002; .bash_profile
. 2021-09-23 09:24:15.154 modify=20200715055811;perm=adfrw;size=231;type=file;unique=CA01UA3C7A;UNIX.group=1002;UNIX.mode=0644;UNIX.owner=1002; .bashrc
. 2021-09-23 09:24:15.154 modify=20210923001641;perm=adfrw;size=37990;type=file;unique=CA01UA3C7B;UNIX.group=1002;UNIX.mode=0644;UNIX.owner=1002; 騾イ謐励←縺・〒縺吶°.jpg
. 2021-09-23 09:24:15.154 modify=20210923001720;perm=adfrw;size=12;type=file;unique=CA01UA3C7C;UNIX.group=1002;UNIX.mode=0644;UNIX.owner=1002; sample.txt
. 2021-09-23 09:24:15.166 Data connection closed
< 2021-09-23 09:24:15.181 226 Transfer complete
. 2021-09-23 09:24:15.181 ディレクトリ一覧の取得が成功しました
# ・・・後略・・・
ログを見ると、
< 2021-09-23 09:24:15.093 227 Entering Passive Mode (10,0,11,251,235,44).
. 2021-09-23 09:24:15.093 Server sent passive reply with unroutable address 10.0.11.251, using host address instead.
とPASV
コマンドで戻されたIPアドレスを意図的に無視していることが分かりました。
この挙動はFTP設定の「パッシブモードでIPアドレスを強制する (Force IP Address for passive mode)」で制御されており、「自動」または「オン」にするとPASV
コマンドで返されたIPアドレスを無視します。
この設定を「オフ」にするとPASV
コマンドで返されたIPを使用する様になります。
この場合正しく?接続エラーとなります。
エラー時のデバッグログもこんな感じです。
> 2021-09-23 09:30:49.400 PASV
< 2021-09-23 09:30:49.424 227 Entering Passive Mode (10,0,11,251,238,18).
> 2021-09-23 09:30:49.424 MLSD
. 2021-09-23 09:30:49.424 10.0.11.251:60946 に接続中...
. 2021-09-23 09:31:04.143 タイムアウトしました。 (データ用接続)
. 2021-09-23 09:31:04.143 ディレクトリの一覧を取得できません。
. 2021-09-23 09:31:04.143 Got reply 1004 to the command 2
* 2021-09-23 09:31:04.200 (EFatal) **切断されました。**
* 2021-09-23 09:31:04.200 タイムアウトしました。 (データ用接続)
* 2021-09-23 09:31:04.200 ディレクトリの一覧を取得できません。
* 2021-09-23 09:31:04.200 ディレクトリ '/' のリスト取得のエラー。
3. curlで接続する
次にcurlコマンドで試します。
今回は手元のWindows 10に標準でインストールされているVer.7.55.1を使います。
PS C:\> curl.exe -V
curl 7.55.1 (Windows) libcurl/7.55.1 WinSSL
curlの場合もWinSCP同様に予想に反してFTP接続できてしまいました。
PS C:\> curl.exe ftp://scott:tiger@35.76.153.xx/
-rw-r--r-- 1 scott scott 12 Sep 23 00:17 sample.txt
-rw-r--r-- 1 scott scott 37990 Sep 23 00:16 進捗どうですか.jpg
curlでは--trace
パラメーターを付けるとトレースログを取得できますので内容を確認していきます。
PS C:\> curl.exe --trace - ftp://scott:tiger@35.76.153.xx/
# ・・・(中略)・・・
=> Send header, 6 bytes (0x6)
0000: 45 50 53 56 0d 0a EPSV..
== Info: Connect data stream passively
== Info: ftp_perform ends with SECONDARY: 0
<= Recv header, 48 bytes (0x30)
0000: 32 32 39 20 45 6e 74 65 72 69 6e 67 20 45 78 74 229 Entering Ext
0010: 65 6e 64 65 64 20 50 61 73 73 69 76 65 20 4d 6f ended Passive Mo
0020: 64 65 20 28 7c 7c 7c 36 30 34 36 39 7c 29 0d 0a de (|||60469|)..
# ・・・(後略)・・・
すると、curlでFTPを扱う場合サーバー側で対応しているとデフォルトでEPSV
を使う挙動となっていることが分かりました。
今回のサーバーではEPSV
を制限していなかったため問題なく結果を返したわけです。
- 参考 : CURL OOTW: –FTP-PASV
ちなみにPASV
コマンドを強制したい場合は--disable-epsv
オプションを付けてやる必要があります。
この場合、
PS C:\> curl.exe --trace - --disable-epsv ftp://scott:tiger@35.76.153.xx/
# ・・・(中略)・・・
=> Send header, 6 bytes (0x6)
0000: 50 41 53 56 0d 0a PASV..
== Info: Connect data stream passively
== Info: ftp_perform ends with SECONDARY: 0
<= Recv header, 50 bytes (0x32)
0000: 32 32 37 20 45 6e 74 65 72 69 6e 67 20 50 61 73 227 Entering Pas
0010: 73 69 76 65 20 4d 6f 64 65 20 28 31 30 2c 30 2c sive Mode (10,0,
0020: 31 31 2c 32 35 31 2c 32 33 37 2c 31 36 38 29 2e 11,251,237,168).
0030: 0d 0a ..
== Info: Trying 10.0.11.251...
== Info: TCP_NODELAY set
== Info: Connecting to 10.0.11.251 (10.0.11.251) port 60840
== Info: connect to 10.0.11.251 port 21 failed: Timed out
== Info: Failed to connect to 35.76.153.xx port 21: Timed out
== Info: Closing connection 0
curl: (7) Failed to connect to 35.76.153.xx port 21: Timed out
といった感じでPASV
コマンドを発行し、接続エラーとなります。
ただ、より新しいバージョン(Ver.7.74.0以降)のcurlではWinSCP同様PASV
コマンドが返すIPを無視するオプション(--ftp-skip-pasv-ip
)がデフォルトになっています。
# 新しいバージョンのcurlを別途用意
PS C:\curl\bin> .\curl.exe -V
curl 7.79.1 (x86_64-pc-win32) libcurl/7.79.1 OpenSSL/3.0.0 (Schannel) zlib/1.2.11 brotli/1.0.9 zstd/1.5.0 libidn2/2.3.2 libssh2/1.10.0 nghttp2/1.45.1 libgsasl/1.10.0
新しいバージョンのcurlで先程と同様にFTP接続を実施、
# 新しいバージョンのcurlでFTP接続
PS C:\curl\bin> .\curl.exe --trace - --disable-epsv ftp://scott:tiger@35.76.153.xx/
# ・・・(中略)・・・
=> Send header, 6 bytes (0x6)
0000: 50 41 53 56 0d 0a PASV..
== Info: Connect data stream passively
== Info: ftp_perform ends with SECONDARY: 0
<= Recv header, 49 bytes (0x31)
0000: 32 32 37 20 45 6e 74 65 72 69 6e 67 20 50 61 73 227 Entering Pas
0010: 73 69 76 65 20 4d 6f 64 65 20 28 31 30 2c 30 2c sive Mode (10,0,
0020: 31 31 2c 32 35 31 2c 32 33 38 2c 35 36 29 2e 0d 11,251,238,56)..
0030: 0a .
== Info: Skip 10.0.11.251 for data connection, re-use 35.76.153.xx instead
== Info: Trying 35.76.153.xx:60984...
== Info: Connecting to 35.76.153.xx (35.76.153.xx) port 60984
== Info: Connected to 35.76.153.xx (35.76.153.xx) port 21 (#0)
# ・・・(後略)・・・
結果
== Info: Skip 10.0.11.251 for data connection, re-use 35.76.153.xx instead
== Info: Trying 35.76.153.xx:60984...
とPASV
コマンドの返すIPアドレスが無視されていることが分かります。
ちなみに古いバージョンのcurlでも--ftp-skip-pasv-ip
オプションを付けてやると同じ挙動になります。
# 古いバージョンなら --ftp-skip-pasv-ip オプションを付けてやると良い
PS C:\> curl.exe --disable-epsv --ftp-skip-pasv-ip ftp://scott:tiger@35.76.153.xx/
-rw-r--r-- 1 scott scott 12 Sep 23 00:17 sample.txt
-rw-r--r-- 1 scott scott 37990 Sep 23 00:16 進捗どうですか.jpg
【追記】デフォルト挙動の変更理由
curl 7.74.0から挙動が変更された理由は脆弱性対応(CVE-2020-8284)でした。
要は悪意あるFTPサーバーを建てるとPASV
で返すIPアドレスとポート番号をユーザーが意図しない攻撃者のサイトに向けることが可能になるという事です。
このためデフォルトでは「PASV
で返すアドレスを信頼しない」ことになっています。
世の中にどの程度悪意あるFTPサーバーがあるのか疑問に思わなくもないですが、脆弱性の指摘自体はもっともだと思いますので古いcurlでも--ftp-skip-pasv-ip
オプションを積極的に使うと良いでしょう。
4. C# (FtpWebRequestクラス) で接続する
最後にプログラムから接続した場合を調べてみました。
今回は私が使い慣れてるC#を対象としています。
C#でFTP接続をする場合は太古の昔からあるFtpWebRequestクラスを使うのがメジャーかと思います。
このクラスは.NET Framework 2.0から存在し.NET Coreにも移植済み(.NET Standard 2.0以上)です。
このクラスで行われるFTP接続確立処理の実装はFtpControlStreamクラスにあります。
基本的な実装は昔から変わっていなかったのでGitHubのソースを見て挙動を調べてみます。
すると、
if (request.UsePassive)
{
string passiveCommand = (ServerAddress.AddressFamily == AddressFamily.InterNetwork || ServerAddress.IsIPv4MappedToIPv6) ? "PASV" : "EPSV";
commandList.Add(new PipelineEntry(FormatFtpCommand(passiveCommand, null), PipelineEntryFlags.CreateDataConnection));
}
の様に接続先がIPV4ならPASV
、それ以外(IPV6)ならEPSV
を使うことがわかりました。
そしてPASV
コマンドで返されるIPアドレスは常に無視されることも分かりました。
// Handle passive responses by parsing the port and later doing a Connect(...)
bool isPassive = false;
int port = -1;
if (entry.Command == "PASV\r\n" || entry.Command == "EPSV\r\n")
{
if (!response.PositiveCompletion)
{
_abortReason = SR.Format(SR.net_ftp_server_failed_passive, response.Status);
return PipelineInstruction.Abort;
}
if (entry.Command == "PASV\r\n")
{
port = GetPortV4(response.StatusDescription!);
}
else
{
port = GetPortV6(response.StatusDescription!);
}
isPassive = true;
}
if (isPassive)
{
if (port == -1)
{
NetEventSource.Fail(this, "'port' not set.");
}
try
{
_dataSocket = CreateFtpDataSocket((FtpWebRequest)_request!, Socket);
}
catch (ObjectDisposedException)
{
throw ExceptionHelper.RequestAbortedException;
}
IPEndPoint localEndPoint = new IPEndPoint(((IPEndPoint)Socket.LocalEndPoint!).Address, 0);
_dataSocket.Bind(localEndPoint);
_passiveEndPoint = new IPEndPoint(ServerAddress, port);
}
_passiveEndPoint = new IPEndPoint(ServerAddress, port);
とPASV
コマンドの結果はポート番号しか使われてない実装となっています。
サンプルプログラム
ここまでを踏まえて簡単なサンプルプログラムを使って挙動を確認してみました。
環境は.NET 5(.NET SDK 5.0.401)で以下の簡単なコンソールアプリケーションを書きました。
using System;
using System.IO;
using System.Net;
namespace ftpsample
{
class Program
{
static void Main(string[] args)
{
var serverAddress = "35.76.153.xx";
var user = "scott";
var password = "tiger";
// Create request
var request = (FtpWebRequest)FtpWebRequest.Create($"ftp://{serverAddress}/");
request.Credentials = new NetworkCredential(user, password);
request.KeepAlive = false;
request.UseBinary = true;
request.UsePassive = true; // Passiveモードを有効に
request.Method = WebRequestMethods.Ftp.ListDirectoryDetails; // LIST
Console.WriteLine($"Connecting to ftp://{user}:{password}@{serverAddress}/");
// Get response
using (var response = request.GetResponse())
using (var stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
{
var message = reader.ReadToEnd();
Console.WriteLine(message);
}
}
}
}
シンプルにLIST
コマンドを発行してその結果をコンソール表示するだけのものとなります。
実行結果は以下の通りサーバーに接続できています。
PS C:\ftpsample> dotnet run
Connecting to ftp://scott:tiger@35.76.153.xx/
-rw-r--r-- 1 scott scott 12 Sep 23 00:17 sample.txt
-rw-r--r-- 1 scott scott 37990 Sep 23 00:16 進捗どうですか.jpg
念のためWireSharkで通信を覗いてみてもPASV
コマンドで返されたIPが無視されていることがわかります。
5. 【追記】Python (ftplib) で接続する
Pythonは標準ライブラリftplibでFTPを扱えます。
ライブラリのソースを見てみるとC#の場合と同様にIPV4ならPASV
、それ以外ならEPSV
コマンドを使う様になっています。
def makepasv(self):
"""Internal: Does the PASV or EPSV handshake -> (address, port)"""
if self.af == socket.AF_INET:
untrusted_host, port = parse227(self.sendcmd('PASV'))
if self.trust_server_pasv_ipv4_address:
host = untrusted_host
else:
host = self.sock.getpeername()[0]
else:
host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())
return host, port
また、先述のCVE-2020-8284の影響を受けてPASV
コマンドで返されるIPアドレスを信頼しない更新が加えられています。
# Disables https://bugs.python.org/issue43285 security if set to True.
trust_server_pasv_ipv4_address = False
- 参考 : Issue 43285
終わりに
以上となります。
最初にWinSCPの挙動を知り他のクライアントではどうなのかと興味を持ち本記事を書くに至りました。
たった45種類を試しただけですが思っていた以上にクライアントの実装やデフォルトの挙動に違いがあり新しい知見を得ることができました。
2021年現在でもなんやかんやでFTPを扱うことはあるかと思います。
通信において変わった挙動や気になる挙動が見られた場合はサーバーだけでなくクライアントの実装も確認しておくと良いでしょう。