Engineering Note

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

2.5 Netcatの置き換え (サイバーセキュリティプログラミング Pythonで学ぶハッカーの思考)

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

今回は、前回までで学んだPythonのsocketを使ったTCP通信の実装をもとに、Unix系のOSでお馴染みのNetcat(nsコマンド)に代わるツールを作成します。

 

 

Netcatとは

Netcatは、TCPおよびUDPを使ったコマンドラインツールです。

その歴史は古く、1995年にバージョン1.00がリリースされて以降、ネットワークの簡易テストからバックドアの作成まで、その豊富なオプションやシェルと組み合わせて使ったりと、様々な用途に用いられています。

なお、現行のLinuxディストリビューションにおいては、ncコマンドとしてデフォルトでインストールされていますが、任意のプログラム('/bin/sh'など)を実行できる"-e"のオプションは、バックドアの作成などに悪用されるため無効となっています。

 

ライブラリのインポート

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

 

# bhnet.py
import sys
import socket
import argparse
from threading import Thread
import subprocess

 

argparseを使ったコマンドラインオプションのチェック

書籍ではgetoptを使用して引数のチェックを行っていますが、ここではargpaseを使って書き直してみました。

argparseの基本的な使い方についてはこちらをご覧ください。

 

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
                description='BHP Net Tool',
                epilog='''\
Examples:
    bhnet.py -t 192.168.0.1 -p 5555 -l -c
    bhnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe
    bhnet.py -t 192.168.0.1 -p 5555 -l -e 'cat /etc/passwd'
    echo 'ABCDEFGHI' | ./bhnet.py -t 192.168.11.12 -p 135''')

parser.add_argument('-l', '--listen', help='listen on [host]:[port] for incoming connections', action='store_true')
parser.add_argument('-e', '--execute', default=None, help='execute the given file upon receiving a connection')
parser.add_argument('-c', '--command', help='initialize a command shell', action='store_true')
parser.add_argument('-u', '--upload', help='upon receiving connection upload a file and write to [destination]')
parser.add_argument('-t', '--target', default=None)
parser.add_argument('-p', '--port', default=None, type=int)
args = parser.parse_args()

 

getoptを使うよりも見やすく、コードもすっきりしたと思います。

実際にヘルプを表示すると以下のようになります。

 

 > python bhnet.py -h
 usage: bhnet.py [-h] [-l] [-e EXECUTE] [-c] [-u UPLOAD] [-t TARGET] [-p PORT]
 
 BHP Net Tool
 
 optional arguments:
   -h, --help            show this help message and exit
   -l, --listen          listen on [host]:[port] for incoming connections
   -e EXECUTE, --execute EXECUTE
                         execute the given file upon receiving a connection
   -c, --command         initialize a command shell
   -u UPLOAD, --upload UPLOAD
                         upon receiving connection upload a file and write to
                         [destination]
   -t TARGET, --target TARGET
   -p PORT, --port PORT
 
 Examples:
     bhnet.py -t 192.168.0.1 -p 5555 -l -c
     bhnet.py -t 192.168.0.1 -p 5555 -l -u c:\target.exe
     bhnet.py -t 192.168.0.1 -p 5555 -l -e 'cat /etc/passwd'
     echo 'ABCDEFGHI' | ./bhnet.py -t 192.168.11.12 -p 135

 

クライアントモードの実装

クライアントとして、リモートサーバに接続した場合のデータ送受信を行う部分になります。

 

def client_sender(buffer):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        client.connect((args.target, args.port))
        if len(buffer):
            client.send(buffer)

        while True:
            recv_len = 1
            response = ''
            while recv_len:
                data = client.recv(4096)
                recv_len = len(data)
                response += data.decode('utf-8')
                if recv_len < 4096:
                    break
            print(response.rstrip(), end='')
            buffer = input()
            if buffer == '':
                continue
            if buffer == 'exit':
                client.send(b'exit')
                break
            client.send(buffer.encode('utf-8'))
        client.close()
    except:
        print('[*] Exception! Exiting.')
        client.close()

 

クライアントから"exit"の入力を受けたら接続を終了するようにしています。

 

サーバモードの実装

サーバとして、クライアントからの接続を処理する部分になります。

 

def server_loop():
    if not args.target:
        args.target = '0.0.0.0'
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((args.target, args.port))

    server.listen(5)
    while True:
        client_socket, addr = server.accept()
        client_thread = Thread(target=client_handler, args=[client_socket,])
        client_thread.start()

 

3行目では、引数に使用するインターフェースのIPアドレスが指定されなかった場合は"0.0.0.0"として、アクティブな全てのインターフェースを使用するようにします。

 

コマンドの実行

subprocessを使ってクライアントから送られてきたコマンドを実行します。

 

def run_command(command):
    command = command.rstrip()

    try:
        output = subprocess.check_output(
                    command,stderr=subprocess.STDOUT, shell=True)
    except:
        output = b'Failed to execute command.'

    return output

 

2行目はクライアントから送られてきたデータの改行文字を取り除きます。

なお、実行したコマンドの結果を返り値とします。

 

オプション処理の実装

サーバモードで起動した際に付与したオプション(コマンド実行、コマンドシェルの実行、ファイルアップロード)を読み取り、それに応じた処理を実装します。

 

def client_handler(client_socket):
    if args.upload:
        file_buffer = b''

        while True:
            data = client_socket.recv(1024)
            file_buffer += data
            if len(data) < 1024:
                break

        try:
            file_descriptor = open(args.upload, 'wb')
            file_descriptor.write(file_buffer)
            file_descriptor.close()

            client_socket.send('Successfully saved file to {}'.format(args.upload).encode('utf-8'))
        except:
            client_socket.send('Failed to save file to {}'.format(args.upload).encode('utf-8'))

    if args.execute:
        output = run_command(args.execute)
        client_socket.send(output)

    if args.command:
        prompt = b'<BH:#> '
        client_socket.send(prompt)

        while True:
            recv_len = 1
            cmd_buffer = ''
            while recv_len:
                buffer = client_socket.recv(1024)
                recv_len = len(buffer)
                cmd_buffer += buffer.decode('utf-8')
                if recv_len < 1024:
                    break
            if cmd_buffer == 'exit':
                client_socket.close()
                break
            response = run_command(cmd_buffer)

            client_socket.send(response + prompt)

 

メイン関数の実装

最後にメイン関数を実装していきます。

クライアントモードおよびサーバモードで起動するのに必要な引数(オプション)がなかった場合は、ヘルプを表示してそのまま終了します。

 

def main():
    if not args.listen and args.target and args.port:
        buffer = sys.stdin.read()
        client_sender(buffer.encode('utf-8'))
    elif args.listen:
        server_loop()
    else:
        parser.print_help()
        sys.exit(1)

if __name__ == '__main__':
    main()

 

ファイルをアップロードしてみる

まずスクリプトをWSL Ubuntu上でスクリプトをサーバモードで起動します。

 

 > python3 bhnet.py -lp 8000 -u hoge

 

"-l"でサーバモードで起動し、"-p"で指定したポートで接続を待ちます。

"-u"で指定したファイル名(ここでは"hoge")で保存します。

 

コマンドプロンプトを起動し、クライアントモードでファイルをアップロードします。

 

 > type test.txt | python .\bhnet.py -t 127.0.0.1 -p 8000
 Successfully saved file to hoge[*] Exception! Exiting.

 

コマンドシェルを起動してみる

次はWSLのUbuntuからコマンドシェルオプションを付けて、サーバを起動します。

 

 > python3 bhnet.py -lp 8000 -c

 

コマンドプロンプトからクライアントモードで起動し、サーバにアクセスします。

なお、接続時はEOFに達するまで標準入力から読み込む仕様になっているので、"Ctrl+Z"を入力して、サーバからプロンプトを受信します。

 

 > python .\bhnet.py -t 127.0.0.1 -p 8000
 ^Z
 <BH:#>ls
 bhnet.py
 hoge
 <BH:#>cat hoge
 test file uploading.
 <BH:#>exit

 

最初にlsコマンドを実行し、その後に前述でアップロードした"hoge"ファイルをcatコマンドで表示しています。

 

最後に

以上がPythonで実装したNetcatライクなコマンドラインツールになります。

簡単なバックドアとして機能しますが、暗号化していないのとポートを開放しなければならないため、ファイアウォールやセキュリティソフト等にすぐ検知されてしまいます。

そのため、実際には「2.7 Paramikoを用いたSSH通信プログラムの作成」の項で説明されている通信の暗号化およびリバースシェルのような機能を実装したほうが重宝されると思います。

 

参考書籍

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