├── .gitignore ├── README.md └── hash.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HashBot 2 | ------ 3 | Willie module for sending hashes to hashcat to be cracked. As soon as a hash is cracked, HashBot will PM the invoker with the cracked hash and plaintext. 4 | 5 | 6 | #### Usage 7 | 8 | .hash [hashmode] [ruleset] [hash] [hash] ... [email] 9 | 10 | The hashmode must be the corresponding number of the type of hash you wish to crack. A complete list can be found here: [List of hashcat hash modes](http://hashcat.net/wiki/doku.php?id=example_hashes) 11 | 12 | You may also replace the hashmode number with one of the following types: sha1, md5, kerberos, ntlm, netntlmv2, netntlmv1, sha512 13 | 14 | If no ruleset is given, Hashbot will default to best64.rule. All the examples below will run exactly the same hashcat command on the server: 15 | 16 | .hash md5 best64.rule 8743b52063cd84097a65d1633f5c74f5 17 | 18 | .hash 0 best64.rule 8743b52063cd84097a65d1633f5c74f5 19 | 20 | .hash md5 8743b52063cd84097a65d1633f5c74f5 21 | 22 | 23 | If you wish to receive an email once the hashcat session is done, simply add your email address to the end of the command. Hashbot will include all the cracked hashes in the email. 24 | 25 | .hash md5 8743b52063cd84097a65d1633f5c74f5 danhmcinerney@gmail.com 26 | 27 | 28 | #### IRC Commands 29 | 30 | See all currently active sessions 31 | 32 | .sessions 33 | 34 | 35 | Kill a session 36 | 37 | .kill [session name] 38 | 39 | 40 | Help 41 | 42 | .help 43 | -------------------------------------------------------------------------------- /hash.py: -------------------------------------------------------------------------------- 1 | ''' 2 | hash.py - Willie module that calls hashcat 3 | Love, DanMcInerney 4 | ''' 5 | # Willie installation 6 | # https://flexion.org/posts/2014-08-installing-willie-irc-bot-on-debian.html 7 | 8 | import io 9 | import os 10 | import re 11 | import time 12 | import glob 13 | import pipes 14 | import string 15 | import random 16 | import signal 17 | import smtplib 18 | import paramiko 19 | import subprocess 20 | import multiprocessing 21 | from willie.module import commands, example 22 | 23 | sessions = {} 24 | # Get all rulefiles (and only files, no dirs) from the rules directory 25 | rulepath = '/opt/oclHashcat-1.36/rules/' 26 | all_rules = [f for f in os.listdir(rulepath) if os.path.isfile(os.path.join(rulepath, f))] 27 | 28 | @commands('help') 29 | def help(bot, trigger): 30 | ''' 31 | Print out the rules and hash types 32 | ''' 33 | # Examples 34 | bot.msg(trigger.nick, 'Usage: ".hash [hashmode] [ruleset] [hash] [hash] [hash] ... [email]"') 35 | bot.msg(trigger.nick, 'Type ".rules" to see a list of rules available') 36 | bot.msg(trigger.nick, 'Type ".sessions" to see a list of active sessions') 37 | bot.msg(trigger.nick, 'Type ".kill " to kill an active session; enter one session at a time') 38 | bot.msg(trigger.nick, 'Output files are dumped to 10.0.0.240:/home/hashbot/ in format -cracked-<6 char ID>.txt') 39 | 40 | @commands('rules') 41 | def rules(bot, trigger): 42 | ''' 43 | Hardcoded list of rules, might make the bot SSH to the rules 44 | dir and list them that way at some point but for now this is 45 | easier and the rules don't change hardly ever 46 | ''' 47 | bot.say('Rules: (takes a moment)') 48 | bot.say('%s' % ' | '.join(all_rules)) 49 | 50 | @commands('kill') 51 | def kill(bot, trigger): 52 | ''' 53 | Kill a session 54 | Cleanup occurs automatically 55 | ''' 56 | global sessions 57 | kill_session = trigger.group(2) 58 | if kill_session: 59 | kill_session = kill_session.strip() 60 | if kill_session in sessions: 61 | bot.say('Killing session: %s' % kill_session) 62 | os.killpg(sessions[kill_session].pid, signal.SIGTERM) 63 | return 64 | 65 | bot.say('No session by that name found. Please enter a single session to kill, .kill , \ 66 | or type .sessions to see all sessions') 67 | 68 | @commands('sessions') 69 | def sessions_printer(bot, trigger): 70 | ''' 71 | Print all sessions 72 | ''' 73 | if len(sessions) == 0: 74 | bot.say('No current sessions initiatied by HashBot') 75 | else: 76 | sessions_list = [k for k in sessions] 77 | bot.say('Current sessions: %s' % ' '.join(sessions_list)) 78 | 79 | @commands('hash') 80 | def hash(bot, trigger): 81 | ''' 82 | Function that's called when user types .hash 83 | ''' 84 | sanitize = re.compile('[\W_]+') 85 | # trigger = u'.hash arg1 arg2...' 86 | # trigger.group(1) = u'hash' 87 | # trigger.group(2) = u'arg1 arg2...' 88 | if not trigger.group(2): 89 | wrong_cmd(bot) 90 | return 91 | 92 | args = trigger.group(2).split() 93 | if len(args) > 1: 94 | 95 | # Sanitize the nick 96 | nick = str(trigger.nick) 97 | sani_nick = sanitize.sub('', nick) 98 | 99 | mode, rule, hashes, email = get_options(bot, args, nick) 100 | if mode and rule and hashes: 101 | # Handle hashcat sessions 102 | sessionname = session_handling(sani_nick) 103 | filename = '/tmp/%s-hashes.txt' % sessionname 104 | 105 | # Download hash file (hashes is a list] 106 | if 'http://' in hashes[0]: 107 | # Sanitize the URL for shell chars 108 | cmd = ['/usr/bin/wget', '-O', filename, pipes.quote(hashes[0])] 109 | subprocess.call(cmd) 110 | 111 | # If no URL, proceed with textually input hashes 112 | else: 113 | write_hashes_to_file(bot, hashes, nick, filename) 114 | 115 | run_cmds(bot, nick, sessionname, mode, rule, email) 116 | else: 117 | wrong_cmd(bot) 118 | 119 | def session_handling(sani_nick): 120 | ''' 121 | Keep track of the sessions 122 | ''' 123 | # Prevent dupe sessions 124 | counter = 1 125 | sessionname = sani_nick 126 | while sessionname in sessions: 127 | sessionname = sani_nick + str(counter) 128 | counter += 1 129 | 130 | return sessionname 131 | 132 | def get_options(bot, args, nick): 133 | ''' 134 | Grab the args the user gives 135 | ''' 136 | email = None 137 | rule = None 138 | hashes = args[1:] 139 | mode = args[0] 140 | common_hashcat_codes = {'ntlm':'1000', 'netntlmv2':'5600', 'ntlmv2':'5600', 'netntlmv1':'5500', 141 | 'sha1':'100', 'md5':'0', 'sha512':'1800', 'kerberos':'7500'} 142 | if mode in common_hashcat_codes: 143 | mode = common_hashcat_codes[mode] 144 | 145 | # Pull out the rule and email from the args 146 | for x in hashes: 147 | # Check if a rule was used, right now only supports one rule 148 | if x in all_rules: 149 | rule = x 150 | # Check for an email address, the only hash that may have an @ in it is Lastpass 151 | # so we gotta make sure the string with @ doesn't also have a : which Lastpass does 152 | if '@' in x and ':' not in x: 153 | email = x 154 | 155 | # Remove the email address and rule from the list of hashes if they exist 156 | if email: 157 | hashes.remove(email) 158 | if rule: 159 | hashes.remove(rule) 160 | else: 161 | rule = 'best64.rule' 162 | bot.msg(nick,'Defaulting to best64.rule. Type .rules to see a list of available rulesets.') 163 | 164 | if len(hashes) == 0: 165 | bot.say('No hashes entered. Please enter in form: .hash [hashtype] [ruleset] [hash] [hash] ... [email]') 166 | return None, None, None 167 | 168 | return mode, rule, hashes, email 169 | 170 | def create_identifier(): 171 | identifier = '' 172 | for x in xrange(0,6): 173 | identifier += random.choice(string.letters) 174 | return identifier 175 | 176 | def run_cmds(bot, nick, sessionname, mode, rule, email): 177 | ''' 178 | Handle interaction with crackerbox 179 | ''' 180 | global sessions 181 | identifier = create_identifier() 182 | 183 | #Create hashcat cmd 184 | cmd = create_cmd(bot, nick, sessionname, mode, rule, identifier) 185 | split_cmd = cmd.split() 186 | 187 | # Continuously poll cracked pw file while waiting for hashcat to finish 188 | cracked = find_cracked_pw(bot, nick, sessionname, mode, identifier, split_cmd) 189 | 190 | cleanup(bot, nick, sessionname, len(cracked), email, identifier) 191 | 192 | def create_cmd(bot, nick, sessionname, mode, rule, identifier): 193 | '''Create hashcat cmd''' 194 | wordlists = ' '.join(glob.glob('/opt/wordlists/*')) 195 | cmd = '/opt/oclHashcat-1.36/oclHashcat64.bin \ 196 | --session %s -m %s -o /home/hashbot/%s-cracked-%s.txt /tmp/%s-hashes.txt %s \ 197 | -r /opt/oclHashcat-1.36/rules/%s'\ 198 | % (sessionname, mode, sessionname, identifier, sessionname, wordlists, rule) 199 | print_cmd = '/opt/oclHashcat-1.36/oclHashcat64.bin \ 200 | --session %s -m %s -o /home/hashbot/%s-cracked-%s.txt /tmp/%s-hashes.txt /opt/wordlists/* \ 201 | -r /opt/oclHashcat-1.36/rules/%s'\ 202 | % (sessionname, mode, sessionname, identifier, sessionname, rule) 203 | 204 | bot.say('Hashcat session name: %s' % sessionname) 205 | bot.msg(nick, 'Cmd: %s' % print_cmd) 206 | 207 | return cmd 208 | 209 | def write_hashes_to_file(bot, hashes, nick, filename): 210 | ''' 211 | Write to /tmp/sessionname-hashes.txt 212 | ''' 213 | with open(filename, 'a+') as f: 214 | for h in hashes: 215 | h = clean_hash(bot, h, nick) 216 | if h != None: 217 | f.write(h+'\n') 218 | 219 | def clean_hash(bot, h, nick): 220 | ''' 221 | Sanitize and confirm hash doesn't have blank hex/unicode chars 222 | ''' 223 | # Sometimes copy pasta causes blank characters at the end or beginning 224 | try: 225 | h.decode('utf8') 226 | except UnicodeEncodeError: 227 | bot.msg(nick, 'Unicode encode error with hash: %s' % repr(h)) 228 | bot.msg(nick, 'If you copy and pasted it, try just deleting and retyping \ 229 | the first and last characters') 230 | return 231 | 232 | return h 233 | 234 | def find_cracked_pw(bot, nick, sessionname, mode, identifier, split_cmd): 235 | ''' 236 | While the hashcat cmd is running, constantly check sessionname-output.txt 237 | for cracked hashes 238 | ''' 239 | cracked = [] 240 | 241 | output_file = '%s-output-%s.txt' % (sessionname, identifier) 242 | 243 | with open(output_file, 'w') as f: 244 | # Run hashcat 245 | hashcat_cmd = subprocess.Popen(split_cmd, stdout=f, stderr=f, preexec_fn=os.setsid) 246 | sessions[sessionname] = hashcat_cmd 247 | 248 | while hashcat_cmd.poll() is None: 249 | cracked += read_cracked_pws(mode, bot, cracked, sessionname, nick, '%s.pot' % sessionname) 250 | time.sleep(1) 251 | 252 | # If it ends in error 253 | with open(output_file, 'r') as out: 254 | output = out.read() 255 | err = print_errors(bot, output) 256 | 257 | # Check one more time for cracked pws 258 | cracked += read_cracked_pws(mode, bot, cracked, sessionname, nick, '%s.pot' % sessionname) 259 | 260 | return cracked 261 | 262 | def print_errors(bot, output): 263 | '''Check and print errors''' 264 | lines = output.splitlines() 265 | for l in lines: 266 | if 'WARNING:' in l or 'ERROR:' in l: 267 | bot.say(l) 268 | return True 269 | 270 | def read_cracked_pws(mode, bot, cracked, sessionname, nick, pw_file): 271 | '''Read from hashcats cracked file''' 272 | new_cracked = [] 273 | 274 | if os.path.isfile(pw_file): 275 | with open(pw_file) as f: 276 | for l in f.readlines(): 277 | # Prevent the cracked message from being too long for ntlmv2 278 | if mode in ['5600','ntlmv2', 'netntlmv2']: 279 | l = l.split(':') 280 | try: 281 | # IRC has char limits that hashes sometimes exceed 282 | l = l[0]+':'+l[2]+':'+l[-1] 283 | except IndexError: 284 | continue 285 | if l not in cracked: 286 | bot.msg(nick, 'Cracked! %s' % l) 287 | new_cracked.append(l) 288 | 289 | return new_cracked 290 | 291 | def send_email(email, sessionname, cracked, cracked_file): 292 | ''' 293 | If user gave an email in the args, send an email when a hash is cracked 294 | ''' 295 | # If the session is killed with ".kill" cmd, then cracked_file may not exist 296 | try: 297 | cracked_hashes = open(cracked_file).read() 298 | except IOError: 299 | cracked_hashes = "Session killed with \".kill\" command and no hashes were cracked." 300 | 301 | from_addr = 'HashbotCF@gmail.com' 302 | password = open('/home/hashbot/.willie/modules/mail-password.txt').read() 303 | msg = "\r\n".join(["From: %s" % from_addr, 304 | "To: %s" % email, 305 | "Subject: Hashcat session \"%s\" completed" % sessionname, 306 | "", 307 | "Finished hashcat session \"%s\", cracked %s hash(es)\n" % (sessionname, str(cracked)), 308 | cracked_hashes]) 309 | 310 | try: 311 | # The actual mail send 312 | server = smtplib.SMTP('smtp.gmail.com:587') 313 | server.starttls() 314 | server.login(from_addr,password) 315 | server.sendmail(from_addr, email, msg) 316 | server.quit() 317 | except Exception as e: 318 | print '[-] Emailed to %s failed: %s' % (email, str(e)) 319 | 320 | def cleanup(bot, nick, sessionname, cracked, email, identifier): 321 | ''' 322 | Cleanup the left over files, save the hashes 323 | ''' 324 | global sessions 325 | 326 | cracked_file = '/home/hashbot/%s-cracked-%s.txt' % (sessionname, identifier) 327 | cracked_pws = '/home/hashbot/%s-output.txt' % sessionname 328 | log_file = '/home/hashbot/%s-log-%s.txt' % (sessionname, identifier) 329 | #err_file = '/home/hashbot/%s-errors-%s.txt' % (sessionname, identifier) 330 | 331 | # Move the cracked hashes and log files to ID'd filenames 332 | if os.path.isfile(cracked_pws): 333 | subprocess.call(['mv', cracked_pws, cracked_file]) 334 | if os.path.isfile(log_file): 335 | subprocess.call(['mv', '%s.log' % sessionname, log_file]) 336 | 337 | # Cleanup files 338 | subprocess.call(['rm', '-rf', '/home/hashbot/%s.pot' % sessionname, 339 | '/tmp/%s-hashes.txt' % sessionname, 340 | '/home/hashbot/%s.induct' % sessionname, 341 | '/home/hashbot/%s.restore' % sessionname, 342 | '/home/hashbot/%s.outfiles' % sessionname]) 343 | 344 | del sessions[sessionname] 345 | bot.reply('completed session %s and cracked %s hash(es)' % (sessionname, str(cracked))) 346 | bot.msg(nick,'Hashcat finished, %d hash(es) stored on 10.0.0.240 at %s and %s'\ 347 | % (cracked, cracked_file, log_file)) 348 | 349 | # Send an email if it was given 350 | if email: 351 | send_email(email, sessionname, cracked, cracked_file) 352 | 353 | def wrong_cmd(bot): 354 | bot.say('Please enter hashes in the following form:') 355 | bot.say('.hash [hashtype] [ruleset] [hash] [hash] [hash] ...') 356 | bot.say('.hash ntlmv2 best64.rule 9D7E463A630AD...') 357 | bot.say('Use ".help" to see available rulesets and hashtypes') 358 | --------------------------------------------------------------------------------