mirror of
https://github.com/itsdave-de/msp.git
synced 2026-08-13 23:20:05 -03:00
fix(ldap): MD4-Fallback für NTLM-Auth gegen Active Directory
Das von uv gebundelte Python (python-build-standalone) bringt OpenSSL
ohne legacy-Provider mit, weshalb hashlib.new('md4') fehlschlägt. ldap3
braucht MD4 aber für NTLM-Auth gegen AD - sichtbar im Workflow-Schritt
'5. AD Computer-Daten speichern' der MSP Documentation als Fehler
'unsupported hash type MD4'.
- Monkey-Patch in tactical-rmm.py: hashlib.new('md4') fällt auf
pycryptodome's MD4 zurück
- pycryptodome zu requirements.txt hinzugefügt
- Toten Prototyp active_directory_tools.py entfernt (nicht importiert,
Logik längst in tactical-rmm.py:fetch_and_store_ad_user_data übernommen)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
import datetime
|
||||
import uuid
|
||||
import pandas as pd
|
||||
from ldap3 import Server, Connection, ALL, NTLM
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configuration from environment variables
|
||||
server_name = os.getenv('LDAP_SERVER')
|
||||
domain_name = os.getenv('LDAP_DOMAIN')
|
||||
username = os.getenv('LDAP_USERNAME') # Format: DOMAIN\username
|
||||
password = os.getenv('LDAP_PASSWORD')
|
||||
|
||||
# Connect to the server
|
||||
server = Server(server_name, get_info=ALL)
|
||||
conn = Connection(server, user=username, password=password, authentication=NTLM)
|
||||
|
||||
# Bind to the server
|
||||
if not conn.bind():
|
||||
print('Error in binding to the server')
|
||||
exit()
|
||||
|
||||
# Get server info for automatic domain detection
|
||||
server_info = server.info
|
||||
|
||||
# Determine search base - use domain root for recursive search
|
||||
search_base = None
|
||||
|
||||
# Try environment variable first
|
||||
env_search_base = os.getenv('LDAP_SEARCH_BASE')
|
||||
if env_search_base:
|
||||
search_base = env_search_base
|
||||
# Try to construct from domain name
|
||||
elif domain_name:
|
||||
domain_parts = domain_name.split('.')
|
||||
search_base = ','.join([f'DC={part}' for part in domain_parts])
|
||||
# Try to get from server naming contexts
|
||||
elif hasattr(server_info, 'naming_contexts') and server_info.naming_contexts:
|
||||
for nc in server_info.naming_contexts:
|
||||
nc_str = str(nc)
|
||||
if nc_str.startswith('DC=') and 'CN=Configuration' not in nc_str and 'CN=Schema' not in nc_str:
|
||||
search_base = nc_str
|
||||
break
|
||||
|
||||
# Fallback
|
||||
if not search_base:
|
||||
search_base = 'DC=corp,DC=local'
|
||||
|
||||
print(f"Durchsuche rekursiv: {search_base}")
|
||||
|
||||
# Simple user filter (exclude computer accounts)
|
||||
search_filter = '(&(objectClass=user)(!(sAMAccountName=*$)))'
|
||||
|
||||
attributes = [
|
||||
'sAMAccountName', 'lastLogon', 'lastLogonTimestamp', 'objectGUID', 'userAccountControl',
|
||||
'givenName', 'sn', 'cn', 'displayName', 'distinguishedName',
|
||||
'userPrincipalName', 'proxyAddresses', 'mail', 'lockoutTime'
|
||||
]
|
||||
|
||||
# Perform recursive search
|
||||
print("Starte rekursive Suche...")
|
||||
try:
|
||||
from ldap3 import SUBTREE
|
||||
success = conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes)
|
||||
|
||||
if success and conn.entries:
|
||||
print(f"Benutzer-Accounts gefunden: {len(conn.entries)}")
|
||||
|
||||
# Remove duplicates based on distinguishedName
|
||||
seen_dns = set()
|
||||
unique_entries = []
|
||||
for entry in conn.entries:
|
||||
dn = str(entry.distinguishedName)
|
||||
if dn not in seen_dns:
|
||||
seen_dns.add(dn)
|
||||
unique_entries.append(entry)
|
||||
|
||||
print(f"Nach Duplikat-Entfernung: {len(unique_entries)} eindeutige Benutzer-Accounts")
|
||||
else:
|
||||
unique_entries = []
|
||||
print("Keine Benutzer-Accounts gefunden")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler bei der Suche: {str(e)}")
|
||||
unique_entries = []
|
||||
|
||||
# Function to convert Windows File Time to human-readable format
|
||||
def convert_filetime(filetime):
|
||||
if filetime and isinstance(filetime, int):
|
||||
return (datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=filetime / 10)).replace(tzinfo=None)
|
||||
elif filetime and isinstance(filetime, datetime.datetime):
|
||||
return filetime.replace(tzinfo=None)
|
||||
else:
|
||||
return None
|
||||
|
||||
# Function to check if the user is disabled
|
||||
def is_user_disabled(user_account_control):
|
||||
return bool(user_account_control & 0x0002)
|
||||
|
||||
# Function to check if the user is locked
|
||||
def is_user_locked(lockout_time):
|
||||
return lockout_time and lockout_time != 0
|
||||
|
||||
# Collecting results in a list
|
||||
results = []
|
||||
|
||||
for entry in unique_entries:
|
||||
username = entry.sAMAccountName.value
|
||||
last_logon = entry.lastLogon.value
|
||||
last_logon_timestamp = entry.lastLogonTimestamp.value
|
||||
object_guid = entry.objectGUID.value
|
||||
user_account_control = entry.userAccountControl.value
|
||||
lockout_time = entry.lockoutTime.value
|
||||
|
||||
# Convert timestamps
|
||||
last_logon_date = convert_filetime(last_logon)
|
||||
last_logon_timestamp_date = convert_filetime(last_logon_timestamp)
|
||||
|
||||
# Determine the most recent logon date
|
||||
if last_logon_date and last_logon_timestamp_date:
|
||||
most_recent_logon = max(last_logon_date, last_logon_timestamp_date)
|
||||
else:
|
||||
most_recent_logon = last_logon_date or last_logon_timestamp_date or 'Never logged in'
|
||||
|
||||
guid_string = object_guid
|
||||
disabled_status = "Disabled" if is_user_disabled(user_account_control) else "Enabled"
|
||||
locked_status = "Locked" if is_user_locked(lockout_time) else "Unlocked"
|
||||
|
||||
given_name = entry.givenName.value if entry.givenName else 'N/A'
|
||||
sn = entry.sn.value if entry.sn else 'N/A'
|
||||
cn = entry.cn.value if entry.cn else 'N/A'
|
||||
display_name = entry.displayName.value if entry.displayName else 'N/A'
|
||||
distinguished_name = entry.distinguishedName.value if entry.distinguishedName else 'N/A'
|
||||
user_principal_name = entry.userPrincipalName.value if entry.userPrincipalName else 'N/A'
|
||||
proxy_addresses = ', '.join(entry.proxyAddresses.values) if entry.proxyAddresses else 'N/A'
|
||||
mail = entry.mail.value if entry.mail else 'N/A'
|
||||
|
||||
# Skip computer accounts based on the presence of a dollar sign in the username
|
||||
if not username.endswith('$'):
|
||||
results.append({
|
||||
'User': username,
|
||||
'Given Name': given_name,
|
||||
'Surname': sn,
|
||||
'Common Name': cn,
|
||||
'Display Name': display_name,
|
||||
'Distinguished Name': distinguished_name,
|
||||
'User Principal Name': user_principal_name,
|
||||
'Proxy Addresses': proxy_addresses,
|
||||
'Mail': mail,
|
||||
'Most Recent Logon': most_recent_logon,
|
||||
'GUID': guid_string,
|
||||
'Status': disabled_status,
|
||||
'Lockout Status': locked_status
|
||||
})
|
||||
|
||||
# Unbind the connection
|
||||
conn.unbind()
|
||||
|
||||
# Create a DataFrame and export to Excel
|
||||
df = pd.DataFrame(results)
|
||||
|
||||
# Check if we have any results
|
||||
if len(results) > 0:
|
||||
# Ensure all datetime columns are timezone-unaware
|
||||
datetime_columns = ['Most Recent Logon']
|
||||
for column in datetime_columns:
|
||||
if column in df.columns:
|
||||
df[column] = df[column].apply(lambda x: x.replace(tzinfo=None) if isinstance(x, datetime.datetime) else x)
|
||||
|
||||
df.to_excel('active_directory_users.xlsx', index=False)
|
||||
print(f"Benutzer-Accounts erfolgreich exportiert. Gefundene Benutzer: {len(results)}")
|
||||
else:
|
||||
print("\n=== DEBUGGING-INFORMATIONEN ===")
|
||||
print("Keine Benutzer-Accounts gefunden.")
|
||||
print(f"Search-Base: {search_base}")
|
||||
print(f"Search-Filter: {search_filter}")
|
||||
print(f"LDAP-Server: {server_name}")
|
||||
print(f"Domain: {domain_name}")
|
||||
|
||||
if server_info and hasattr(server_info, 'naming_contexts'):
|
||||
print(f"Server Naming Contexts: {list(server_info.naming_contexts)}")
|
||||
if server_info and hasattr(server_info, 'schema_entry'):
|
||||
print(f"Schema Entry: {server_info.schema_entry}")
|
||||
|
||||
print("\nBitte überprüfen Sie:")
|
||||
print("1. LDAP-Verbindung und Anmeldedaten")
|
||||
print("2. Search-Base Konfiguration")
|
||||
print("3. Berechtigungen für die Benutzer-Suche")
|
||||
print("4. Domain-Controller Erreichbarkeit")
|
||||
|
||||
# Create empty Excel file with headers for reference
|
||||
headers = ['User', 'Given Name', 'Surname', 'Common Name', 'Display Name', 'Distinguished Name',
|
||||
'User Principal Name', 'Proxy Addresses', 'Mail', 'Most Recent Logon', 'GUID', 'Status', 'Lockout Status']
|
||||
empty_df = pd.DataFrame(columns=headers)
|
||||
empty_df.to_excel('active_directory_users.xlsx', index=False)
|
||||
@@ -6,6 +6,27 @@ from pprint import pprint
|
||||
import re
|
||||
import datetime
|
||||
import os
|
||||
|
||||
# MD4 ist im OpenSSL der uv-bundled Python (python-build-standalone) nicht verfuegbar.
|
||||
# ldap3 braucht MD4 fuer NTLM-Authentifizierung gegen Active Directory. Wir patchen
|
||||
# hashlib.new(), damit es fuer 'md4' auf pycryptodome zurueckfaellt.
|
||||
import hashlib as _hashlib
|
||||
try:
|
||||
_hashlib.new('md4')
|
||||
except Exception:
|
||||
from Crypto.Hash import MD4 as _CryptoMD4
|
||||
_orig_hashlib_new = _hashlib.new
|
||||
|
||||
def _hashlib_new_md4_fallback(name, data=b'', **kwargs):
|
||||
if name.lower() in ('md4', 'md-4'):
|
||||
h = _CryptoMD4.new()
|
||||
if data:
|
||||
h.update(data)
|
||||
return h
|
||||
return _orig_hashlib_new(name, data, **kwargs)
|
||||
|
||||
_hashlib.new = _hashlib_new_md4_fallback
|
||||
|
||||
from ldap3 import Server, Connection, ALL, NTLM, SUBTREE
|
||||
from .tools import render_card_html, render_single_card
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ ipaddress
|
||||
passwordgenerator
|
||||
urbackup-server-web-api-wrapper
|
||||
pandas
|
||||
pycryptodome
|
||||
|
||||
Reference in New Issue
Block a user