Hikvision 考勤机数据提取
海康威视的某考勤机可以根据网上的 api 接口提取数据,而不必用优盘拷贝。
经过观察,有如下的流程——
graph TD
A[开始] --> B[Step 1: GET sessionLogin/capabilities with random]
B --> C[Step 2: POST sessionLogin with timestamp]
C --> D[Step 3: PUT sessionHeartbeat]
D --> E[Step 4: GET Security/capabilities]
E --> F[Step 5: 计算固定AES密钥]
F --> G[Step 6: GET Security/users 验证密钥]
G --> H[Step 7: POST AcsEvent 获取数据]
H --> I[Step 8: AES-CBC + Base64 解密]
I --> J[保存为CSV或JSON]
J --> K[结束]
考勤等数据存在 AES CBC 加密和 Base64 编码。
aes key是固定不变的,可以调试得到(或者由固定的盐值按算法计算而来)




searchId 可以选择某个固定的值,或者使用 uuidv4


#!/usr/bin/env python3
"""
Hikvision Attendance Data Extractor (Cleaned & Enhanced)
- Uses fixed AES key
- Splits ISO8601 time into 'date' and 'time' (HH:MM:SS)
- Removes redundant comments/logic
"""
import json
import binascii
import base64
import hashlib
import time
import re
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# ------------------- Config -------------------
HOST = "192.168.1.192"
USERNAME = "admin"
PASSWORD = "password"
BASE_URL = f"http://{HOST}"
AES_KEY = "90c5ba363d3a4d7db66d2564c5697319" # 32 hex chars = 16 bytes
session = requests.Session()
# ------------------- Auth -------------------
def login():
# Step 1: Get session challenge
random_param = str(int(hashlib.md5(str(time.time()).encode()).hexdigest()[:8], 16))[:8]
cap_url = f"{BASE_URL}/ISAPI/Security/sessionLogin/capabilities?username={USERNAME}&random={random_param}"
try:
resp = session.get(cap_url, timeout=10)
resp.raise_for_status()
session_id = re.search(r"<sessionID>(\w+)</sessionID>", resp.text).group(1)
challenge = re.search(r"<challenge>([a-f0-9]+)</challenge>", resp.text).group(1)
iterations = int(re.search(r"<iterations>(\d+)</iterations>", resp.text).group(1))
salt = re.search(r"<salt>(\w+)</salt>", resp.text).group(1)
except Exception as e:
print(json.dumps({"error": f"Get capabilities failed: {e}"}))
return None
# Step 2: Compute password hash
pwd = hashlib.sha256((USERNAME + salt + PASSWORD).encode()).hexdigest()
pwd = hashlib.sha256((pwd + challenge).encode()).hexdigest()
for _ in range(2, iterations):
pwd = hashlib.sha256(pwd.encode()).hexdigest()
# Step 3: Login
login_xml = f"""<SessionLogin>
<userName>{USERNAME}</userName>
<password>{pwd}</password>
<sessionID>{session_id}</sessionID>
<isSupportSessionTag>false</isSupportSessionTag>
<isSessionIDValidLongTerm>false</isSessionIDValidLongTerm>
<sessionIDVersion>2</sessionIDVersion>
</SessionLogin>"""
ts = int(time.time() * 1000)
login_url = f"{BASE_URL}/ISAPI/Security/sessionLogin?timeStamp={ts}"
try:
resp = session.post(login_url, data=login_xml, timeout=10)
if "<statusValue>200</statusValue>" not in resp.text:
print(json.dumps({"error": "Login failed"}))
return None
return ts
except Exception as e:
print(json.dumps({"error": f"Login error: {e}"}))
return None
# ------------------- Decrypt -------------------
def aes_decrypt_base64(ciphertext_hex, key_hex, iv_hex):
if not ciphertext_hex:
return ""
try:
key = binascii.unhexlify(key_hex)
iv = binascii.unhexlify(iv_hex)
ct = binascii.unhexlify(ciphertext_hex)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = unpad(cipher.decrypt(ct), AES.block_size)
b64_str = decrypted.decode('utf-8')
return base64.b64decode(b64_str).decode('utf-8')
except Exception:
return ciphertext_hex # fallback to original if decryption fails
# ------------------- Fetch -------------------
def fetch_attendance(start_time, end_time, login_ts):
iv = hashlib.md5(str(login_ts).encode()).hexdigest()
url = f"{BASE_URL}/ISAPI/AccessControl/AcsEvent?format=json&security=1&iv={iv}"
records = []
position = 0
while True:
payload = {
"AcsEventCond": {
"searchID": "c6b8f41a-d75a-4e43-a0c9-8e032f636bb1",
"searchResultPosition": position,
"maxResults": 24,
"major": 0,
"minor": 0,
"startTime": start_time,
"endTime": end_time,
}
}
try:
resp = session.post(url, json=payload, timeout=30)
resp.raise_for_status()
data = resp.json()
info_list = data.get("AcsEvent", {}).get("InfoList", [])
if not info_list:
break
for item in info_list:
name_enc = item.get("name", "")
emp_enc = item.get("employeeNoString", "")
time_raw = item.get("time", "")
# Decrypt fields if long (likely encrypted)
name = aes_decrypt_base64(name_enc, AES_KEY, iv) if len(name_enc) > 20 else name_enc
emp_no = aes_decrypt_base64(emp_enc, AES_KEY, iv) if len(emp_enc) > 20 else emp_enc
# Split ISO8601 time: "2025-10-09T08:16:09+08:00" → date="2025-10-09", time="08:16:09"
date_part = time_part = ""
if time_raw:
if "T" in time_raw:
dt_part = time_raw.split("T")[0]
t_part = time_raw.split("T")[1]
time_part = t_part.split("+")[0].split(".")[0] # Remove timezone and millis
date_part = dt_part
else:
date_part = time_part = time_raw
records.append({
"employeeNo": emp_no,
"name": name,
"date": date_part,
"time": time_part,
"cardNo": item.get("cardNo", "")
})
position += len(info_list)
total = data.get("AcsEvent", {}).get("totalMatches", 0)
if position >= total:
break
except Exception as e:
print(json.dumps({"error": f"Fetch error: {e}"}))
break
return records
# ------------------- Main -------------------
def main():
login_ts = login()
if not login_ts:
return
start = "2025-10-01T00:00:00+08:00"
end = "2025-10-31T23:59:59+08:00"
records = fetch_attendance(start, end, login_ts)
with open("attendance_simple.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print(f"✅ Successfully saved {len(records)} records to attendance_simple.json")
if __name__ == "__main__":
main()
参考阅读:
海康摄像头Web登陆算法及过程 - 刚先生
记录一下项目中对接海康设备时使用ISAPI的一些经验
海康api 签名 认证 demo 海康摄像头认证方式basic
调用认证
利用Python调用海康威视综合管理平台openAPI接口
常见问题解决 --- 海康OpenAPI安全认证库的demo运行报错
浙公网安备 33010602011771号