Hikvision 考勤机数据提取(3)
同样使用 HTTPDigestAuth, 同时由于认证信息(Nonce)过期或连接断开导致 401 错误,可以在每次请求前重新创建 HTTPDigestAuth 认证对象,或者 使用 requests session 添加重试机制。
def fetch_attendance_v3(start_time, end_time, aes_key, base_url, employee_no=None, proxies=None, fixed_iv=None):
all_records = []
# Determine IV
if fixed_iv:
iv = fixed_iv
print(f"Using Fixed IV: {iv}")
else:
# Dynamic IV based on timestamp
login_timestamp = int(time.time() * 1000)
iv = hashlib.md5(str(login_timestamp).encode()).hexdigest()
print(f"Using Dynamic IV: {iv}")
url = f"{base_url}/ISAPI/AccessControl/AcsEvent?format=json&security=1&iv={iv}"
position = 0
batch_size = 24
search_id = str(uuid.uuid4())
print(f"Fetching data from {start_time} to {end_time}...")
target_encrypted_emp = None
if employee_no:
if len(employee_no) > 20 and all(c in '0123456789abcdefABCDEF' for c in employee_no):
target_encrypted_emp = employee_no
else:
target_encrypted_emp = aes_encrypt(employee_no, aes_key, iv)
print(f" -> Encrypted Employee No: {target_encrypted_emp}")
# Use a Session for connection pooling and auth state persistence
session = requests.Session()
if proxies:
session.proxies.update(proxies)
# Initialize Auth
session.auth = DIGEST_AUTH
while True:
acs_event_cond = {
"searchID": search_id,
"searchResultPosition": position,
"maxResults": batch_size,
"major": 0,
"minor": 0,
"startTime": start_time,
"endTime": end_time,
}
if target_encrypted_emp:
acs_event_cond["employeeNoString"] = target_encrypted_emp
payload = {"AcsEventCond": acs_event_cond}
headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
# Retry loop for current batch
max_retries = 3
success = False
for attempt in range(max_retries):
try:
resp = session.post(
url,
json=payload,
headers=headers,
timeout=30
)
if resp.status_code == 401:
print(f"Warning: 401 Unauthorized at position {position}. Retrying ({attempt+1}/{max_retries})...")
# Force a fresh auth context if needed, or just let the session handle the next challenge
# Sometimes creating a new auth object helps if the old one's state is stale
session.auth = HTTPDigestAuth(USERNAME, PASSWORD)
time.sleep(1)
continue
resp.raise_for_status()
data = resp.json()
acs = data.get('AcsEvent', {})
info = acs.get('InfoList', [])
if not info:
# No more data in this batch, but check totalMatches to be sure
# If totalMatches > position, maybe we just got an empty page?
# Usually empty InfoList means done.
pass
for item in info:
name_enc = item.get('name', '')
emp_enc = item.get('employeeNoString', '')
name = aes_decrypt_base64(name_enc, aes_key, iv) if len(name_enc) > 20 else name_enc
curr_employee_no = aes_decrypt_base64(emp_enc, aes_key, iv) if len(emp_enc) > 20 else emp_enc
# Create record by copying original item and updating decrypted fields
record = item.copy()
record['name'] = name
record['employeeNoString'] = curr_employee_no
if 'employeeNo' not in record:
record['employeeNo'] = curr_employee_no
all_records.append(record)
position += len(info)
total_matches = acs.get('totalMatches', 0)
print(f"Fetched {position}/{total_matches} records...")
if position >= total_matches:
return all_records # Done
success = True
break # Move to next batch
except Exception as e:
print(f"Error fetching batch at {position}: {e}")
time.sleep(2)
if not success:
print(f"Failed to fetch batch at position {position} after retries. Stopping.")
break
return all_records
下面是每次Post都重新创建
import json
import binascii
import base64
import hashlib
import time
import requests
import argparse
import uuid
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
from requests.auth import HTTPDigestAuth
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DEFAULT_IP = "192.168.1.192"
DEFAULT_PORT = "8080"
USERNAME = "admin"
PASSWORD = "password"
# 认证对象
# DIGEST_AUTH = HTTPDigestAuth(USERNAME, PASSWORD)
# ---------------------------------------------------------------------------
# Key Derivation
# ---------------------------------------------------------------------------
def get_aes_key(host_str, username, password, auth, proxies=None):
"""
Derive AES Key dynamically from device capabilities.
"""
try:
url = f"http://{host_str}/ISAPI/Security/capabilities?username={username}"
print(f"Fetching capabilities from {url}...")
resp = requests.get(url, auth=auth, timeout=10, verify=False, proxies=proxies)
resp.raise_for_status()
root = ET.fromstring(resp.text)
ns = {'ns': 'http://www.isapi.org/ver20/XMLSchema'}
salt_node = root.find('ns:salt', ns)
if salt_node is None:
print("Error: Could not find <salt> in capabilities response.")
return None
salt = salt_node.text
print(f"Got Salt: {salt}")
# 1. Calculate IrreversibleKey
combined_string = f"{username}{salt}{password}"
irreversible_key = hashlib.sha256(combined_string.encode('utf-8')).hexdigest()
# 2. Calculate Final Key with Challenge
# Note: The challenge "AaBbCcDd1234!@#$" appears to be hardcoded or specific to this auth mode
challenge = "AaBbCcDd1234!@#$"
combined_for_hash = f"{irreversible_key}{challenge}"
key = hashlib.sha256(combined_for_hash.encode('utf-8')).hexdigest()
# 3. Iterate hashing
iterations = 100
for _ in range(1, iterations):
key = hashlib.sha256(key.encode()).hexdigest()
aes_key = key[:32]
print(f"Derived AES Key: {aes_key}")
return aes_key
except Exception as e:
print(f"Key derivation failed: {e}")
return None
# ---------------------------------------------------------------------------
# Encryption / Decryption
# ---------------------------------------------------------------------------
def aes_encrypt(plaintext, key_hex, iv_hex):
try:
key_bytes = binascii.unhexlify(key_hex)
iv_bytes = binascii.unhexlify(iv_hex)
b64_str = base64.b64encode(plaintext.encode('utf-8')).decode('utf-8')
data_bytes = b64_str.encode('utf-8')
padded_data = pad(data_bytes, AES.block_size)
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
ciphertext_bytes = cipher.encrypt(padded_data)
return binascii.hexlify(ciphertext_bytes).decode('utf-8')
except Exception as e:
print(f"Encryption error: {e}")
return None
def aes_decrypt_base64(ciphertext_hex, key_hex, iv_hex):
if not ciphertext_hex:
return ""
try:
key_bytes = binascii.unhexlify(key_hex)
iv_bytes = binascii.unhexlify(iv_hex)
ct_bytes = binascii.unhexlify(ciphertext_hex)
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
decrypted = cipher.decrypt(ct_bytes)
decrypted = unpad(decrypted, AES.block_size)
b64_str = decrypted.decode('utf-8')
plain_bytes = base64.b64decode(b64_str)
return plain_bytes.decode('utf-8')
except Exception:
return ""
def parse_time_arg(time_str):
if not time_str:
return None
if 'T' in time_str:
if '+' not in time_str and 'Z' not in time_str:
return time_str + "+08:00"
return time_str
try:
dt = None
time_str = time_str.strip()
if len(time_str) == 10:
dt = datetime.strptime(time_str, "%Y-%m-%d")
elif len(time_str) == 16:
dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M")
elif len(time_str) == 19:
dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
if dt:
return dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
except ValueError:
pass
return time_str
# ---------------------------------------------------------------------------
# Data Fetching
# ---------------------------------------------------------------------------
def fetch_attendance_v3(start_time, end_time, aes_key, base_url, employee_no=None, proxies=None, fixed_iv=None):
all_records = []
# Determine IV
if fixed_iv:
iv = fixed_iv
print(f"Using Fixed IV: {iv}")
else:
# Dynamic IV based on timestamp
login_timestamp = int(time.time() * 1000)
iv = hashlib.md5(str(login_timestamp).encode()).hexdigest()
print(f"Using Dynamic IV: {iv}")
url = f"{base_url}/ISAPI/AccessControl/AcsEvent?format=json&security=1&iv={iv}"
position = 0
batch_size = 24
search_id = str(uuid.uuid4())
print(f"Fetching data from {start_time} to {end_time}...")
target_encrypted_emp = None
if employee_no:
if len(employee_no) > 20 and all(c in '0123456789abcdefABCDEF' for c in employee_no):
target_encrypted_emp = employee_no
else:
target_encrypted_emp = aes_encrypt(employee_no, aes_key, iv)
print(f" -> Encrypted Employee No: {target_encrypted_emp}")
retry_count = 0
max_retries = 3
while True:
acs_event_cond = {
"searchID": search_id,
"searchResultPosition": position,
"maxResults": batch_size,
"major": 0,
"minor": 0,
"startTime": start_time,
"endTime": end_time,
}
if target_encrypted_emp:
acs_event_cond["employeeNoString"] = target_encrypted_emp
payload = {"AcsEventCond": acs_event_cond}
headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
}
try:
fresh_auth = HTTPDigestAuth(USERNAME, PASSWORD)
resp = requests.post(
url,
json=payload,
headers=headers,
auth=fresh_auth,
proxies=proxies,
timeout=30
)
resp.raise_for_status()
data = resp.json()
acs = data.get('AcsEvent', {})
info = acs.get('InfoList', [])
if not info:
break
for item in info:
name_enc = item.get('name', '')
emp_enc = item.get('employeeNoString', '')
name = aes_decrypt_base64(name_enc, aes_key, iv) if len(name_enc) > 20 else name_enc
curr_employee_no = aes_decrypt_base64(emp_enc, aes_key, iv) if len(emp_enc) > 20 else emp_enc
record = {
'employeeNo': curr_employee_no,
'name': name,
'time': item.get('time', ''),
'cardNo': item.get('cardNo', ''),
'raw_employeeNoString': emp_enc
}
all_records.append(record)
position += len(info)
total_matches = acs.get('totalMatches', 0)
print(f"Fetched {position}/{total_matches} records...")
retry_count =0
if position >= total_matches:
break
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401 and retry_count < max_retries:
retry_count += 1
print(f"Authentication failed. Retrying {retry_count}/{max_retries}...")
continue
else:
print(json.dumps({"error": f"Fetch error: {e}"}, ensure_ascii=False))
break
except Exception as e:
print(json.dumps({"error": f"Fetch error: {e}"}, ensure_ascii=False))
break
return all_records
# ---------------------------------------------------------------------------
# Main Function
# ---------------------------------------------------------------------------
def main_v3():
# global DIGEST_AUTH
parser = argparse.ArgumentParser(description="Hikvision Attendance Fetcher V3 (Dynamic Key)")
parser.add_argument("--start", help="Start time (e.g. 2025-12-08 08:00)", default=None)
parser.add_argument("--end", help="End time (e.g. 2025-12-08 23:59)", default=None)
parser.add_argument("--employeeNo", help="Employee Number (e.g. 16128 or encrypted hex)", default=None)
parser.add_argument("--out", help="Output JSON file", default="attendance_v3.json")
parser.add_argument("--ip", help=f"Target IP (default: {DEFAULT_IP})", default=DEFAULT_IP)
parser.add_argument("--port", help=f"Target Port (default: {DEFAULT_PORT})", default=DEFAULT_PORT)
parser.add_argument("--proxy", help="Proxy URL (e.g. http://127.0.0.1:8899)", default=None)
parser.add_argument("--iv", help="Fixed IV (optional, for encrypted employeeNo)", default=None)
args = parser.parse_args()
host_str = f"{args.ip}:{args.port}"
base_url = f"http://{host_str}"
# Configure Proxies
proxies = None
if args.proxy:
proxies = {
"http": args.proxy,
"https": args.proxy,
}
print(f"Using Proxy: {args.proxy}")
DIGEST_AUTH = HTTPDigestAuth(USERNAME, PASSWORD)
# 1. Derive Key
print("Deriving AES Key...")
aes_key = get_aes_key(host_str, USERNAME, PASSWORD, DIGEST_AUTH, proxies)
if not aes_key:
print("Failed to derive key. Exiting.")
return
# 2. Parse Times
if args.start:
args.start = parse_time_arg(args.start)
else:
now = datetime.now()
start_dt = datetime(now.year, now.month, now.day, 0, 0, 0)
args.start = start_dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
if args.end:
args.end = parse_time_arg(args.end)
else:
now = datetime.now()
end_dt = datetime(now.year, now.month, now.day, 23, 59, 59)
args.end = end_dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
# 3. Fetch Data
records = fetch_attendance_v3(args.start, args.end, aes_key, base_url, args.employeeNo, proxies, args.iv)
# 4. Save
with open(args.out, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print(f"Data successfully saved to {args.out} ({len(records)} records)")
if __name__ == "__main__":
main_v3()


浙公网安备 33010602011771号