Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
48 changes: 45 additions & 3 deletions src/seeyoucm_thief/thief.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'<device>(SEP[0-9A-Fa-f]{12})</device>', xml_body)
Expand Down Expand Up @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_brute_host_prefixes.py
Original file line number Diff line number Diff line change
@@ -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
Loading