たれながし.info

とあるITエンジニアの備忘録

MySQL5.7でX Protocolを有効化する


はじめに

MySQLの「X Protocol」とは、MySQL 5.7.12 以降で実装された新しいクライアントプロトコルです。
ポート番号は33060/tcpを使用します。※設定で変更可

参考情報)
Understanding MySQL X (All Flavors)

MySQL8以降ではデフォルト有効、MySQL5.7ではデフォルト無効となっています。
MySQL5.7で有効にするにはX Pluginをインストールする必要があります。

この記事では、MySQL5.7でX Pluguinをインストールして、X Protocolを有効にする方法を記載します。

X Protocolの有効化

環境について

RHEL8.7にインストールした、MySQL5.7を使用しています。
環境の準備方法については、以下の記事で説明しています。

tarenagashi.hatenablog.jp

有効化の実施

X Protocolを有効化します。
手順はMySQL公式の手順を参照しました。

参考情報)
MySQL :: MySQL 5.7 Reference Manual :: 19.2 Setting Up MySQL as a Document Store

・MySQLに接続する
# mysql -u root -p

・x pluginのインストール
mysql> INSTALL PLUGIN mysqlx SONAME 'mysqlx.so';

・x pluginのインストール確認
mysql> show plugins;
+----------------------------+----------+--------------------+----------------------+---------+
| Name                       | Status   | Type               | Library              | License |
+----------------------------+----------+--------------------+----------------------+---------+
…(省略)…
| mysqlx                     | ACTIVE   | DAEMON             | mysqlx.so            | GPL     |
+----------------------------+----------+--------------------+----------------------+---------+

プラグインのインストールが終わると33060/tcpで待ち受けが開始されている

# ss -atnp | grep mysql
LISTEN 0      70                 *:33060               *:*    users:(("mysqld",pid=28081,fd=49))              
LISTEN 0      80                 *:3306                *:*    users:(("mysqld",pid=28081,fd=21))

動作確認

MySQL Shellを使って動作確認します。

参考情報)
MySQL :: MySQL Shell 8.0 :: 2.2 Linux への MySQL Shell のインストール
MySQL :: MySQL 8.0 リファレンスマニュアル :: 20.3.1 MySQL Shell

MySQL Shellをインストールする

# rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2023
# yum install mysql-shell

mysqlshコマンドでX Protocolに接続し、SQLを実行してみます。
SQL実行時は「\sql」でSQLモードに変更、終了は「\exit」
※不要な出力は記載を省略している

# mysqlsh root@localhost:33060

 MySQL  localhost:33060+ ssl  JS > \sql
 MySQL  localhost:33060+ ssl  SQL > select now();
+---------------------+
| now()               |
+---------------------+
| 2024-03-16 19:41:07 |
+---------------------+

 MySQL  localhost:33060+ ssl  SQL > \exit
Bye!

プロトコルに準拠していれば、MySQL Shell以外の他クライアントも使用可能と思われる。

RHEL8へのMySQL5.7インストール手順


はじめに

Red Hat Enterprise Linux 8(以下RHEL8)にMySQL5.7をインストールする手順について記載します。

RHEL8がリリースされた2019年5月時点で、MySQL8.xが存在し(8.0は2018年4月リリース)、MySQL5.7は既に古いバージョンであったためか?MySQL5.7のRHEL8向けリポジトリは存在しないようです。
ということで、RHEL8にMySQL5.7をRPMパッケージからインストールするには、RHEL7向けのリポジトリをRHEL8に追加しインストールする必要があるようです。

ググると実施手順を記載したブログなどの情報がヒットするが、手順通りにうまく行かない部分があったので、試行錯誤してうまくいった手順をメモします。

ちなみに、MySQL公式の手順が以下URLで公開されているが、序盤の「MySQL5.7リポジトリの追加用rpm」のURLにたどり着けません。それ以外は参考になります。

参考情報)
MySQL :: MySQL 5.7 Reference Manual :: 2.5.1 Installing MySQL on Linux Using the MySQL Yum Repository

最後に、MySQL5.7は2023年10月にサポート終了となっています。

RHEL8にMySQL5.7をインストール

RHEL8.7にMySQL5.7をインストールします。

OSのバージョン確認

OSはRHEL8.7を使用しています。

# cat /etc/redhat-release
Red Hat Enterprise Linux release 8.7 (Ootpa)

SELinuxは無効です。

# getenforce
Disabled

リポジトリの追加

# yum install https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm

# cat /etc/yum.repos.d/mysql-community.repo[mysql57-community]
name=MySQL 5.7 Community Server
baseurl=http://repo.mysql.com/yum/mysql-5.7-community/el/7/$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
…

GPG KEYのインポート

・GPG KEYのインポート
# rpm --import https://repo.mysql.com/RPM-GPG-KEY-mysql-2022

・インポートしたKEYの確認
# rpm -qi gpg-pubkey-3a79bd29-61b8bab7
Name        : gpg-pubkey
Version     : 3a79bd29
Release     : 61b8bab7
…
Summary     : gpg(MySQL Release Engineering <mysql-build@oss.oracle.com>)

MySQL5.7のインストール

・デフォルトのMySQLモジュールの無効化
# yum module disable mysql

・MySQL5.7のインストール
# yum install mysql-community-server

・インストール確認
# rpm -qa | grep mysql-community-server
mysql-community-server-5.7.44-1.el7.x86_64

MySQLの起動

# systemctl start mysqld
# systemctl enable mysqld

# ss -atnp | grep mysql
LISTEN 0      80                 *:3306                *:*    users:(("mysqld",pid=30322,fd=32))

rootパスワード変更

ログからデフォルトパスワードを確認し変更する

# cat /var/log/mysqld.log | grep root
2024-03-16T18:41:32.404003Z 1 [Note] A temporary password is generated for root@localhost: [デフォルトPW]

# mysql -u root -p

mysql> SET PASSWORD = '[新しいPW]';
Query OK, 0 rows affected (0.00 sec)

2024年のパッチチューズデー(Patch Tuesday)


目的

パッチチューズデーを求めるPythonのプログラムを以前作成しました。
もう12月なので、そのプログラムで来年2024年のパッチチューズデーを求めてみます。

tarenagashi.hatenablog.jp

パッチチューズデー

Pythonプログラム

import datetime, calendar, pytz

def get_day_of_nth_dow(year, month, nth=2, dow=1):

    first_dow, n = calendar.monthrange(year, month)
    day = 7 * (nth - 1) + (dow - first_dow) % 7 + 1
    
    return day

if __name__ == '__main__':

    year=2024 # 2024年

    print('-' * 49)
    title = '|' + ' ' * 4 + '太平洋時間(PST)' + ' ' * 4 + \
            '|' + ' ' * 5 + '日本時間(JST)' + ' ' * 5 + '|'     
    print(title)
    print('-' * 49)

    for month in range(1, 13):

        # 第2火曜日の日付の取得
        day = get_day_of_nth_dow(year, month)
        naive_pst = datetime.datetime(year, month, day, 10)

        # 太平洋標準時(PST)の計算
        pst = pytz.timezone('US/Pacific')
        aware_pst = pst.localize(naive_pst)

        # 日本標準時(JST)の計算
        jst = pytz.timezone('Asia/Tokyo')
        aware_jst = aware_pst.astimezone(jst)

        print(f'| {aware_pst.strftime("%Y/%m/%d(%a) %H:%M")} | {aware_jst.strftime("%Y/%m/%d(%a) %H:%M")} |')
 
    print('-' * 49)

実行結果

-------------------------------------------------
|    太平洋時間(PST)    |     日本時間(JST)     |
-------------------------------------------------
| 2024/01/09(Tue) 10:00 | 2024/01/10(Wed) 03:00 |
| 2024/02/13(Tue) 10:00 | 2024/02/14(Wed) 03:00 |
| 2024/03/12(Tue) 10:00 | 2024/03/13(Wed) 02:00 |
| 2024/04/09(Tue) 10:00 | 2024/04/10(Wed) 02:00 |
| 2024/05/14(Tue) 10:00 | 2024/05/15(Wed) 02:00 |
| 2024/06/11(Tue) 10:00 | 2024/06/12(Wed) 02:00 |
| 2024/07/09(Tue) 10:00 | 2024/07/10(Wed) 02:00 |
| 2024/08/13(Tue) 10:00 | 2024/08/14(Wed) 02:00 |
| 2024/09/10(Tue) 10:00 | 2024/09/11(Wed) 02:00 |
| 2024/10/08(Tue) 10:00 | 2024/10/09(Wed) 02:00 |
| 2024/11/12(Tue) 10:00 | 2024/11/13(Wed) 03:00 |
| 2024/12/10(Tue) 10:00 | 2024/12/11(Wed) 03:00 |
-------------------------------------------------

結論

ということで、初回は1/10(水)となりました。※日本時間

多くの会社は1/4(木)が年の始業だと思います。1/8(月)は成人の日で休みなので、1/10(水)は4営業日目です。
年始4営業日目にパッチリリースとか、人によってはツライですね。
とはいえ、今年2023年も1/11(水)(※5営業日目)だったのでそんなに変わりませんが…

あと、プログラムで求めるまでもなく、Microsoftがリリーススケジュールを公開してました。
プログラムで求めた結果と相違は無かったです。
msrc.microsoft.com

パッチチューズデーには余り関係ありませんが、2024年はうるう年で2月29日が存在します。
営業日が1日増えても月給制、年俸制の人は給料が変わりませんから、それもツライなとカレンダー見て思いました(私も月給制)。

responderでNTLMハッシュをキャプチャする


目的

responderというツールで、NTLMハッシュをキャプチャできると聞いたので実施してみました。クライアントがADドメインに所属している場合/していない場合の2パターンで実施します。

responderは、Kali Linuxにデフォルトで入っているので、それを使います。
www.kali.org

環境

  • Kali Linux:2022.3
    └ responder:3.1.3.0
  • ADドメコンWindows Server 2019
  • クライアント:Windows10 21H2 Enterprise

NTLMハッシュのキャプチャ

クライアントがADドメインに所属していない場合

クライアントがADドメインに所属していない場合です。


実施手順

Kali Linuxでresponderを起動 ※root権限で実行する必要あり

$ sudo responder -I eth0 -v

クライアントが所属するドメイン名を確認→ドメインには所属していない

> systeminfo | findstr "ドメイン"
ドメイン:               WORKGROUP

クライアントからresponderにファイル共有で接続する

> net use \\[Kali LinuxのIPアドレス]
結果

responderに表示された「NTLMv2-SSP Hash」の長い文字列がNTLMv2のハッシュ値らしい。


クライアントがADドメインに所属している場合

ADドメインに所属している端末はケルベロス認証を使うと聞くので、クライアントがADドメインに所属している場合に違いがあるのかも調べてみました。結果はクライアントがADドメインに所属していない場合と変わりませんでした。responderがADドメインに所属しているか、していないかでも結果は変わるかもしれません(今回はresponderはADドメインに所属していない)。


実施手順

Kali Linuxでresponderを起動 ※root権限で実行する必要あり

$ sudo responder -I eth0 -v

クライアントのADドメイン所属を確認

> whoami /UPN
user1@test.local

クライアントからresponderにファイル共有で接続する

> net use \\[Kali LinuxのIPアドレス]
結果

responderに表示された「NTLMv2-SSP Hash」の長い文字列がNTLMv2のハッシュ値らしい。

サーバーのSSL/TLS証明書のフィールドをPythonで出力する

Webサーバーなどに設定されているSSL/TLSサーバー証明書について、複数サーバーの証明書のフィールドをチェックする際にどうしたら良いか?と考えた時に、プログラム書いて調べたら良いかなと思ったので、実現方法を調べてみました。


例)www.google.comに設定されているサーバー証明書

結論としては、Pythonの標準モジュール「ssl」「socket」を使ってできました。

以下サイトを参考にしました。
www.askpython.com

実施方法

サーバーの「HostName」をキーにSSL/TLSサーバー証明書のフィールドを出力するサンプルです。

「CommonName」と「期限(開始と終了)」を出力します。
サブジェクト代替名(subjectAltName/DNSName)」なども出力可能ですが、証明書によっては大量に設定されている場合があるので出力部分はコメントアウトしています。

Pythonプログラム

import ssl
import socket
 
def verify_ssl_certificate(hostname):
    context = ssl.create_default_context() 
    with socket.create_connection((hostname, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            ssock.do_handshake()
            cert = ssock.getpeercert()
            CommonName = get_commonname(cert["subject"])
            NotBefore = cert["notBefore"]
            NotAfter = cert["notAfter"]
            # SubjectAltName = cert["subjectAltName"]
            print(f'"{hostname}","{CommonName}","{NotBefore}","{NotAfter}"')

def get_commonname(subjects):
    for subject in subjects:
        if subject[0][0] == "commonName":
            return subject[0][1]
    return ""

if __name__ == '__main__':
    print(f'"HostName","CommonName","NotBefore", "NotAfter"')
    for hostname in ["www.yahoo.co.jp", "www.google.com"]:
        verify_ssl_certificate(hostname)

実行結果

> python check_servercert.py
"HostName","CommonName","NotBefore", "NotAfter"
"www.yahoo.co.jp","edge01.yahoo.co.jp","Jul 28 02:16:59 2023 GMT","Aug 27 14:59:00 2024 GMT"
"www.google.com","www.google.com","Oct 16 08:10:46 2023 GMT","Jan  8 08:10:45 2024 GMT"

ランサムウェアグループの投稿を収集するツール「ransomwatch」を動かしたい

ここ数年ランサムウェアの被害が継続発生しています。収まる様子は見られません。
ランサムウェアの攻撃グループは、攻撃の成功や身代金の要求を彼ら自身が運営するWebサイトに投稿することが多いです。ほとんどのサイトはTor経由でないとアクセスできない.onionドメインのサイトです。

そういったランサムウェアグループの投稿を自動収集するための「ransomwatch」というツールがあります。ソースコードGitHubで公開されています。

github.com

「ransomwatch」は、作者の人がツールを定期稼働させて、結果を「https://ransomwatch.telemetry.ltd」で公開しています。なので、本来は自前の環境で動かす必要はありませんが、動作の仕組みが知りたかったので、自前の環境で動かしてみました。
結論としては、うまく動かない部分があって途中で諦めました。しかし、ここに奮闘の結果をメモしておきます。

また「ransomwatch」からフォークした「ransomware.live」というツールもあります。
こちらもgithubでソースコードが公開されていて、実行結果が公開されているサイトがあります。
こちらの方が頻繁に更新され高機能なので、「ransomware.live」を使った方が良かったのかもしれません。

動作環境について

「Torプロキシ」と「ransomwatch」を動かす必要があります。
今回は、Windows上でDockerを稼働させ、Docker上で「Torプロキシ用のコンテナ」と「ransomwatchのコンテナ」を稼働させます。必ずしもDockerを使用する必要はないですが、Dockerを使うと楽です。


  • ホストOS:Windows11 22H2 Enterprise x64
  • Docker Desktop:4.25.0
  • Ubuntu on WSL2:22.04.2 LTS

環境構築

WSL2とDockerをインストールした後、コンテナを起動します。

WSL2のインストール

WSL2はDockerを動作させるために使用します。
MS公式の手順を元にインストールしました。
WSL のインストール | Microsoft Learn

PowerShellを管理者で起動し、wslをインストールする。

> wsl --install
…
要求された操作は正常に終了しました。変更を有効にするには、システムを再起動する必要があります。

Windowsを再起動する。WSL2上でUbuntuが起動するが使いません。

> wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Running         2

Docker Desktopのインストール

Docker DesktopをDLしてインストールする

> Start-BitsTransfer -Source "https://desktop.docker.com/win/main/amd64/Docker Desktop Installer.exe" -Destination "Docker 20Desktop 20Installer.exe"

「Use WSL 2 instead of Hyper-V」にチェックを入れてインストールする

インストールが完了したらWindowsを再起動

WSL2の状態を見ると、dockerが動作している

> wsl -l -v
  NAME                   STATE           VERSION
* Ubuntu                 Running         2
  docker-desktop         Running         2
  docker-desktop-data    Running         2

Torプロキシ用のコンテナ起動

Torプロキシ用のコンテナを起動する

> docker run -p9050:9050 ghcr.io/joshhighet/torsocc:latest
Nov 03 15:17:14.214 [notice] Tor 0.4.7.13 running on Linux with Libevent 2.1.12-stable, OpenSSL 3.0.8, Zlib 1.2.13, Liblzma 5.2.9, Libzstd 1.5.2 and Unknown N/A as libc.
…
Nov 03 15:17:54.000 [notice] Bootstrapped 100% (done): Done

9050/TCPでTorプロキシが起動する

> docker container ls
CONTAINER ID   IMAGE                               COMMAND                   CREATED          STATUS          PORTS                    NAMES
364f5a623eb8   ghcr.io/joshhighet/torsocc:latest   "tor -f /etc/tor/tor…"   27 seconds ago   Up 26 seconds   0.0.0.0:9050->9050/tcp   kind_bohr

ransomwatchのコンテナイメージ作成と起動

ransomwatchをDLし解凍する

> Start-BitsTransfer -Source https://codeload.github.com/joshhighet/ransomwatch/zip/refs/heads/main -Destination ransomwatch.zip
> Expand-Archive .\ransomwatch.zip

dockerfileからイメージを作成する

> cd ransomwatch\ransomwatch-main
> docker build -t ransomwatch:1.0 .
[+] Building 357.5s (17/17) FINISHED

>docker image ls
REPOSITORY                   TAG       IMAGE ID       CREATED          SIZE
ransomwatch                  1.0       c818deea9ad0   14 seconds ago   2GB

コンテナを起動しbashで接続する

> docker run -it --name ransomwatch --entrypoint "/bin/bash" ransomwatch:1.0

vimのインストール

# apt-get install vim

ransomwatchが参照するプロキシのIPアドレスを修正する
127.0.0.1だとコンテナ内を参照してしまうので、ホストのIPアドレスを参照するように変更

# vim sharedutils.py21行目を書き換えます。
修正前)sockshost = '127.0.0.1'
修正後)sockshost = 'host.docker.internal'

「/source」ディレクトリを作成する

# mkdir /source

ransomwatchの利用

ransomwatchを使っていきます。

スクレイピングの実施(scrape)

「scrape」でランサムウェア攻撃グループのWebサイトをスクレイピングする。
スクレイピング結果はコンテナ内の「/source」にhtmlファイルとして保存される。

スクレイピングの実施

# python3 ransomwatch.py scrape

2023-11-03:17:59:44,273 INFO     sharedutils: attempting socket connection
2023-11-03:17:59:44,291 INFO     sharedutils: socket - successful connection
…
2023-11-03:18:35:00,161 INFO     ransomwatch: scraping http://threeamkelxicjsaf2czjyz2lc4q3ngqkxhhlexyfcp2o6raw4rphyad.onion successful
2023-11-03:18:35:00,162 INFO     ransomwatch: saving /source/threeam-threeamkelxicjsaf2czjyz2lc4q3ngqkxhhlexyfcp2o6raw4rphyad.html
…
2023-11-03:18:35:29,480 INFO     ransomwatch: scrape run complete

完了すると「/source」にhtmlファイルが作成される

# ls -l /source

-rw-r--r-- 1 root root    2305 Nov  5 01:41 0mega-omegalock5zxwbhswbisc42o2q2i54vdulyvtqqbudqousisjgc7j7yd.html
-rw-r--r-- 1 root root    1205 Nov  5 01:47 0xFFF-contiuevxdgdhn3zl2kubpajtfgqq4ssj2ipv6ujw7fwhggev3rk6hqd.html
-rw-r--r-- 1 root root   34094 Nov  5 01:28 RAMP-rampjcdlqvgkoz5oywutpo6ggl7g6tvddysustfl6qzhr5osr24xxqqd.html
-rw-r--r-- 1 root root   14373 Nov  5 01:47 abyss-3ev4metjirohtdpshsqlkrqcmxq6zu3d7obrdhglpy5jpbr7whmlfgqd.html
-rw-r--r-- 1 root root   49720 Nov  5 01:48 akira-akiral2iz6a7qgd3ayp3l6yub7xx2uep76idk3u2kollpj5z3z636bad.html
…

パースの実施(parse)

「parse」で取得したhtmlから投稿を抜き出す
パーサーがおかしいのか異常終了したため、ここで諦めました

# python3 ransomwatch.py parse2023-11-04:02:13:45,99 INFO     sharedutils: running shell command -
    grep --no-filename '<a href="/company/' source/cuba-*.html | cut -d '/' -f 3 | cut -d '"' -f 1 | sort --uniq | grep -v company

grep: source/cuba-*.html: No such file or directory
Traceback (most recent call last):
  File "//ransomwatch.py", line 242, in <module>
    parsers.cuba()
  File "/parsers.py", line 298, in cuba
    posts = runshellcmd(parser)
            ^^^^^^^^^^^^^^^^^^^
  File "/sharedutils.py", line 176, in runshellcmd
    cmdout = subprocess.run(
             ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/subprocess.py", line 571, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '
    grep --no-filename '<a href="/company/' source/cuba-*.html | cut -d '/' -f 3 | cut -d '"' -f 1 | sort --uniq | grep -v company
    ' returned non-zero exit status 1.

Webページの生成(markdown

markdown」でパースした結果から、markdown形式のWebサイトを生成できるらしいです。
今回はパースができなかったので、試せていません。

# python3 ransomwatch.py markdown

結論

ということで、スクレイピングはできましたが、その後の処理が実施できませんでした。
「ransomwatch」を稼働させるだけでも、修正が必要な部分がいくつかあったので、大変でした。とはいえ、大体の仕組みは分かったので個人的には満足です。

自前でランサムウェアグループの投稿を収集したい人は、「ransomwatch」より「ransomware.live」を使った方がいいのかもしれません。もしくは自作のツールを作るとか…。
こんな記事もあります。

xtech.nikkei.com

個人的なメモ:コンテナの操作

私はDockerをほとんど触ったことがなかったので、今回知ったDocker操作コマンドのメモです。

・稼働中のコンテナ一覧の確認
> docker container ls

・停止中含めたコンテナ一覧の確認
> docker container ps -a

・ローカルのファイルをコンテナにコピーする
> docker cp [ローカルのファイルパス] [コンテナID]:[コンテナのファイルパス]

CentOSでFTPサーバーを建てた

急遽FTPサーバーを建てる必要があったので、CentOS7&vsftpdで建てた時のメモです。
セキュリティを考慮せず建てたので、あまり参考にはしないでください。

本来なら下記を考慮した方が良いと思います。

  • 最新のOSを使う(CentOSの開発は終了している)
  • FTPではなくFTPSやSFTPを使う
  • FTPを許可するユーザーを明示的に指定する
  • アクセスできるディレクトリを制限する

FTPサーバーの環境

構築環境

構築した環境です。

  • CentOS:7.6
  • vsftpd:3.0.2-29.el7_9

要件

FTPサーバーの要件です。

  • Anonymousの接続は許可しない
  • FTPサーバーにはPassiveで接続する(データのコネクションもクライアントから接続)

FTPサーバーの構築

事前準備

SELinuxは無効にする

# getenforce
Disabled

vsftpdのインストール

# yum -y install vsftpd

vsftpdの設定

# cd /etc/vsftpd
# cp -a vsftpd.conf{,.org}

# vi vsftpd.conf
※以下変更点だけ書きます。

# Anonymousの接続は許可しないに変更
anonymous_enable=YES
→anonymous_enable=NO

# パッシブモード有効化&ポート範囲を追記
# pasv_enableはDefaultっでYESなので書かなくても良いらしい
pasv_enable=YES
pasv_min_port=60100
pasv_max_port=60200

サービス起動

# systemctl start vsftpd
# systemctl status vsftpd
● vsftpd.service - Vsftpd ftp daemon
   Loaded: loaded (/usr/lib/systemd/system/vsftpd.service; disabled; vendor preset: disabled)
   Active: active (running) since 月 2023-10-30 20:10:05 JST; 7s ago
  Process: 6711 ExecStart=/usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf (code=exited, status=0/SUCCESS)
 Main PID: 6712 (vsftpd)
   CGroup: /system.slice/vsftpd.service
           mq6712 /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf

Firewalldの設定

21,60100-60200/tcpを許可する

# firewall-cmd --add-service=ftp --permanent
# firewall-cmd --add-port=60100-60200/tcp --permanent
# firewall-cmd --reload

# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens192
  sources:
  services: ssh dhcpv6-client
  ports: 993/tcp 143/tcp 21/tcp 60100-60200/tcp
  ...

動作確認

無事接続できました。

注意点:Windowsの標準FTPクライアントはPassive接続に対応していない

Windowsに標準インストールされているFTPクライアントはPassive接続に対応していないそうです。
FTPクライアントのオプションやコマンド一覧を確認しても、Passiveで接続するようなものが存在しません。

>where ftp.exe
C:\Windows\System32\ftp.exe
>ftp -h

FTP サーバー サービス (デーモンとも呼ぶ) を実行するコンピューターとの間で、
ファイルの送受信を行います。FTP は対話的に使用できます。

FTP [-v] [-d] [-i] [-n] [-g] [-s:filename] [-a] [-A] [-x:sendbuffer]
    [-r:recvbuffer] [-b:asyncbuffers] [-w:windowsize] [host]

  -v              リモート サーバーの応答を表示しません。
  -n              最初の接続時に自動ログインを行いません。
  -i              複数ファイルの転送中に、対話的なメッセージ表示を
                  無効にします。
  -d              デバッグを有効にします。
  -g              ファイル名のグロビングを無効にします (GLOB コマンドを参照)。
  -s:filename     FTP コマンドを記述したテキスト ファイルを指定します。
                  これらのコマンドは、FTP の開始後に自動実行されます。
  -a              データ接続のバインド時に、いずれかのローカル インターフェイ
                  スを使用します。
  -A              匿名でログインします。
  -x:send sockbuf SO_SNDBUF の既定のサイズである 8192 を上書きします。
  -r:recv sockbuf SO_RCVBUF の既定のサイズである 8192 を上書きします。
  -b:async count  既定の非同期数である 3 を上書きします。
  -w:windowsize   既定の転送バッファー サイズである 65535 を上書きします。
  host            接続先のリモート ホストのホスト名または IP アドレス
                  を指定します。

メモ:
  - mget および mput コマンドには、それぞれ yes/no/quit の意味を表す y/n/q を
    指定します。
  - コマンドを中止するには Ctrl+C キーを押します。
>ftp
ftp> ?
コマンドは省略することができます。コマンド:

!               delete          literal         prompt          send
?               debug           ls              put             status
append          dir             mdelete         pwd             trace
ascii           disconnect      mdir            quit            type
bell            get             mget            quote           user
binary          glob            mkdir           recv            verbose
bye             hash            mls             remotehelp
cd              help            mput            rename
close           lcd             open            rmdir