diff --git a/README.md b/README.md index 8adb744..130ab94 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ Export to CSV: - `-e, --enumsubnet`: Enumerate and attack subnet in CIDR notation ### Attack Options -- `-b, --brute-mac`: Brute force MAC variations (4,096 combinations per phone). If no `-p` phones are given, reuses MAC prefixes discovered on a previous scan from the database (unless `--no-db`) +- `-b, --brute-mac`: Brute force MAC variations (4,096 combinations per phone). If no `-p` phones are given, reuses MAC prefixes discovered on a previous scan from the database (unless `--no-db`). With `-H` and no `-p`, prefixes come from that server's `mac_prefixes` rows plus any `SEP…` devices harvested from it by `--userenum`/`--spray`; if the server has none recorded, prefixes discovered elsewhere are retargeted at it - `--force`: Bypass cache and force re-download of all configuration files - `--userenum`: Extract usernames via CUCM User Data Services (UDS) API (paginates the full directory) and harvest the full directory records (names incl. nickname, phone/home/mobile/pager numbers, email, directory URI, MS URI, department, title, manager, UUID) into the `uds_directory` table; always writes `cucm_directory.csv` (override with `--directory-outfile`) - `--directory`: Harvest the unauthenticated CUCM corporate directory from `/cucm-uds/users` without any device probing or config downloads — requires `-H`; always writes `cucm_directory.csv` (override with `--directory-outfile`), prints a console table, and stores to `uds_directory` unless `--no-db` diff --git a/src/seeyoucm_thief/thief.py b/src/seeyoucm_thief/thief.py index 3965cc1..ff6d04a 100644 --- a/src/seeyoucm_thief/thief.py +++ b/src/seeyoucm_thief/thief.py @@ -979,6 +979,37 @@ def get_mac_prefixes_from_db(db_file='thief.db'): return rows +def get_uds_device_macs_from_db(cucm_host, db_file='thief.db'): + """ + Return MACs of SEP devices discovered on cucm_host via UDS as a list of + (full_mac, cucm_host) tuples, newest first. Lets --brute-mac run with only + -H by reusing devices found by --userenum/--spray against that server. + """ + rows = [] + try: + conn = sqlite3.connect(db_file, timeout=30.0) + cursor = conn.cursor() + try: + cursor.execute(''' + SELECT DISTINCT device_name + FROM uds_devices + WHERE cucm_host = ? + ORDER BY discovery_time DESC + ''', (cucm_host,)) + for (device_name,) in cursor.fetchall(): + match = re.match(r'SEP([0-9A-Fa-f]{12})$', device_name or '') + if match: + rows.append((match.group(1).upper(), cucm_host)) + except sqlite3.OperationalError as e: + if 'no such table' not in str(e): + raise + conn.close() + except Exception as e: + if globals().get('debug', False): + print(f'[!] get_uds_device_macs_from_db error: {e}') + return rows + + def parse_uds_devices(xml_body): """Extract SEP device names from a /cucm-uds/user/{id} XML response body.""" return re.findall(r'(SEP[0-9A-Fa-f]{12})', xml_body) @@ -2968,11 +2999,22 @@ def main(): print(f'MAC brute force mode enabled for {len(phones)} phone(s) with suffix length {brute_mac_len}\n') else: # No phones supplied: reuse MAC prefixes discovered on a previous scan. - db_prefixes = [] if no_db else get_mac_prefixes_from_db(db_file) + all_prefixes = [] if no_db else get_mac_prefixes_from_db(db_file) if CUCM_host: - db_prefixes = [(fm, c) for fm, c in db_prefixes if c == CUCM_host] + db_prefixes = [(fm, c) for fm, c in all_prefixes if c == CUCM_host] + if not no_db: + # SEP devices harvested from this server via --userenum/--spray + db_prefixes += get_uds_device_macs_from_db(CUCM_host, db_file) + if not db_prefixes and all_prefixes: + # Nothing recorded for this server yet: retarget prefixes + # learned from phones on other servers at -H. + db_prefixes = [(fm, CUCM_host) for fm, _ in all_prefixes] + print(f'[*] No MAC prefixes recorded for {CUCM_host}; reusing ' + f'{len(db_prefixes)} prefix(es) discovered elsewhere\n') + else: + db_prefixes = all_prefixes if not db_prefixes: - print('You must specify at least one phone with -p when using --brute-mac') + print('You must specify at least one phone with -p (or a CUCM server with -H) when using --brute-mac') if not no_db: print(' (and no previously discovered phones were found in the database)') quit(1) diff --git a/tests/test_brute_host_prefixes.py b/tests/test_brute_host_prefixes.py new file mode 100644 index 0000000..a2cfe7e --- /dev/null +++ b/tests/test_brute_host_prefixes.py @@ -0,0 +1,50 @@ +import sqlite3 + +import thief + + +def _db(tmp_path): + db_file = str(tmp_path / 'thief.db') + thief.init_database(db_file) + return db_file + + +def test_uds_device_macs_scoped_to_host(tmp_path): + db_file = _db(tmp_path) + thief.log_uds_device('cucm1', 'alice', 'SEPAABBCCDDEEFF', 'userenum', db_file) + thief.log_uds_device('cucm2', 'bob', 'SEP112233445566', 'userenum', db_file) + + assert thief.get_uds_device_macs_from_db('cucm1', db_file) == [ + ('AABBCCDDEEFF', 'cucm1') + ] + assert thief.get_uds_device_macs_from_db('cucm3', db_file) == [] + + +def test_uds_device_macs_ignores_non_sep_names(tmp_path): + db_file = _db(tmp_path) + thief.log_uds_device('cucm1', 'alice', 'CSFALICE', 'userenum', db_file) + assert thief.get_uds_device_macs_from_db('cucm1', db_file) == [] + + +def test_uds_device_macs_missing_table(tmp_path): + db_file = str(tmp_path / 'empty.db') + sqlite3.connect(db_file).close() + assert thief.get_uds_device_macs_from_db('cucm1', db_file) == [] + + +def test_brute_mac_accepts_host_without_phone(tmp_path): + import os + import subprocess + + db_file = str(tmp_path / 'thief.db') + thief.init_database(db_file) + thief.log_uds_device('cucm1', 'alice', 'SEPAABBCCDDEEFF', 'userenum', db_file) + + env = os.environ.copy() + env['PYTEST_CURRENT_TEST'] = '1' + result = subprocess.run( + ['python3', 'thief.py', '-b', '1', '-H', 'cucm1', '--db', db_file], + capture_output=True, text=True, env=env, timeout=300, + ) + assert 'You must specify at least one phone' not in result.stdout + assert 'MAC brute force mode enabled using 1 MAC prefix' in result.stdout