Path: blob/master/lib/postgres/postgres-pr/connection.rb
23771 views
# -*- coding: binary -*-1#2# Author:: Michael Neumann3# Copyright:: (c) 2005 by Michael Neumann4# License:: Same as Ruby's or BSD5#67require 'postgres_msf'8require 'postgres/postgres-pr/message'9require 'postgres/postgres-pr/version'10require 'postgres/postgres-pr/scram_sha_256'11require 'uri'12require 'rex/socket'1314# Namespace for Metasploit branch.15module Msf16module Db1718module PostgresPR1920PROTO_VERSION = 3 << 16 #1966082122class AuthenticationMethodMismatch < StandardError23end2425class Connection2627# Allow easy access to these instance variables28attr_reader :conn, :params, :transaction_status2930# A block which is called with the NoticeResponse object as parameter.31attr_accessor :notice_processor3233#34# Returns one of the following statuses:35#36# PQTRANS_IDLE = 0 (connection idle)37# PQTRANS_INTRANS = 2 (idle, within transaction block)38# PQTRANS_INERROR = 3 (idle, within failed transaction)39# PQTRANS_UNKNOWN = 4 (cannot determine status)40#41# Not yet implemented is:42#43# PQTRANS_ACTIVE = 1 (command in progress)44#45def transaction_status46case @transaction_status47when ?I48049when ?T50251when ?E52353else54455end56end5758def initialize(database, user, password=nil, uri = nil, proxies = nil, ssl = nil, ssl_opts = {})59uri ||= DEFAULT_URI6061@transaction_status = nil62@params = { 'username' => user, 'database' => database }63establish_connection(uri, proxies, ssl, ssl_opts)6465# Check if the password supplied is a Postgres-style md5 hash66md5_hash_match = password.match(/^md5([a-f0-9]{32})$/)6768write_message(StartupMessage.new(PROTO_VERSION, 'user' => user, 'database' => database))6970loop do71msg = Message.read(@conn)7273case msg74when AuthentificationClearTextPassword75raise ArgumentError, "no password specified" if password.nil?76raise AuthenticationMethodMismatch, "Server expected clear text password auth" if md5_hash_match77write_message(PasswordMessage.new(password))78when AuthentificationCryptPassword79raise ArgumentError, "no password specified" if password.nil?80raise AuthenticationMethodMismatch, "Server expected crypt password auth" if md5_hash_match81write_message(PasswordMessage.new(password.crypt(msg.salt)))82when AuthentificationMD5Password83raise ArgumentError, "no password specified" if password.nil?84require 'digest/md5'8586if md5_hash_match87m = md5_hash_match[1]88else89m = Digest::MD5.hexdigest(password + user)90end91m = Digest::MD5.hexdigest(m + msg.salt)92m = 'md5' + m9394write_message(PasswordMessage.new(m))9596when AuthenticationSASL97negotiate_sasl(msg, user, password)98when UnknownAuthType99raise "unknown auth type '#{msg.auth_type}' with buffer content:\n#{Rex::Text.to_hex_dump(msg.buffer.content)}"100101when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential102raise "unsupported authentication"103104when AuthentificationOk105when ErrorResponse106handle_server_error_message(msg)107when NoticeResponse108@notice_processor.call(msg) if @notice_processor109when ParameterStatus110@params[msg.key] = msg.value111when BackendKeyData112# TODO113#p msg114when ReadyForQuery115@transaction_status = msg.backend_transaction_status_indicator116break117else118raise "unhandled message type"119end120end121end122123def peerhost124@conn.peerhost125end126127def peerport128@conn.peerport129end130131def peerinfo132"#{peerhost}:#{peerport}"133end134135def current_database136@params['database']137end138139# List of supported PostgreSQL platforms & architectures:140# https://postgrespro.com/docs/postgresql/16/supported-platforms141def map_compile_os_to_platform(compile_os)142return Msf::Platform::Unknown.realname if compile_os.blank?143144compile_os = compile_os.downcase.encode(::Encoding::BINARY)145146if compile_os.match?('linux')147platform = Msf::Platform::Linux148elsif compile_os.match?(/(darwin|mac|osx)/)149platform = Msf::Platform::OSX150elsif compile_os.match?('win')151platform = Msf::Platform::Windows152elsif compile_os.match?('free')153platform = Msf::Platform::FreeBSD154elsif compile_os.match?('net')155platform = Msf::Platform::NetBSD156elsif compile_os.match?('open')157platform = Msf::Platform::OpenBSD158elsif compile_os.match?('solaris')159platform = Msf::Platform::Solaris160elsif compile_os.match?('aix')161platform = Msf::Platform::AIX162elsif compile_os.match?('hpux')163platform = Msf::Platform::HPUX164elsif compile_os.match?('irix')165platform = Msf::Platform::Irix166else167# Return the query result if the value can't be mapped168return compile_os169end170171platform.realname172end173174# List of supported PostgreSQL platforms & architectures:175# https://postgrespro.com/docs/postgresql/16/supported-platforms176def map_compile_arch_to_architecture(compile_arch)177return '' if compile_arch.blank?178179compile_arch = compile_arch.downcase.encode(::Encoding::BINARY)180181if compile_arch.match?('sparc')182if compile_arch.include?('64')183arch = ARCH_SPARC64184else185arch = ARCH_SPARC186end187elsif compile_arch.include?('mips')188arch = ARCH_MIPS189elsif compile_arch.include?('ppc')190arch = ARCH_PPC191elsif compile_arch.match?('arm')192if compile_arch.match?('64')193arch = ARCH_AARCH64194elsif compile_arch.match?('arm')195arch = ARCH_ARMLE196end197elsif compile_arch.match?('64')198arch = ARCH_X86_64199elsif compile_arch.match?('86') || compile_arch.match?('i686')200arch = ARCH_X86201else202# Return the query result if the value can't be mapped203arch = compile_arch204end205206arch207end208209# @return [Hash] Detect the platform and architecture of the PostgreSQL server:210# * :arch [String] The server architecture.211# * :platform [String] The server platform.212def detect_platform_and_arch213result = {}214215query_result = query('select version()').rows[0][0]216match_platform_and_arch = query_result.match(/on (?<architecture>\w+)-\w+-(?<platform>\w+)/)217218if match_platform_and_arch.nil?219arch = platform = query_result220else221arch = match_platform_and_arch[:architecture]222platform = match_platform_and_arch[:platform]223end224225result[:arch] = map_compile_arch_to_architecture(arch)226result[:platform] = map_compile_os_to_platform(platform)227228result229end230231def close232raise "connection already closed" if @conn.nil?233if @conn.respond_to?(:shutdown)234@conn.shutdown235elsif @conn.respond_to?(:close)236@conn.close237end238@conn = nil239end240241class Result242attr_accessor :rows, :fields, :cmd_tag243def initialize(rows=[], fields=[])244@rows, @fields = rows, fields245end246end247248def query(sql)249write_message(Query.new(sql))250251result = Result.new252errors = []253254loop do255msg = Message.read(@conn)256case msg257when DataRow258result.rows << msg.columns259when CommandComplete260result.cmd_tag = msg.cmd_tag261when ReadyForQuery262@transaction_status = msg.backend_transaction_status_indicator263break264when RowDescription265result.fields = msg.fields266when CopyInResponse267when CopyOutResponse268when EmptyQueryResponse269when ErrorResponse270# TODO271errors << msg272when NoticeResponse273@notice_processor.call(msg) if @notice_processor274else275# TODO276end277end278279raise errors.map{|e| e.field_values.join("\t") }.join("\n") unless errors.empty?280281result282end283284285# @param [AuthenticationSASL] msg286# @param [String] user287# @param [String,nil] password288def negotiate_sasl(msg, user, password = nil)289if msg.mechanisms.include?('SCRAM-SHA-256')290scram_sha_256 = ScramSha256.new291# Start negotiating scram, additionally wrapping in SASL and unwrapping the SASL responses292scram_sha_256.negotiate(user, password) do |state, value|293if state == :client_first294sasl_initial_response_message = SaslInitialResponseMessage.new(295mechanism: 'SCRAM-SHA-256',296value: value297)298299write_message(sasl_initial_response_message)300301sasl_continue = Message.read(@conn)302raise handle_server_error_message(sasl_continue) if sasl_continue.is_a?(ErrorResponse)303raise AuthenticationMethodMismatch, "Did not receive AuthenticationSASLContinue - instead got #{sasl_continue}" unless sasl_continue.is_a?(AuthenticationSASLContinue)304305server_first_string = sasl_continue.value306server_first_string307elsif state == :client_final308sasl_initial_response_message = SASLResponseMessage.new(309value: value310)311312write_message(sasl_initial_response_message)313314server_final = Message.read(@conn)315raise handle_server_error_message(server_final) if server_final.is_a?(ErrorResponse)316raise AuthenticationMethodMismatch, "Did not receive AuthenticationSASLFinal - instead got #{server_final}" unless server_final.is_a?(AuthenticationSASLFinal)317318server_final_string = server_final.value319server_final_string320else321raise AuthenticationMethodMismatch, "Unexpected negotiation state #{state}"322end323end324else325raise AuthenticationMethodMismatch, "unsupported SASL mechanisms #{msg.mechanisms.inspect}"326end327end328329DEFAULT_PORT = 5432330DEFAULT_HOST = 'localhost'331DEFAULT_PATH = '/tmp'332DEFAULT_URI =333if RUBY_PLATFORM.include?('win')334'tcp://' + DEFAULT_HOST + ':' + DEFAULT_PORT.to_s335else336'unix:' + File.join(DEFAULT_PATH, '.s.PGSQL.' + DEFAULT_PORT.to_s)337end338339private340341# @param [ErrorResponse] server_error_message342# @raise [RuntimeError]343def handle_server_error_message(server_error_message)344raise server_error_message.field_values.join("\t")345end346347# tcp://localhost:5432348# unix:/tmp/.s.PGSQL.5432349def establish_connection(uri, proxies, ssl = nil, ssl_opts = {})350u = URI.parse(uri)351case u.scheme352when 'tcp'353params = Rex::Socket::Parameters.from_hash(354'PeerHost' => (u.host || DEFAULT_HOST).gsub(/\[|\]/, ''),355'PeerPort' => (u.port || DEFAULT_PORT),356'proto' => 'tcp',357'Proxies' => proxies,358'SSLVersion' => ssl_opts[:ssl_version],359'SSLVerifyMode' => ssl_opts[:ssl_verify_mode],360'SSLCipher' => ssl_opts[:ssl_cipher]361)362@conn = Rex::Socket.create_param(params)363364if ssl365ssl_request_message = SSLRequest.new(80877103)366@conn.write(ssl_request_message.dump)367response = @conn.read(1)368if response == 'S'369@conn.extend(Rex::Socket::SslTcp)370@conn.initsock_with_ssl_version(params, (params.ssl_version || Rex::Socket::Ssl::DEFAULT_SSL_VERSION))371elsif response == 'N'372# Server does not support SSL373raise "SSL connection requested but server at #{u.host}:#{u.port} does not support SSL"374else375raise "Unexpected response to SSLRequest: #{response.inspect}"376end377end378when 'unix'379@conn = UNIXSocket.new(u.path)380else381raise 'unrecognized uri scheme format (must be tcp or unix)'382end383end384385# @param [Message] message386# @return [Numeric] The byte count successfully written to the currently open connection387def write_message(message)388@conn << message.dump389end390end391392end # module PostgresPR393394end395end396397398