CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/postgres/postgres-pr/connection.rb
Views: 1904
1
# -*- coding: binary -*-
2
#
3
# Author:: Michael Neumann
4
# Copyright:: (c) 2005 by Michael Neumann
5
# License:: Same as Ruby's or BSD
6
#
7
8
require 'postgres_msf'
9
require 'postgres/postgres-pr/message'
10
require 'postgres/postgres-pr/version'
11
require 'postgres/postgres-pr/scram_sha_256'
12
require 'uri'
13
require 'rex/socket'
14
15
# Namespace for Metasploit branch.
16
module Msf
17
module Db
18
19
module PostgresPR
20
21
PROTO_VERSION = 3 << 16 #196608
22
23
class AuthenticationMethodMismatch < StandardError
24
end
25
26
class Connection
27
28
# Allow easy access to these instance variables
29
attr_reader :conn, :params, :transaction_status
30
31
# A block which is called with the NoticeResponse object as parameter.
32
attr_accessor :notice_processor
33
34
#
35
# Returns one of the following statuses:
36
#
37
# PQTRANS_IDLE = 0 (connection idle)
38
# PQTRANS_INTRANS = 2 (idle, within transaction block)
39
# PQTRANS_INERROR = 3 (idle, within failed transaction)
40
# PQTRANS_UNKNOWN = 4 (cannot determine status)
41
#
42
# Not yet implemented is:
43
#
44
# PQTRANS_ACTIVE = 1 (command in progress)
45
#
46
def transaction_status
47
case @transaction_status
48
when ?I
49
0
50
when ?T
51
2
52
when ?E
53
3
54
else
55
4
56
end
57
end
58
59
def initialize(database, user, password=nil, uri = nil, proxies = nil)
60
uri ||= DEFAULT_URI
61
62
@transaction_status = nil
63
@params = { 'username' => user, 'database' => database }
64
establish_connection(uri, proxies)
65
66
# Check if the password supplied is a Postgres-style md5 hash
67
md5_hash_match = password.match(/^md5([a-f0-9]{32})$/)
68
69
write_message(StartupMessage.new(PROTO_VERSION, 'user' => user, 'database' => database))
70
71
loop do
72
msg = Message.read(@conn)
73
74
case msg
75
when AuthentificationClearTextPassword
76
raise ArgumentError, "no password specified" if password.nil?
77
raise AuthenticationMethodMismatch, "Server expected clear text password auth" if md5_hash_match
78
write_message(PasswordMessage.new(password))
79
when AuthentificationCryptPassword
80
raise ArgumentError, "no password specified" if password.nil?
81
raise AuthenticationMethodMismatch, "Server expected crypt password auth" if md5_hash_match
82
write_message(PasswordMessage.new(password.crypt(msg.salt)))
83
when AuthentificationMD5Password
84
raise ArgumentError, "no password specified" if password.nil?
85
require 'digest/md5'
86
87
if md5_hash_match
88
m = md5_hash_match[1]
89
else
90
m = Digest::MD5.hexdigest(password + user)
91
end
92
m = Digest::MD5.hexdigest(m + msg.salt)
93
m = 'md5' + m
94
95
write_message(PasswordMessage.new(m))
96
97
when AuthenticationSASL
98
negotiate_sasl(msg, user, password)
99
when UnknownAuthType
100
raise "unknown auth type '#{msg.auth_type}' with buffer content:\n#{Rex::Text.to_hex_dump(msg.buffer.content)}"
101
102
when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
103
raise "unsupported authentication"
104
105
when AuthentificationOk
106
when ErrorResponse
107
handle_server_error_message(msg)
108
when NoticeResponse
109
@notice_processor.call(msg) if @notice_processor
110
when ParameterStatus
111
@params[msg.key] = msg.value
112
when BackendKeyData
113
# TODO
114
#p msg
115
when ReadyForQuery
116
@transaction_status = msg.backend_transaction_status_indicator
117
break
118
else
119
raise "unhandled message type"
120
end
121
end
122
end
123
124
def peerhost
125
@conn.peerhost
126
end
127
128
def peerport
129
@conn.peerport
130
end
131
132
def peerinfo
133
"#{peerhost}:#{peerport}"
134
end
135
136
def current_database
137
@params['database']
138
end
139
140
# List of supported PostgreSQL platforms & architectures:
141
# https://postgrespro.com/docs/postgresql/16/supported-platforms
142
def map_compile_os_to_platform(compile_os)
143
return Msf::Platform::Unknown.realname if compile_os.blank?
144
145
compile_os = compile_os.downcase.encode(::Encoding::BINARY)
146
147
if compile_os.match?('linux')
148
platform = Msf::Platform::Linux
149
elsif compile_os.match?(/(darwin|mac|osx)/)
150
platform = Msf::Platform::OSX
151
elsif compile_os.match?('win')
152
platform = Msf::Platform::Windows
153
elsif compile_os.match?('free')
154
platform = Msf::Platform::FreeBSD
155
elsif compile_os.match?('net')
156
platform = Msf::Platform::NetBSD
157
elsif compile_os.match?('open')
158
platform = Msf::Platform::OpenBSD
159
elsif compile_os.match?('solaris')
160
platform = Msf::Platform::Solaris
161
elsif compile_os.match?('aix')
162
platform = Msf::Platform::AIX
163
elsif compile_os.match?('hpux')
164
platform = Msf::Platform::HPUX
165
elsif compile_os.match?('irix')
166
platform = Msf::Platform::Irix
167
else
168
# Return the query result if the value can't be mapped
169
return compile_os
170
end
171
172
platform.realname
173
end
174
175
# List of supported PostgreSQL platforms & architectures:
176
# https://postgrespro.com/docs/postgresql/16/supported-platforms
177
def map_compile_arch_to_architecture(compile_arch)
178
return '' if compile_arch.blank?
179
180
compile_arch = compile_arch.downcase.encode(::Encoding::BINARY)
181
182
if compile_arch.match?('sparc')
183
if compile_arch.include?('64')
184
arch = ARCH_SPARC64
185
else
186
arch = ARCH_SPARC
187
end
188
elsif compile_arch.include?('mips')
189
arch = ARCH_MIPS
190
elsif compile_arch.include?('ppc')
191
arch = ARCH_PPC
192
elsif compile_arch.match?('arm')
193
if compile_arch.match?('64')
194
arch = ARCH_AARCH64
195
elsif compile_arch.match?('arm')
196
arch = ARCH_ARMLE
197
end
198
elsif compile_arch.match?('64')
199
arch = ARCH_X86_64
200
elsif compile_arch.match?('86') || compile_arch.match?('i686')
201
arch = ARCH_X86
202
else
203
# Return the query result if the value can't be mapped
204
arch = compile_arch
205
end
206
207
arch
208
end
209
210
# @return [Hash] Detect the platform and architecture of the PostgreSQL server:
211
# * :arch [String] The server architecture.
212
# * :platform [String] The server platform.
213
def detect_platform_and_arch
214
result = {}
215
216
query_result = query('select version()').rows[0][0]
217
match_platform_and_arch = query_result.match(/on (?<architecture>\w+)-\w+-(?<platform>\w+)/)
218
219
if match_platform_and_arch.nil?
220
arch = platform = query_result
221
else
222
arch = match_platform_and_arch[:architecture]
223
platform = match_platform_and_arch[:platform]
224
end
225
226
result[:arch] = map_compile_arch_to_architecture(arch)
227
result[:platform] = map_compile_os_to_platform(platform)
228
229
result
230
end
231
232
def close
233
raise "connection already closed" if @conn.nil?
234
@conn.shutdown
235
@conn = nil
236
end
237
238
class Result
239
attr_accessor :rows, :fields, :cmd_tag
240
def initialize(rows=[], fields=[])
241
@rows, @fields = rows, fields
242
end
243
end
244
245
def query(sql)
246
write_message(Query.new(sql))
247
248
result = Result.new
249
errors = []
250
251
loop do
252
msg = Message.read(@conn)
253
case msg
254
when DataRow
255
result.rows << msg.columns
256
when CommandComplete
257
result.cmd_tag = msg.cmd_tag
258
when ReadyForQuery
259
@transaction_status = msg.backend_transaction_status_indicator
260
break
261
when RowDescription
262
result.fields = msg.fields
263
when CopyInResponse
264
when CopyOutResponse
265
when EmptyQueryResponse
266
when ErrorResponse
267
# TODO
268
errors << msg
269
when NoticeResponse
270
@notice_processor.call(msg) if @notice_processor
271
else
272
# TODO
273
end
274
end
275
276
raise errors.map{|e| e.field_values.join("\t") }.join("\n") unless errors.empty?
277
278
result
279
end
280
281
282
# @param [AuthenticationSASL] msg
283
# @param [String] user
284
# @param [String,nil] password
285
def negotiate_sasl(msg, user, password = nil)
286
if msg.mechanisms.include?('SCRAM-SHA-256')
287
scram_sha_256 = ScramSha256.new
288
# Start negotiating scram, additionally wrapping in SASL and unwrapping the SASL responses
289
scram_sha_256.negotiate(user, password) do |state, value|
290
if state == :client_first
291
sasl_initial_response_message = SaslInitialResponseMessage.new(
292
mechanism: 'SCRAM-SHA-256',
293
value: value
294
)
295
296
write_message(sasl_initial_response_message)
297
298
sasl_continue = Message.read(@conn)
299
raise handle_server_error_message(sasl_continue) if sasl_continue.is_a?(ErrorResponse)
300
raise AuthenticationMethodMismatch, "Did not receive AuthenticationSASLContinue - instead got #{sasl_continue}" unless sasl_continue.is_a?(AuthenticationSASLContinue)
301
302
server_first_string = sasl_continue.value
303
server_first_string
304
elsif state == :client_final
305
sasl_initial_response_message = SASLResponseMessage.new(
306
value: value
307
)
308
309
write_message(sasl_initial_response_message)
310
311
server_final = Message.read(@conn)
312
raise handle_server_error_message(server_final) if server_final.is_a?(ErrorResponse)
313
raise AuthenticationMethodMismatch, "Did not receive AuthenticationSASLFinal - instead got #{server_final}" unless server_final.is_a?(AuthenticationSASLFinal)
314
315
server_final_string = server_final.value
316
server_final_string
317
else
318
raise AuthenticationMethodMismatch, "Unexpected negotiation state #{state}"
319
end
320
end
321
else
322
raise AuthenticationMethodMismatch, "unsupported SASL mechanisms #{msg.mechanisms.inspect}"
323
end
324
end
325
326
DEFAULT_PORT = 5432
327
DEFAULT_HOST = 'localhost'
328
DEFAULT_PATH = '/tmp'
329
DEFAULT_URI =
330
if RUBY_PLATFORM.include?('win')
331
'tcp://' + DEFAULT_HOST + ':' + DEFAULT_PORT.to_s
332
else
333
'unix:' + File.join(DEFAULT_PATH, '.s.PGSQL.' + DEFAULT_PORT.to_s)
334
end
335
336
private
337
338
# @param [ErrorResponse] server_error_message
339
# @raise [RuntimeError]
340
def handle_server_error_message(server_error_message)
341
raise server_error_message.field_values.join("\t")
342
end
343
344
# tcp://localhost:5432
345
# unix:/tmp/.s.PGSQL.5432
346
def establish_connection(uri, proxies)
347
u = URI.parse(uri)
348
case u.scheme
349
when 'tcp'
350
@conn = Rex::Socket.create(
351
'PeerHost' => (u.host || DEFAULT_HOST).gsub(/[\[\]]/, ''), # Strip any brackets off (IPv6)
352
'PeerPort' => (u.port || DEFAULT_PORT),
353
'proto' => 'tcp',
354
'Proxies' => proxies
355
)
356
when 'unix'
357
@conn = UNIXSocket.new(u.path)
358
else
359
raise 'unrecognized uri scheme format (must be tcp or unix)'
360
end
361
end
362
363
# @param [Message] message
364
# @return [Numeric] The byte count successfully written to the currently open connection
365
def write_message(message)
366
@conn << message.dump
367
end
368
end
369
370
end # module PostgresPR
371
372
end
373
end
374
375