窓を開けっ放しにするな:RDP(TCP 3389)を巡る攻防と、境界防御のリアル
ネットワークエンジニアとして現場に立っていると、深夜2時にアラートが鳴り響き、ログを確認した瞬間に溜息をつくことがよくあります。「またか」と。攻撃者のボットネットが、世界中のIPアドレスに対してしつこく TCP 3389 を叩いているのです。
Remote Desktop Protocol(RDP)は非常に便利なツールですが、インターネットに対して無防備にポートを開放することは、泥棒に「どうぞ、ここから侵入してください」と玄関の鍵を開けておくのと同じです。今日は、このRDP接続を巡る通信の裏側と、私たちがファイアウォールでどのようにこの脅威を封じ込めるべきか、その「実戦的な処方箋」を書き残します。
—
1. RDPセッション確立の裏側:パケットの「握手」と「妥協」
まず、TCP 3389 で何が起きているのか。RDPの接続シーケンスは、標準的な TCP 3-way handshake から始まります。
1. SYN: クライアントが SYN パケットを送信。
2. SYN-ACK: サーバーが応答。
3. ACK: クライアントが接続を確立。
ここまではただのTCP接続ですが、RDPの場合はここから X.224 プロトコルによるネゴシエーションが始まります。攻撃者は、この TCP 接続が成立した直後の「脆弱な認証フェーズ」や、稀に存在するスタックのバグを狙ってきます。
ステートフルインスペクションファイアウォール(SPI)は、単に「3389が開いているか」を見るだけではありません。パケットのシーケンス番号、ACKの正当性、そして接続が確立された後の「状態(State)」を追跡し、異常なパケットフラグや、短時間での異常なコネクション試行回数を検知することで、攻撃の芽を摘み取ります。
—
2. 現場の防壁:ファイアウォールによる「異常接続遮断」の設計
Web APIを構築する際、私たちはAPI Gateway等でレートリミットをかけますが、RDPに対しても同じ考え方が必要です。しかし、RDPは HTTP ではないため、Nginx のようなL7レイヤーでの制御が難しい。そこで、私たちは境界ファイアウォールで「動的な遮断」を実装します。
iptablesによる「叩き落とし」の実装例
Linuxベースのファイアウォールやゲートウェイで、辞書攻撃を仕掛けてくるIPを自動的にBANする設定は、今や必須の防具です。
# 1. 3389ポートへの接続試行を監視し、短時間に複数回アクセスがあれば「攻撃」とみなす
# 60秒間に4回以上の接続試行があれば、そのIPをリストに追加する
iptables -A INPUT -p tcp --dport 3389 -m state --state NEW -m recent --set --name RDP_BLOCK
iptables -A INPUT -p tcp --dport 3389 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name RDP_BLOCK -j DROP
# 2. DROPしたことをログに記録(後で分析できるようにする)
iptables -A INPUT -p tcp --dport 3389 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 --name RDP_BLOCK -j LOG --log-prefix "RDP_ATTACK: "
この設定のポイントは、recent モジュールを使っている点です。これにより、単なるIPフィルタリングではなく、「挙動」に基づく動的な防御が可能になります。
—
3. なぜ「ポート開放」自体が設計の敗北なのか
現代のゼロトラストアーキテクチャにおいて、TCP 3389 をインターネットへ直接露出させることは、原則禁止とするのがベストプラクティスです。
もし開発や運用の都合でどうしても外部から接続する必要があるなら、以下の代替案を検討してください。これらは、パケットレベルの防御以上に強力な「論理的防御」です。
- VPN / SDP (Software Defined Perimeter): 認証済みのクライアントのみに内部ネットワークのトンネルを提供します。
- RD Gateway (Remote Desktop Gateway):
HTTPS (TCP 443)を介してRDPをカプセル化します。これにより、Webサーバーと同様のSSL/TLS証明書による保護と認証が可能になります。
Pythonによる「死活監視」の自動化(デバッグ用Tips)
運用中、ファイアウォールの設定が正しく効いているかを確認するために、簡単な疎通確認スクリプトを書いておくと便利です。
import socket
def check_rdp_port(target_ip):
# 3389ポートへの接続を試行する簡易チェッカー
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3) # 3秒以上応答がなければ遮断されている可能性
try:
result = sock.connect_ex((target_ip, 3389))
if result == 0:
print(f"[!] 警告: {target_ip}:3389 が開いています")
else:
print(f"[+] 正常: {target_ip}:3389 は遮断されています")
finally:
sock.close()
# 運用中のホストに対して実行
check_rdp_port("192.0.2.1")
—
最後の教訓:防御は「多層」で考える
RDPのポートを閉じることは、防御の第一歩に過ぎません。IPアドレス単位のブロックは、IPを偽装されたり、広大なボットネットから攻撃されたりすれば、いずれ限界が来ます。
1. ファイアウォールでの遮断: 通信の「入口」で弾く。
2. エンドポイントでの強化: NLA (Network Level Authentication) を強制し、認証なしの接続を拒否する。
3. モニタリング: Event Viewer や Syslog を収集し、異常なログイン試行をアラート化する。
ネットワークは生き物です。攻撃手法が進化するように、私たちの防御もまた、泥臭いトラブルシューティングと設定の積み重ねによって進化し続けなければなりません。今日実装したその一行のルールが、明日、会社を守る最後の砦になることを忘れないでください。
コメント