├── README.md ├── hashms.conf ├── hashms.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # hashms 2 | 3 | hashms runs during a hashcat cracking session and checks the given outfile (-o/--outfile parameter in hashcat) at specified intervals. If the outfile has additional lines (i.e. additional hashes have been cracked) hashms sends a notification via SMS and/or Slack. The intent is to reduce the delay between cracking a hash and follow-on operations, as well as the manual effort involved in checking and re-checking ongoing cracking sessions. 4 | 5 | hashms uses [Textbelt](https://textbelt.com/) for SMS. An API key is required for SMS, and a [Slack webhook URL](https://api.slack.com/incoming-webhooks) is required for Slack messages. 6 | 7 | ## Installation 8 | 9 | Clone the repositoriy and install the requirements: 10 | 11 | git clone https://github.com/WJDigby/hashms.git 12 | pip3 install -r requirements.txt 13 | 14 | The only non-standard library required is [requests](http://docs.python-requests.org/en/master/). The repository includes an example configuration file. 15 | 16 | ## Use 17 | 18 | Run hashms in a screen, tmux, or other terminal session while hashcat is running and provide it the name of your hashcat outfile to monitor. If you are running on a shared machine, or the user running hashms is different than the user running hashcat, make sure hashms has permissions to read the outfile. The hashcat64 process name is hardcoded in hashms.py. 19 | 20 | The Textbelt API and SLack URL values can be set with environmental variables or with a configuration file. Running hashms with command-line parameters -p / --phone-number and/or -s / --slack will look for environmental variables. 21 | 22 | python3 hashms.py -o hashes.outfile -p 5551234567 23 | 24 | Running hashms.py with the configuration file option (-c / --config) will cause it to look for the Textbelt API key, phone number, Slack URL, and Slack username in the configuration file. Configuration files may be useful in situations where a team shares a cracking rig. 25 | 26 | Arguments include: 27 | * -o / --outfile - The location of the hashcat outfile to monitor (mandatory) 28 | * -i / --interval - Interval in minutes between checks. Default is 15. This is a float value in case the operator wants to check more often than once a minute. 29 | * -n / --notification-count - How many notifications to send overall. Default is 5. This is useful in case the operator is running a large cracking job and does not want to eat through all of his or her Textbelt quota. 30 | * -t / --test - Send a test message to the configured options. Does not count against the notification count, but an SMS will of course count against the Textbelt quota. 31 | * -c / --config - Use a configuration file. A sample configuration file is included in the repo. If used, hashms expects all notification values to come from the configuration file. This option is mutually exclusive with -p and -s. 32 | * -p / --phone-number - Phone number to send SMS to. Textbelt API will come from environmental variable. 33 | * -s / --slack - If enabled, send a slack message. Slack URL will come from environmental variable. 34 | * -u / --user - Slack user to send the message to (e.g. @user). Useful to get targeted slack notifications. 35 | 36 | ## Examples 37 | 38 | Check the file hashes.out hourly. If additional hashes have been cracked, send a text to (555) 123-4567 and a slack message to the user wjdigby. Send a maximum of 10 notifications: 39 | 40 | python3 hashms.py -o hashes.out -i 60 -n 10 -p 5551234567 -s -u wjdigby 41 | 42 | Check the file hashes.out every 10 minutes. If additional hashes have been cracked, send a Slack message based on the contents of a configuration file (to send only Slack messages when using a configuration file, only fill out the Slack options. Likewise for SMS). 43 | 44 | python3 hashms.py -o hashes.out -i 10 -c hashms.conf 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /hashms.conf: -------------------------------------------------------------------------------- 1 | [Textbelt] 2 | # This is an example and is NOT a valid API key. 3 | TextbeltAPI = ddYPuc5sKyfbw7kGGD86eZu2ps336dJ33oPjbzEnjfFUHVR000CKJEI0XmpHGN22fg 4 | PhoneNumber = 5551234567 5 | 6 | [Slack] 7 | # This is an example and is NOT a valid Slack webhook. 8 | SlackURL = https://hooks.slack.com/services/ABCDEFGHI/JKLMNOPQR/STUVWXYZ1234567890ABCDEF 9 | SlackUser = wjdigby 10 | -------------------------------------------------------------------------------- /hashms.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import configparser 3 | import time 4 | from os import path, environ 5 | from subprocess import Popen, PIPE 6 | import requests 7 | 8 | PROCESS_NAME = 'hashcat64.bin' 9 | TEXTBELT_API_KEY = environ.get('TEXTBELT_API_KEY') 10 | SLACK_URL = environ.get('SLACK_URL') 11 | 12 | 13 | def check_pid(process_name): 14 | """Return pid of hashcat process.""" 15 | stdout = Popen('pidof ' + process_name, shell=True, stdout=PIPE).stdout 16 | output = stdout.read().rstrip() 17 | output = output.decode('utf-8') 18 | if output: 19 | return output 20 | return False 21 | 22 | 23 | def check_file(hashcat_outfile): 24 | """Check number of lines in designated outfile.""" 25 | if not path.isfile(hashcat_outfile): 26 | return False 27 | with open(hashcat_outfile) as file: 28 | i = 0 29 | for i, lines in enumerate(file): 30 | pass 31 | return i + 1 32 | 33 | 34 | def send_text(textbelt_api_key, phone_number, message): 35 | """Send an SMS using the Textbelt API.""" 36 | response = requests.post('https://textbelt.com/text', 37 | {'phone': str(phone_number), 38 | 'message': message, 39 | 'key': textbelt_api_key}) 40 | response_json = response.json() 41 | if 'textId' in response_json: 42 | output = '[*] SMS sent with ID {}\n[*] Quota remaining: {}'.format(response_json['textId'], 43 | str(response_json['quotaRemaining'])) 44 | elif 'error' in response_json: 45 | output = '[-] Received erorr message from textbelt: {}'.format(response_json['error']) 46 | return output 47 | 48 | 49 | def send_slack(slack_url, message, user=False): 50 | """Send a notification to Slack.""" 51 | if user: 52 | message = '<@{}> : {}'.format(user, message) 53 | response = requests.post(slack_url, json={"text": message}) 54 | status_code, content = response.status_code, response.content.decode('utf-8') 55 | output = '[*] Posted message "{}" to Slack.'.format(message) 56 | else: 57 | response = requests.post(slack_url, json={"text": message}) 58 | status_code, content = response.status_code, response.content.decode('utf-8') 59 | output = ('[*] Sent message "{}" to Slack.\n[*] Received status code "{}"" and' 60 | 'response "{}"'.format(message, status_code, content)) 61 | return output 62 | 63 | 64 | def parse_config(config): 65 | """Return pid of hashcat process.""" 66 | configuration = configparser.ConfigParser() 67 | configuration.read(config) 68 | textbelt_api_key = configuration['Textbelt']['TextbeltAPI'] 69 | phone_number = configuration['Textbelt']['PhoneNumber'] 70 | slack_url = configuration['Slack']['SlackURL'] 71 | user = configuration['Slack']['SlackUser'] 72 | return textbelt_api_key, phone_number, slack_url, user 73 | 74 | 75 | def main(): 76 | """Take user input to setup notifications. Print status updates to terminal.""" 77 | parser = argparse.ArgumentParser(description='Periodically check hashcat cracking progress and notify of success.') 78 | parser.add_argument('-o', '--outfile', dest='hashcat_outfile', required=True, 79 | help='hashcat outfile to monitor.') 80 | parser.add_argument('-i', '--interval', dest='check_interval', required=False, type=float, 81 | default=15, help='Interval in minutes between checks. Default 15.') 82 | parser.add_argument('-n', '--notification-count', dest='notification_count', required=False, 83 | type=int, default=5, help='Cease operation after N notifications. Default 5.') 84 | parser.add_argument('-t', '--test', dest='test', required=False, action='store_true', 85 | help='Send test message via SMS and/or Slack.' 86 | 'Does not count against notifications.') 87 | parser.add_argument('-c', '--config', dest='config', required=False, 88 | help='Use a configuration file instead of command-line arguments.') 89 | parser.add_argument('-p', '--phone', dest='phone_number', required=False, 90 | help='Phone numer to send SMS to. Format 5551234567.') 91 | parser.add_argument('-s', '--slack', dest='slack', required=False, default=False, 92 | action='store_true', help='Send notification to slack channel.') 93 | parser.add_argument('-u', '--user', dest='user', required=False, help='Slack user to notify.') 94 | args = parser.parse_args() 95 | 96 | hashcat_outfile = args.hashcat_outfile 97 | check_interval = args.check_interval 98 | notification_count = args.notification_count 99 | test = args.test 100 | phone_number = args.phone_number 101 | slack = args.slack 102 | user = args.user 103 | config = args.config 104 | 105 | parameters = (phone_number, slack, user) 106 | if config and any(parameters): 107 | print('[-] Configuration file (-c/--config) and command-line parameters' 108 | '(-p/--phone, -s/--slack, -u/--user) are mutually exclusive.') 109 | exit() 110 | 111 | if config: 112 | slack = False 113 | textbelt_api_key, phone_number, slack_url, user = parse_config(config) 114 | if slack_url: 115 | slack = True 116 | print('[*] Parsing configuration file {}'.format(config.rstrip())) 117 | else: 118 | textbelt_api_key = TEXTBELT_API_KEY 119 | slack_url = SLACK_URL 120 | 121 | if not phone_number and not slack: 122 | print('[-] You have not entered a phone number (-p/--phone-number or [PhoneNumber]) or' 123 | 'selected slack (-s/--slack or [SlackURL])\n[-] No notifications will be sent!') 124 | 125 | if user and not slack: 126 | print('[-] User parameter (-u/--user or [SlackUser]) is only used in conjunction with slack' 127 | '(-s/--slack or [SlackURL]).') 128 | 129 | if test: 130 | print('[*] Conducting test. This does not count against notifications.') 131 | message = 'hashms test message.' 132 | if phone_number: 133 | output = send_text(textbelt_api_key, phone_number, message) 134 | print(output) 135 | if slack and not user: 136 | output = send_slack(slack_url, message) 137 | print(output) 138 | if slack and user: 139 | output = send_slack(slack_url, message, user) 140 | print(output) 141 | 142 | if phone_number and not textbelt_api_key: 143 | print('[-] No textbelt API key - check environmental variable or configuration file.') 144 | exit() 145 | if slack and not slack_url: 146 | print('[-] No slack URL - check environmental variable or configuration file.') 147 | exit() 148 | 149 | starting_pid = check_pid(PROCESS_NAME) 150 | if not starting_pid: 151 | print('[-] hashcat is not running. Exiting.') 152 | exit() 153 | print('[*] hashcat PID: {}'.format(starting_pid)) 154 | 155 | starting_outfile = check_file(hashcat_outfile) 156 | if starting_outfile: 157 | print('[*] Outfile exists and is {} lines long.'.format(starting_outfile)) 158 | 159 | 160 | i = 1 161 | try: 162 | while i < notification_count + 1: 163 | current_pid = check_pid(PROCESS_NAME) 164 | current_outfile = check_file(hashcat_outfile) 165 | current_time = time.strftime('%A %d %B %Y at %H:%M') 166 | if starting_pid != current_pid: 167 | print('[-] Original hashcat process stopped. Exiting.') 168 | exit() 169 | elif not current_outfile: 170 | print('[-] File does not exist. Monitoring for file creation.' 171 | 'Checked on {}'.format(current_time)) 172 | elif starting_outfile == current_outfile: 173 | print('[-] No more hashes cracked yet. Checked on {}'.format(current_time)) 174 | elif starting_outfile != current_outfile: 175 | print('[+] Additional hashes cracked! Checked on {}'.format(current_time)) 176 | message = ('{} hashes have been cracked.' 177 | 'Notification {} of {}.'.format(current_outfile, i, notification_count)) 178 | if phone_number: 179 | output = send_text(textbelt_api_key, phone_number, message) 180 | print(output) 181 | if slack: 182 | output = send_slack(slack_url, message, user) 183 | print(output) 184 | i += 1 185 | if i == notification_count + 1: 186 | print('[*] Notification limit reached. Happy hunting.') 187 | exit() 188 | starting_outfile = current_outfile 189 | print('[*] Sent {} out of {} notifications.'.format(i - 1, notification_count)) 190 | print('[*] Sleeping for {} minutes...'.format(check_interval)) 191 | time.sleep(float(check_interval) * 60) 192 | except KeyboardInterrupt: 193 | print('[-] Ctrl+C detected. Exiting.') 194 | exit() 195 | 196 | 197 | if __name__ == '__main__': 198 | main() 199 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | ####### hashms requirements.txt ####### 3 | # 4 | ###### Requirements without Version Specifiers ###### 5 | requests 6 | # 7 | --------------------------------------------------------------------------------