├── .gitignore ├── setup.sh ├── .github └── FUNDING.yml ├── LICENSE ├── README.md └── nullinux.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Packaging 2 | develop-eggs/ 3 | __pycache__/ 4 | *.egg-info/ 5 | build/ 6 | dist/ 7 | *.egg 8 | 9 | # Byte-compiled 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # PyCharm 17 | .idea -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # nullinux install script 3 | 4 | if [[ $(id -u) != 0 ]]; then 5 | echo -e "\n[!] Setup script needs to run as root\n\n" 6 | exit 0 7 | fi 8 | 9 | echo -e "\n[*] Starting nullinux setup script" 10 | 11 | echo -e "[*] Checking for smbclient" 12 | if [[ $(smbclient -V 2>&1) == *"not found"* ]] 13 | then 14 | echo -e "[*] Installing smbclient" 15 | apt-get install smbclient -y 16 | else 17 | echo "[+] smbclient installed" 18 | fi 19 | 20 | cp ./nullinux.py /usr/local/bin/nullinux 21 | 22 | chmod +x /usr/local/bin/nullinux 23 | 24 | echo -e "\n[*] nullinux setup complete\n\n" -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: #m8sec 4 | patreon: # Replace with a single patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single ko_fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 m8r0wn 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nullinux 2 | ![](https://img.shields.io/badge/Python-2.7%20&%203+-blue.svg)   3 | ![](https://img.shields.io/badge/License-MIT-green.svg)   4 | [![](https://img.shields.io/badge/Demo-Youtube-red.svg)](https://www.youtube.com/watch?v=akvWRGxxDp0)   5 | 6 | Nullinux is an internal penetration testing tool for Linux that can be used to enumerate OS information, domain information, shares, directories, and users through SMB. If no username and password are provided in the command line arguments, an anonymous login, or null session, is attempted. Nullinux acts as a wrapper around the Samba tools smbclient & rpcclient to enumerate hosts using a variety of techniques. 7 | 8 | Key Features: 9 | * Single or multi-host enumeration 10 | * Enumerate shares and list files in root directory 11 | * Enumerate users & groups 12 | * Multi-threaded RID Cycling 13 | * Creates a formatted nullinux_users.txt output file free of duplicates for further exploitation 14 | * Python 2.7 & 3 compatible 15 | 16 | For more information, and example output, visit the [wiki page](https://github.com/m8r0wn/nullinux/wiki). 17 | 18 | ### Getting Started 19 | In the Linux terminal run: 20 | ``` 21 | git clone https://github.com/m8sec/nullinux 22 | cd nullinux 23 | sudo bash setup.sh 24 | ``` 25 | 26 | ### Usage 27 | ``` 28 | positional arguments: 29 | target Target server 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -v Verbose output 33 | -o OUTPUT_FILE Output users to the specified file 34 | 35 | Authentication: 36 | -u USERNAME, -U USERNAME Username 37 | -p PASSWORD, -P PASSWORD Password 38 | 39 | Enumeration: 40 | -shares Enumerate shares only 41 | -users Enumerate users only 42 | -q, -quick Fast user enumeration 43 | -r, -rid Perform RID cycling only 44 | -range RID_RANGE Set Custom RID cycling range (Default: '500-550') 45 | -T MAX_THREADS Max threads for RID cycling (Default: 15) 46 | ``` 47 | -------------------------------------------------------------------------------- /nullinux.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function 3 | 4 | import sys 5 | import re 6 | import argparse 7 | import datetime 8 | from time import sleep 9 | from ipaddress import IPv4Network 10 | from threading import Thread, activeCount 11 | 12 | if sys.version_info[0] < 3: 13 | from commands import getoutput 14 | else: 15 | from subprocess import getoutput 16 | 17 | class TargetParser(): 18 | # Condensed version of IPParser using only standard libraries 19 | regex = { 20 | 'single': re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"), 21 | 'range': re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}-\d{1,3}$"), 22 | 'cidr': re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$"), 23 | 'dns': re.compile("^.+\.[a-z|A-Z]{2,}$") 24 | } 25 | 26 | def __init__(self): 27 | self.hosts = [] 28 | 29 | def parse(self, target): 30 | try: 31 | self.controller(target) 32 | return self.hosts 33 | except Exception as e: 34 | print_failure('Target Error: {}\n'.format(str(e))) 35 | sys.exit(1) 36 | 37 | def controller(self, target): 38 | if target.endswith('.txt'): 39 | self.fileParser(target) 40 | elif re.match(self.regex['range'], target): 41 | self.rangeParser(target) 42 | elif re.match(self.regex['dns'], target): 43 | self.hosts.append(target) 44 | elif ',' in target: 45 | self.multiParser(target) 46 | else: 47 | for ip in IPv4Network(target): 48 | self.hosts.append(ip) 49 | 50 | def fileParser(self, filename): 51 | with open(filename, 'r') as f: 52 | for line in f: 53 | self.controller(line.strip()) 54 | 55 | def multiParser(self, target): 56 | for t in target.strip().split(','): 57 | self.controller(t) 58 | 59 | def rangeParser(self, target): 60 | a = target.split("-") 61 | b = a[0].split(".") 62 | for x in range(int(b[3]), int(a[1]) + 1): 63 | tmp = b[0] + "." + b[1] + "." + b[2] + "." + str(x) 64 | self.hosts.append(tmp) 65 | 66 | 67 | class nullinux(): 68 | known_users = ['Administrator', 'Guest', 'krbtgt', 'root', 'bin'] 69 | domain_sid = "" 70 | acquired_users = [] 71 | 72 | def __init__(self, username, password, verbose, output_file): 73 | self.username = username 74 | self.password = password 75 | self.verbose = verbose 76 | self.output_file = output_file 77 | 78 | def enum_os(self, target): 79 | cmd = "smbclient //{}/IPC$ -U {}%{} -t 1 -c exit".format(target,self.username, self.password) 80 | for line in getoutput(cmd).splitlines(): 81 | if "Domain=" in line: 82 | # OS info is no longer enumerated in newer Windows servers 83 | print_success("{}: {}".format(target, line)) 84 | elif "NT_STATUS_LOGON_FAILURE" in line: 85 | print_failure("{}: Authentication Failed".format(target)) 86 | return False 87 | return True 88 | 89 | def get_dom_sid(self, target): 90 | print("\n\033[1;34m[*]\033[1;m Enumerating Domain Information for: {}".format(target)) 91 | cmd = "rpcclient -c lsaquery -U {}%{} {}".format(self.username, self.password, target) 92 | for line in getoutput(cmd).splitlines(): 93 | if "Domain Name:" in line: 94 | print_success(line) 95 | elif "Domain Sid:" in line: 96 | self.domain_sid = line.split(":")[1].strip() 97 | print_success("Domain SID: {}".format(self.domain_sid)) 98 | if not self.domain_sid: 99 | print_failure("Could not attain Domain SID") 100 | 101 | def create_userfile(self): 102 | openfile = open(self.output_file, 'a') 103 | for user in self.acquired_users: 104 | openfile.write('{}\n'.format(user)) 105 | openfile.close() 106 | 107 | def enum_shares(self, target): 108 | count = 0 109 | acquired_shares = [] 110 | smbclient_types = ['Disk', 'IPC', 'Printer'] 111 | print("\n\033[1;34m[*]\033[1;m Enumerating Shares for: {}".format(target)) 112 | cmd = "smbclient -L {} -U {}%{} -t 2".format(target, self.username, self.password) 113 | for line in getoutput(cmd).splitlines(): 114 | if count == 0: #Print Enum Share Heading 115 | print(" {:26} {}".format("Shares", "Comments")) 116 | print(" " + "-" * 43) 117 | count += 1 118 | for t in smbclient_types: #Check if output in known share types 119 | if t in line: 120 | try: 121 | if 'IPC$' in line: 122 | print(" \\\{}\{}".format(target, "IPC$")) 123 | acquired_shares.append("IPC$") 124 | else: 125 | share = line.split(t)[0].strip() 126 | comment = line.split(t)[1].strip() 127 | print(" \\\{}\{:15} {}".format(target, share, comment)) 128 | acquired_shares.append(share) 129 | except KeyboardInterrupt: 130 | print("\n[!] Key Event Detected...\n\n") 131 | sys.exit(0) 132 | except: 133 | pass 134 | if acquired_shares: 135 | #Enumerate dir of each new share 136 | for s in acquired_shares: 137 | self.enum_dir(target, s) 138 | else: 139 | print(" ") 140 | print_failure("No Shares Detected") 141 | 142 | def share_header(self, target, share): 143 | print("\n ", end='') 144 | print_status("Enumerating: \\\%s\%s" % (target, share)) 145 | 146 | def enum_dir(self, target, share): 147 | header_count = 0 148 | cmd = "smbclient //{}/\'{}\' -t 3 -U {}%{} -c dir".format(target, share, self.username, self.password) 149 | for line in getoutput(cmd).splitlines(): 150 | if "NT_STATUS" in line or "_ACCESS_DENIED" in line: 151 | if self.verbose: 152 | if header_count == 0: 153 | header_count += 1 154 | self.share_header(target, share) 155 | print(" ", end='') 156 | print_failure(line) 157 | elif "Domain=" in line or "blocks available" in line or "WARNING" in line or "failed:" in line or not line: 158 | pass 159 | else: 160 | if header_count == 0: 161 | header_count += 1 162 | self.share_header(target, share) 163 | print(" "+line) 164 | 165 | def enum_querydispinfo(self, target): 166 | print("\n\033[1;34m[*]\033[1;m Enumerating querydispinfo for: {}".format(target)) 167 | cmd = "rpcclient -c querydispinfo -U {}%{} {}".format(self.username, self.password, target) 168 | for line in getoutput(cmd).splitlines(): 169 | try: 170 | user_account = line.split("Name:")[0].split("Account:")[1].strip() 171 | print(" " + user_account) 172 | if user_account not in self.acquired_users: 173 | self.acquired_users.append(user_account) 174 | except KeyboardInterrupt: 175 | print("\n[!] Key Event Detected...\n\n") 176 | sys.exit(0) 177 | except: 178 | pass 179 | 180 | def enum_enumdomusers(self, target): 181 | print("\n\033[1;34m[*]\033[1;m Enumerating enumdomusers for: {}".format(target)) 182 | cmd = "rpcclient -c enumdomusers -U {}%{} {}".format(self.username, self.password, target) 183 | for line in getoutput(cmd).splitlines(): 184 | try: 185 | user_account = line.split("[")[1].split("]")[0].strip() 186 | print(" "+user_account) 187 | if user_account not in self.acquired_users: 188 | self.acquired_users.append(user_account) 189 | except KeyboardInterrupt: 190 | print("\n[!] Key Event Detected...\n\n") 191 | sys.exit(0) 192 | except: 193 | pass 194 | 195 | def enum_lsa(self, target): 196 | print("\n\033[1;34m[*]\033[1;m Enumerating LSA for: {}".format(target)) 197 | cmd = "rpcclient -c lsaenumsid -U {}%{} {}".format(self.username, self.password, target) 198 | output = getoutput(cmd) 199 | for line in output.splitlines(): 200 | try: 201 | if "S-1-5-21" in line: 202 | user_sid = "rpcclient -c 'lookupsids {}' -U {}%{} {}".format(line, self.username, self.password, target) 203 | for x in getoutput(user_sid).splitlines(): 204 | user_account = x.split("\\")[1].split("(")[0].strip() 205 | count = int(x.split("(")[1].split(")")[0].strip()) 206 | if count == 1: 207 | if self.verbose: 208 | print(" "+x) 209 | else: 210 | print(" "+user_account) 211 | if user_account not in self.acquired_users: 212 | self.acquired_users.append(user_account) 213 | elif count > 1 and "*unknown*\*unknown*" not in line: 214 | if self.verbose: 215 | print(" {:35} (Network/LocalGroup)".format(x)) 216 | else: 217 | print(" {:35} (Network/Local Group)".format(user_account)) 218 | except KeyboardInterrupt: 219 | print("\n[!] Key Event Detected...\n\n") 220 | sys.exit(0) 221 | except: 222 | pass 223 | 224 | def rid_cycling(self, target, ridrange, max_threads): 225 | print("\n\033[1;34m[*]\033[1;m Performing RID Cycling for: {}".format(target)) 226 | if not self.domain_sid: 227 | print_failure("RID Failed: Could not attain Domain SID") 228 | return False 229 | # Handle custom RID range input 230 | try: 231 | r = ridrange.split("-") 232 | rid_range = list(range(int(r[0]), int(r[1])+1)) 233 | except: 234 | print_failure("Error parsing custom RID range, reverting to default") 235 | rid_range = list(range(500, 551)) 236 | for rid in rid_range: 237 | try: 238 | Thread(target=self.rid_thread, args=(rid,target,), daemon=True).start() 239 | except: 240 | pass 241 | while activeCount() > max_threads: 242 | sleep(0.001) 243 | while activeCount() > 1: 244 | sleep(0.001) 245 | 246 | def rid_thread(self, rid, target): 247 | cmd = "rpcclient -c \"lookupsids {}-{}\" -U {}%{} {}".format(self.domain_sid, rid, self.username, self.password,target) 248 | for line in getoutput(cmd).splitlines(): 249 | if "S-1-5-21" in line: 250 | # Split output to get username/group name 251 | user_account = line.split("\\")[1].split("(")[0].strip() 252 | count = int(line.split("(")[1].split(")")[0].strip()) 253 | if count == 1: 254 | if self.verbose: 255 | print(" " + line) 256 | else: 257 | print(" " + user_account) 258 | if user_account not in self.acquired_users: 259 | self.acquired_users.append(user_account) 260 | elif count > 1 and "*unknown*\*unknown*" not in line: 261 | if self.verbose: 262 | print(" {:35} (Network/LocalGroup)".format(line)) 263 | else: 264 | print(" {:35} (Network/LocalGroup)".format(user_account)) 265 | 266 | def enum_known_users(self, target): 267 | print("\n\033[1;34m[*]\033[1;m Testing {} for Known Users".format(target)) 268 | for user in self.known_users: 269 | cmd = "rpcclient -c \"lookupnames {}\" -U {}%{} {}".format(user, self.username, self.password, target) 270 | for line in getoutput(cmd).splitlines(): 271 | if "S-1-5" in line: 272 | try: 273 | user_account = line.split(" ")[0].strip() 274 | if self.verbose: 275 | print(" " + line) 276 | else: 277 | print(" " + user_account) 278 | if user_account not in self.acquired_users and int(line.split("User:")[1]) == 1: 279 | self.acquired_users.append(user_account) 280 | except KeyboardInterrupt: 281 | print("\n[!] Key Event Detected...\n\n") 282 | sys.exit(0) 283 | except: 284 | pass 285 | 286 | def enum_dom_groups(self, target): 287 | print("\n\033[1;34m[*]\033[1;m Enumerating Group Memberships for: {}".format(target)) 288 | cmd = "rpcclient -c enumdomgroups -U {}%{} {}".format(self.username, self.password, target) 289 | for line in getoutput(cmd).splitlines(): 290 | if "rid:" in line: 291 | try: 292 | group = line.split("[")[1].split("]")[0].strip() 293 | print_success("Group: %s" % (group)) 294 | self.enum_group_mem(target, group) 295 | except KeyboardInterrupt: 296 | print("\n[!] Key Event Detected...\n\n") 297 | sys.exit(0) 298 | except: 299 | pass 300 | 301 | def enum_group_mem(self, target, group): 302 | cmd = "net rpc group members \'{}\' -U {}%{} -I {}".format(group, self.username, self.password, target) 303 | for line in getoutput(cmd).splitlines(): 304 | try: 305 | user_account = line.split("\\")[1].strip() 306 | print(" " + user_account) 307 | if user_account not in self.acquired_users: 308 | self.acquired_users.append(user_account) 309 | except KeyboardInterrupt: 310 | print("\n[!] Key Event Detected...\n\n") 311 | sys.exit(0) 312 | except: 313 | pass 314 | 315 | def print_success(msg): 316 | print('\033[1;32m[+]\033[0m {}'.format(msg)) 317 | 318 | def print_status(msg): 319 | print('\033[1;34m[*]\033[0m {}'.format(msg)) 320 | 321 | def print_failure(msg): 322 | print('\033[1;31m[-]\033[0m {}'.format(msg)) 323 | 324 | def time_stamp(): 325 | return datetime.datetime.now().strftime('%m-%d-%Y %H:%M') 326 | 327 | def nullinux_enum(args, scan, target): 328 | scan.enum_os(target) 329 | if args.users: 330 | scan.enum_shares(target) 331 | if args.shares: 332 | if not scan.domain_sid: 333 | scan.get_dom_sid(target) 334 | scan.enum_querydispinfo(target) 335 | scan.enum_enumdomusers(target) 336 | if not args.quick: 337 | scan.enum_lsa(target) 338 | scan.rid_cycling(target, args.rid_range, args.max_threads) 339 | scan.enum_known_users(target) 340 | scan.enum_dom_groups(target) 341 | 342 | def main(args): 343 | print("\n Starting nullinux v{} | {}\n\n".format(version, time_stamp())) 344 | scan = nullinux('\"{}\"'.format(args.username), '\"{}\"'.format(args.password), args.verbose, args.output_file) 345 | for t in args.target: 346 | try: 347 | if args.rid_only: 348 | scan.get_dom_sid(t) 349 | scan.rid_cycling(t, args.rid_range, args.max_threads) 350 | else: 351 | nullinux_enum(args, scan, t) 352 | except Exception as e: 353 | print("\n[*] Main Error: {}\n\n".format(e)) 354 | 355 | if args.users: 356 | print("\n\033[1;34m[*]\033[1;m {} unique user(s) identified".format(len(scan.acquired_users))) 357 | if scan.acquired_users: 358 | print("\033[1;32m[+]\033[1;m Writing users to file: {}\n".format(args.output_file)) 359 | scan.create_userfile() 360 | 361 | if __name__ == '__main__': 362 | try: 363 | version = '5.5.0dev' 364 | args = argparse.ArgumentParser(description=(""" 365 | nullinux | v{0} 366 | ----------------------------------- 367 | SMB null-session enumeration tool to gather OS, 368 | user, share, and domain information. 369 | 370 | usage: 371 | nullinux -users -quick DC1.demo.local,10.0.1.1 372 | nullinux -rid -range 500-600 10.0.0.1 373 | nullinux -shares -U 'Domain\\User' -P 'Password1' 10.0.0.1""").format(version), formatter_class=argparse.RawTextHelpFormatter, usage=argparse.SUPPRESS) 374 | args.add_argument('-v', dest="verbose", action='store_true', help="Verbose output") 375 | args.add_argument('-o', dest="output_file", type=str, default="./nullinux_users.txt", help="Output users to the specified file") 376 | auth = args.add_argument_group("Authentication") 377 | auth.add_argument('-u', '-U', dest='username', type=str, default="", help='Username') 378 | auth.add_argument('-p', '-P', dest='password', type=str, default="", help='Password') 379 | enum = args.add_argument_group("Enumeration") 380 | enum.add_argument('-shares', dest="shares", action='store_false', help="Enumerate shares only") 381 | enum.add_argument('-users', dest="users", action='store_false', help="Enumerate users only") 382 | enum.add_argument('-q', '-quick', dest="quick", action='store_true', help="Fast user enumeration") 383 | enum.add_argument('-r', '-rid', dest="rid_only", action='store_true', help="Perform RID cycling only") 384 | enum.add_argument('-range', dest='rid_range', type=str, default="500-550", help='Set Custom RID cycling range (Default: \'500-550\')') 385 | enum.add_argument('-T', dest='max_threads', type=int, default=15, help='Max threads for RID cycling (Default: 15)') 386 | args.add_argument(dest='target', nargs='+', help='Target server') 387 | args = args.parse_args() 388 | args.target = TargetParser().parse(args.target[0]) 389 | main(args) 390 | except KeyboardInterrupt: 391 | print("\n[!] Key Event Detected...\n\n") 392 | sys.exit(0) 393 | --------------------------------------------------------------------------------