Skip to content

Commit 3d1d49b

Browse files
authored
Merge pull request #20517 from cgranleese-r7/adds-postgres-ssl-support
Adds SSL support to the postgres_login module
2 parents 070bf7f + 40f6e2c commit 3d1d49b

File tree

7 files changed

+159
-28
lines changed

7 files changed

+159
-28
lines changed

lib/metasploit/framework/login_scanner/postgres.rb

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ module LoginScanner
1111
class Postgres
1212
include Metasploit::Framework::LoginScanner::Base
1313

14+
# @!attribute ssl
15+
# @return [Boolean] Whether the connection should use SSL
16+
attr_accessor :ssl
17+
# @!attribute ssl_version
18+
# @return [String] The version of SSL to implement
19+
attr_accessor :ssl_version
20+
# @!attribute ssl_verify_mode
21+
# @return [String] the SSL certification verification mechanism
22+
attr_accessor :ssl_verify_mode
23+
# @!attribute ssl_cipher
24+
# @return [String] The SSL cipher to use for the context
25+
attr_accessor :ssl_cipher
26+
# @!attribute max_send_size
27+
# @return [Integer] The max size of the data to encapsulate in a single packet
28+
attr_accessor :max_send_size
29+
# @!attribute send_delay
30+
# @return [Integer] The delay between sending packets
31+
attr_accessor :send_delay
32+
1433
# @returns [Boolean] If a login is successful and this attribute is true - a Msf::Db::PostgresPR::Connection instance is used as proof,
1534
# and the socket is not immediately closed
1635
attr_accessor :use_client_as_proof
@@ -45,7 +64,20 @@ def attempt_login(credential)
4564
pg_conn = nil
4665

4766
begin
48-
pg_conn = Msf::Db::PostgresPR::Connection.new(db_name,credential.public,credential.private,uri,proxies)
67+
ssl_opts = {}
68+
ssl_opts[:ssl_version] = ssl_version if ssl_version
69+
ssl_opts[:ssl_verify_mode] = ssl_verify_mode if ssl_verify_mode
70+
ssl_opts[:ssl_cipher] = ssl_cipher if ssl_cipher
71+
72+
pg_conn = Msf::Db::PostgresPR::Connection.new(
73+
db_name,
74+
credential.public,
75+
credential.private,
76+
uri,
77+
proxies,
78+
ssl,
79+
ssl_opts
80+
)
4981
rescue ::RuntimeError => e
5082
case e.to_s.split("\t")[1]
5183
when "C3D000"
@@ -90,13 +122,15 @@ def attempt_login(credential)
90122

91123
::Metasploit::Framework::LoginScanner::Result.new(result_options)
92124
end
93-
end
94125

95-
def set_sane_defaults
96-
self.connection_timeout ||= 30
97-
self.port ||= DEFAULT_PORT
98-
end
126+
def set_sane_defaults
127+
self.connection_timeout ||= 30
128+
self.port ||= DEFAULT_PORT
129+
self.max_send_size ||= 0
130+
self.send_delay ||= 0
131+
end
99132

133+
end
100134
end
101135
end
102136
end

lib/msf/core/exploit/remote/postgres.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ module Exploit::Remote::Postgres
1212

1313
require 'postgres_msf'
1414
require 'base64'
15+
require 'metasploit/framework/tcp/client'
1516
include Msf::Db::PostgresPR
17+
include Exploit::Remote::Tcp
1618

1719
# @!attribute [rw] postgres_conn
1820
# @return [::Msf::Db::PostgresPR::Connection]

lib/postgres/postgres-pr/connection.rb

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ def transaction_status
5656
end
5757
end
5858

59-
def initialize(database, user, password=nil, uri = nil, proxies = nil)
59+
def initialize(database, user, password=nil, uri = nil, proxies = nil, ssl = nil, ssl_opts = {})
6060
uri ||= DEFAULT_URI
6161

6262
@transaction_status = nil
6363
@params = { 'username' => user, 'database' => database }
64-
establish_connection(uri, proxies)
64+
establish_connection(uri, proxies, ssl, ssl_opts)
6565

6666
# Check if the password supplied is a Postgres-style md5 hash
6767
md5_hash_match = password.match(/^md5([a-f0-9]{32})$/)
@@ -231,7 +231,11 @@ def detect_platform_and_arch
231231

232232
def close
233233
raise "connection already closed" if @conn.nil?
234-
@conn.shutdown
234+
if @conn.respond_to?(:shutdown)
235+
@conn.shutdown
236+
elsif @conn.respond_to?(:close)
237+
@conn.close
238+
end
235239
@conn = nil
236240
end
237241

@@ -343,16 +347,35 @@ def handle_server_error_message(server_error_message)
343347

344348
# tcp://localhost:5432
345349
# unix:/tmp/.s.PGSQL.5432
346-
def establish_connection(uri, proxies)
350+
def establish_connection(uri, proxies, ssl = nil, ssl_opts = {})
347351
u = URI.parse(uri)
348352
case u.scheme
349353
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-
)
354+
params = Rex::Socket::Parameters.from_hash(
355+
'PeerHost' => (u.host || DEFAULT_HOST).gsub(/\[|\]/, ''),
356+
'PeerPort' => (u.port || DEFAULT_PORT),
357+
'proto' => 'tcp',
358+
'Proxies' => proxies,
359+
'SSLVersion' => ssl_opts[:ssl_version],
360+
'SSLVerifyMode' => ssl_opts[:ssl_verify_mode],
361+
'SSLCipher' => ssl_opts[:ssl_cipher]
362+
)
363+
@conn = Rex::Socket.create_param(params)
364+
365+
if ssl
366+
ssl_request_message = SSLRequest.new(80877103)
367+
@conn.write(ssl_request_message.dump)
368+
response = @conn.read(1)
369+
if response == 'S'
370+
@conn.extend(Rex::Socket::SslTcp)
371+
@conn.initsock_with_ssl_version(params, (params.ssl_version || Rex::Socket::Ssl::DEFAULT_SSL_VERSION))
372+
elsif response == 'N'
373+
# Server does not support SSL
374+
raise "SSL connection requested but server at #{u.host}:#{u.port} does not support SSL"
375+
else
376+
raise "Unexpected response to SSLRequest: #{response.inspect}"
377+
end
378+
end
356379
when 'unix'
357380
@conn = UNIXSocket.new(u.path)
358381
else

modules/auxiliary/scanner/postgres/postgres_login.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,14 @@ def run_host(ip)
113113
stop_on_success: datastore['STOP_ON_SUCCESS'],
114114
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
115115
connection_timeout: 30,
116+
max_send_size: (datastore['TCP::max_send_size']),
117+
send_delay: (datastore['TCP::send_delay']),
116118
framework: framework,
117119
framework_module: self,
120+
ssl: datastore['SSL'],
121+
ssl_version: datastore['SSLVersion'],
122+
ssl_verify_mode: datastore['SSLVerifyMode'],
123+
ssl_cipher: datastore['SSLCipher'],
118124
use_client_as_proof: create_session?
119125
)
120126
)

spec/lib/metasploit/framework/login_scanner/postgres_spec.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
let(:public) { 'root' }
66
let(:private) { 'toor' }
77
let(:realm) { 'template1' }
8+
let(:host) { '127.0.0.1' }
89

910
let(:full_cred) {
1011
Metasploit::Framework::Credential.new(
@@ -23,7 +24,7 @@
2324
)
2425
}
2526

26-
subject(:login_scanner) { described_class.new }
27+
subject(:login_scanner) { described_class.new(host: host) }
2728

2829
it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true
2930

@@ -40,7 +41,7 @@
4041

4142
context 'when there is no realm on the credential' do
4243
it 'uses template1 as the default realm' do
43-
expect(Msf::Db::PostgresPR::Connection).to receive(:new).with('template1', 'root', 'toor', 'tcp://:', nil)
44+
expect(Msf::Db::PostgresPR::Connection).to receive(:new).with('template1', 'root', 'toor', 'tcp://127.0.0.1:5432', nil, nil, {})
4445
login_scanner.attempt_login(cred_no_realm)
4546
end
4647
end

spec/lib/msf/core/rhosts_walker_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -972,9 +972,9 @@ def create_tempfile(content)
972972
it 'enumerates postgres schemes' do
973973
postgres_mod.datastore['RHOSTS'] = 'postgres://postgres:@example.com "postgres://user:a b [email protected]/" "postgres://user:a b [email protected]:9001/database_name"'
974974
expected = [
975-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 5432, 'USERNAME' => 'postgres', 'PASSWORD' => '', 'DATABASE' => 'template1' },
976-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 5432, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'template1' },
977-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 9001, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'database_name' }
975+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 5432, 'SSL' => false, 'USERNAME' => 'postgres', 'PASSWORD' => '', 'DATABASE' => 'template1' },
976+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 5432,'SSL' => false, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'template1' },
977+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => '192.0.2.2', 'RPORT' => 9001, 'SSL' => false, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'database_name' }
978978
]
979979
expect(each_error_for(postgres_mod)).to be_empty
980980
expect(each_host_for(postgres_mod)).to have_datastore_values(expected)
@@ -984,9 +984,9 @@ def create_tempfile(content)
984984
postgres_mod.datastore['RHOSTS'] = 'postgres://postgres:@example.com "postgres://user:a b [email protected]/" "postgres://user:a b [email protected]:9001/database_name"'
985985
postgres_mod.datastore['PROXIES'] = 'socks5h:198.51.100.1:1080'
986986
expected = [
987-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'USERNAME' => 'postgres', 'PASSWORD' => '', 'DATABASE' => 'template1' },
988-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'template1' },
989-
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 9001, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'database_name' }
987+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'SSL' => false, 'USERNAME' => 'postgres', 'PASSWORD' => '', 'DATABASE' => 'template1' },
988+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 5432, 'SSL' => false, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'template1' },
989+
{ 'RHOSTNAME' => 'example.com', 'RHOSTS' => 'example.com', 'RPORT' => 9001, 'SSL' => false, 'USERNAME' => 'user', 'PASSWORD' => 'a b c', 'DATABASE' => 'database_name' }
990990
]
991991
expect(each_error_for(postgres_mod)).to be_empty
992992
expect(each_host_for(postgres_mod)).to have_datastore_values(expected)

spec/lib/rex/proto/postgresql/client_spec.rb

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,81 @@
1111
let(:socket) { double(Rex::Socket, peerhost: host, peerport: port) }
1212
let(:message) { Msf::Db::PostgresPR::ReadyForQuery.new('') }
1313

14-
subject do
14+
before do
1515
allow(socket).to receive(:<<)
16+
allow(socket).to receive(:write)
17+
allow(socket).to receive(:read).and_return('S')
18+
allow(socket).to receive(:extend)
19+
allow(socket).to receive(:initsock_with_ssl_version)
1620
allow(Msf::Db::PostgresPR::Message).to receive(:read).and_return(message)
17-
allow(Rex::Socket).to receive(:create).and_return(socket)
18-
client = described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}")
19-
client
21+
allow(Rex::Socket).to receive(:create_param).and_return(socket)
22+
end
23+
24+
subject do
25+
described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}")
2026
end
2127

2228
it_behaves_like 'session compatible SQL client'
2329

30+
describe 'SSL connection' do
31+
let(:ssl_request_message) { instance_double(Msf::Db::PostgresPR::SSLRequest) }
32+
let(:ssl_opts) { { ssl_version: 'TLS1.2', ssl_verify_mode: 'peer', ssl_cipher: 'AES256' } }
33+
34+
before do
35+
allow(Msf::Db::PostgresPR::SSLRequest).to receive(:new).with(80877103).and_return(ssl_request_message)
36+
allow(ssl_request_message).to receive(:dump).and_return('ssl_request_data')
37+
end
38+
39+
context 'when SSL is enabled and server supports SSL' do
40+
it 'successfully establishes SSL connection' do
41+
allow(socket).to receive(:read).with(1).and_return('S')
42+
43+
expect(socket).to receive(:write).with('ssl_request_data')
44+
expect(socket).to receive(:extend).with(Rex::Socket::SslTcp)
45+
expect(socket).to receive(:initsock_with_ssl_version)
46+
47+
client = described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}", nil, true, ssl_opts)
48+
expect(client).to be_a(Msf::Db::PostgresPR::Connection)
49+
end
50+
end
51+
52+
context 'when SSL is enabled but server does not support SSL' do
53+
it 'raises an error when server responds with N' do
54+
allow(socket).to receive(:read).with(1).and_return('N')
55+
56+
expect(socket).to receive(:write).with('ssl_request_data')
57+
expect(socket).not_to receive(:extend)
58+
59+
expect {
60+
described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}", nil, true, ssl_opts)
61+
}.to raise_error("SSL connection requested but server at #{host}:#{port} does not support SSL")
62+
end
63+
end
64+
65+
context 'when SSL is enabled but server responds unexpectedly' do
66+
it 'raises an error for unexpected SSL response' do
67+
allow(socket).to receive(:read).with(1).and_return('X')
68+
69+
expect(socket).to receive(:write).with('ssl_request_data')
70+
expect(socket).not_to receive(:extend)
71+
72+
expect {
73+
described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}", nil, true, ssl_opts)
74+
}.to raise_error('Unexpected response to SSLRequest: "X"')
75+
end
76+
end
77+
78+
context 'when SSL is disabled' do
79+
it 'does not attempt SSL handshake' do
80+
expect(socket).not_to receive(:write).with('ssl_request_data')
81+
expect(socket).not_to receive(:extend).with(Rex::Socket::SslTcp)
82+
83+
client = described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}", nil, false, ssl_opts)
84+
expect(client).to be_a(Msf::Db::PostgresPR::Connection)
85+
end
86+
end
87+
end
88+
2489
describe '#map_compile_os_to_platform' do
2590
[
2691
{ info: 'linux', expected: Msf::Platform::Linux.realname },

0 commit comments

Comments
 (0)