Skip to content

Commit

Permalink
Add PostgreSQL session type
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanusz-r7 committed Jan 4, 2024
1 parent 5e59389 commit 381ceae
Show file tree
Hide file tree
Showing 12 changed files with 728 additions and 3 deletions.
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ PATH
rb-readline
recog
redcarpet
reline (~> 0.4.1)
rex-arch
rex-bin_tools
rex-core
Expand Down Expand Up @@ -377,7 +378,7 @@ GEM
nokogiri
redcarpet (3.6.0)
regexp_parser (2.8.1)
reline (0.3.8)
reline (0.4.1)
io-console (~> 0.5)
require_all (3.0.0)
rex-arch (0.1.15)
Expand Down
20 changes: 18 additions & 2 deletions lib/metasploit/framework/login_scanner/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ module LoginScanner
class Postgres
include Metasploit::Framework::LoginScanner::Base

# @returns [Boolean] If a login is successful and this attribute is true - a PostgreSQL::Client instance is used as proof,
# and the socket is not immediately closed
attr_accessor :use_client_as_proof

# @!attribute dispatcher
# @return [PostgreSQL::Dispatcher::Socket]
attr_accessor :dispatcher

DEFAULT_PORT = 5432
DEFAULT_REALM = 'template1'
LIKELY_PORTS = [ DEFAULT_PORT ]
Expand Down Expand Up @@ -42,7 +50,7 @@ def attempt_login(credential)

begin
pg_conn = Msf::Db::PostgresPR::Connection.new(db_name,credential.public,credential.private,uri)
rescue RuntimeError => e
rescue ::RuntimeError => e
case e.to_s.split("\t")[1]
when "C3D000"
result_options.merge!({
Expand Down Expand Up @@ -70,8 +78,16 @@ def attempt_login(credential)
end

if pg_conn
pg_conn.close
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL

# This module no long owns the socket, return it as proof so the calling context can perform additional operations
# Additionally assign values to nil to avoid closing the socket etc automatically
if use_client_as_proof
result_options[:proof] = pg_conn
pg_conn = nil
else
pg_conn.close
end
else
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
end
Expand Down
145 changes: 145 additions & 0 deletions lib/msf/base/sessions/postgresql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# -*- coding: binary -*-

require 'rex/post/postgresql'

class Msf::Sessions::PostgreSQL
#
# This interface supports basic interaction.
#
include Msf::Session::Basic
include Msf::Sessions::Scriptable

# @return [Rex::Post::PostgreSQL::Ui::Console] The interactive console
attr_accessor :console
# @return [PostgreSQL::Client]
attr_accessor :client
attr_accessor :platform, :arch

# @param[Rex::IO::Stream] rstream
# @param [Hash] opts
# @param opts [PostgreSQL::Client] :client
def initialize(rstream, opts = {})
@client = opts.fetch(:client)
@console = ::Rex::Post::PostgreSQL::Ui::Console.new(self)
super(rstream, opts)
end

def bootstrap(datastore = {}, handler = nil)
# this won't work after the rstream is initialized, so do it first
# @platform = 'windows' # Metasploit::Framework::Ssh::Platform.get_platform(ssh_connection)
# super

session = self
session.init_ui(user_input, user_output)

@info = "PostgreSQL #{datastore['USERNAME']} @ #{@peer_info}"
end

def process_autoruns(datastore)
['InitialAutoRunScript', 'AutoRunScript'].each do |key|
next if datastore[key].nil? || datastore[key].empty?

args = Shellwords.shellwords(datastore[key])
print_status("Session ID #{session.sid} (#{session.tunnel_to_s}) processing #{key} '#{datastore[key]}'")
session.execute_script(args.shift, *args)
end
end

def type
self.class.type
end

#
# @return [String] The type of the session
#
def self.type
'PostgreSQL'
end

#
# @return [Boolean] Can the session clean up after itself
def self.can_cleanup_files
false
end

#
# @return [String] The session description
#
def desc
'PostgreSQL'
end

def address
return @address if @address

@address, @port = @client.conn.peerinfo.split(':')
@address
end

def port
return @port if @port

@address, @port = @client.conn.peerinfo.split(':')
@port
end

##
# :category: Msf::Session::Interactive implementors
#
# Initializes the console's I/O handles.
#
def init_ui(input, output)
super(input, output)

console.init_ui(input, output)
console.set_log_source(self.log_source)
end

##
# :category: Msf::Session::Interactive implementors
#
# Resets the console's I/O handles.
#
def reset_ui
console.unset_log_source
console.reset_ui
end

def exit
console.stop
end

protected

##
# :category: Msf::Session::Interactive implementors
#
# Override the basic session interaction to use shell_read and
# shell_write instead of operating on rstream directly.
def _interact
framework.events.on_session_interact(self)
framework.history_manager.with_context(name: type.to_sym) { _interact_stream }
end

##
# :category: Msf::Session::Interactive implementors
#
def _interact_stream
framework.events.on_session_interact(self)

console.framework = framework
# if framework.datastore['MeterpreterPrompt']
# console.update_prompt(framework.datastore['MeterpreterPrompt'])
# end
# Call the console interaction of the smb client and
# pass it a block that returns whether or not we should still be
# interacting. This will allow the shell to abort if interaction is
# canceled.
console.interact { interacting != true }
console.framework = nil

# If the stop flag has been set, then that means the user exited. Raise
# the EOFError so we can drop this handle like a bad habit.
raise ::EOFError if (console.stopped? == true)
end
end
1 change: 1 addition & 0 deletions lib/msf_autoload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def custom_inflections
'cli' => 'CLI',
'sqlitei' => 'SQLitei',
'mysqli' => 'MySQLi',
'postgresql' => 'PostgreSQL',
'postgresqli' => 'PostgreSQLi',
'ssh' => 'SSH',
'winrm' => 'WinRM',
Expand Down
3 changes: 3 additions & 0 deletions lib/rex/post/postgresql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: binary -*-

require 'rex/post/postgresql/ui'
3 changes: 3 additions & 0 deletions lib/rex/post/postgresql/ui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: binary -*-

require 'rex/post/postgresql/ui/console'
135 changes: 135 additions & 0 deletions lib/rex/post/postgresql/ui/console.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# -*- coding: binary -*-

module Rex
module Post
module PostgreSQL
module Ui
###
#
# This class provides a shell driven interface to the PostgreSQL client API.
#
###
class Console
include Rex::Ui::Text::DispatcherShell

# Dispatchers
require 'rex/post/postgresql/ui/console/command_dispatcher'
require 'rex/post/postgresql/ui/console/command_dispatcher/core'
require 'rex/post/postgresql/ui/console/command_dispatcher/client'

#
# Initialize the PostgreSQL console.
#
# @param [Msf::Sessions::PostgreSQL] session
def initialize(session)
# The postgresql client context
self.session = session
self.client = session.client
prompt = "%undPostgreSQL @ #{self.client.conn.remote_address.ip_address}:#{self.client.conn.remote_address.ip_port}%clr"
history_manager = self.session.framework&.history_manager&.with_context(name: :postgresql)
super(prompt, '>', history_manager, self.session&.framework, :postgresql)

# Queued commands array
self.commands = []

# Point the input/output handles elsewhere
reset_ui

enstack_dispatcher(::Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Core)
enstack_dispatcher(::Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Client)

# Set up logging to whatever logsink 'core' is using
if ! $dispatcher['postgresql']
$dispatcher['postgresql'] = $dispatcher['core']
end
end

#
# Called when someone wants to interact with the postgresql client. It's
# assumed that init_ui has been called prior.
#
def interact(&block)
# Run queued commands
commands.delete_if do |ent|
run_single(ent)
true
end

# Run the interactive loop
run do |line|
# Run the command
run_single(line)

# If a block was supplied, call it, otherwise return false
if block
block.call
else
false
end
end
end

#
# Queues a command to be run when the interactive loop is entered.
#
def queue_cmd(cmd)
self.commands << cmd
end

#
# Runs the specified command wrapper in something to catch meterpreter
# exceptions.
#
def run_command(dispatcher, method, arguments)
begin
super
rescue ::Timeout::Error
log_error('Operation timed out.')
rescue ::Rex::InvalidDestination => e
log_error(e.message)
rescue ::Errno::EPIPE, ::OpenSSL::SSL::SSLError, ::IOError
self.session.kill
rescue ::StandardError => e
log_error("Error running command #{method}: #{e.class} #{e}")
elog(e)
end
end

#
# Logs that an error occurred and persists the callstack.
#
def log_error(msg)
print_error(msg)

elog(msg, 'postgresql')

dlog("Call stack:\n#{$@.join("\n")}", 'postgresql')
end

# @return [Msf::Sessions::PostgreSQL]
attr_reader :session

# @return [PostgreSQL::Client]
attr_reader :client # :nodoc:

# TODO Should this belong elsewhere - it's required for prompt details
# @return [String]
attr_accessor :cwd

def format_prompt(val)
# TODO: How can the currently selected DB/Table/Module impact the prompt?
super
end

protected

attr_writer :session # :nodoc:
attr_writer :client # :nodoc:
attr_accessor :commands # :nodoc:

end

end
end
end
end
Loading

0 comments on commit 381ceae

Please sign in to comment.