Update public source
This commit is contained in:
@@ -0,0 +1,834 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Общая бизнес-логика Hashcat.
|
||||
|
||||
Этот модуль используется одновременно:
|
||||
• серверным Reports;
|
||||
• CLI tools/hashcat.py.
|
||||
|
||||
Модуль не открывает соединение с SQLite самостоятельно.
|
||||
|
||||
Соединение передаётся вызывающим кодом и обслуживается
|
||||
существующим reports.database.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Constants
|
||||
# ============================================================
|
||||
|
||||
MAC_PATTERN = re.compile(
|
||||
r"^[0-9a-fA-F]{12}$"
|
||||
)
|
||||
|
||||
SUPPORTED_HANDSHAKE_TYPES = {
|
||||
"WPA*01",
|
||||
"WPA*02",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Export filename
|
||||
# ============================================================
|
||||
|
||||
def sanitize_export_query(
|
||||
query,
|
||||
):
|
||||
value = str(
|
||||
query or ""
|
||||
).strip().lower()
|
||||
|
||||
value = re.sub(
|
||||
r"[^a-z0-9]+",
|
||||
"-",
|
||||
value
|
||||
)
|
||||
|
||||
value = value.strip(
|
||||
"-"
|
||||
)
|
||||
|
||||
if not value:
|
||||
value = "all"
|
||||
|
||||
return value[
|
||||
:80
|
||||
]
|
||||
|
||||
|
||||
def build_export_filename(
|
||||
export_type,
|
||||
query="",
|
||||
):
|
||||
timestamp = datetime.now(
|
||||
timezone.utc
|
||||
).strftime(
|
||||
"%Y%m%d-%H%M%S"
|
||||
)
|
||||
|
||||
if export_type == "all":
|
||||
|
||||
return (
|
||||
"wifi-gps-mapper-all-"
|
||||
f"{timestamp}.hc22000"
|
||||
)
|
||||
|
||||
if export_type == "search":
|
||||
|
||||
safe_query = sanitize_export_query(
|
||||
query
|
||||
)
|
||||
|
||||
return (
|
||||
"wifi-gps-mapper-search-"
|
||||
f"{safe_query}-"
|
||||
f"{timestamp}.hc22000"
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"Unknown export type: {export_type}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Hash export
|
||||
# ============================================================
|
||||
|
||||
def _build_hash_export_from_rows(
|
||||
rows,
|
||||
):
|
||||
hashes = [
|
||||
str(
|
||||
row["hash22000"]
|
||||
)
|
||||
for row in rows
|
||||
if row["hash22000"] is not None
|
||||
and str(
|
||||
row["hash22000"]
|
||||
).strip()
|
||||
]
|
||||
|
||||
if not hashes:
|
||||
return ""
|
||||
|
||||
return (
|
||||
"\n".join(
|
||||
hashes
|
||||
)
|
||||
+
|
||||
"\n"
|
||||
)
|
||||
|
||||
|
||||
def get_all_hashes(
|
||||
conn,
|
||||
):
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT
|
||||
h.hash22000
|
||||
FROM handshakes AS h
|
||||
WHERE
|
||||
h.hash22000 IS NOT NULL
|
||||
AND TRIM(h.hash22000) != ''
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM credentials AS c
|
||||
WHERE c.handshake_id = h.id
|
||||
)
|
||||
ORDER BY
|
||||
h.hash22000
|
||||
"""
|
||||
)
|
||||
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def build_all_hash_export(
|
||||
conn,
|
||||
):
|
||||
rows = get_all_hashes(
|
||||
conn
|
||||
)
|
||||
|
||||
return _build_hash_export_from_rows(
|
||||
rows
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Database statistics
|
||||
# ============================================================
|
||||
|
||||
def get_export_statistics(
|
||||
conn,
|
||||
):
|
||||
rows = get_all_hashes(
|
||||
conn
|
||||
)
|
||||
|
||||
statistics = {
|
||||
"exported_hashes": len(rows),
|
||||
"wpa01": 0,
|
||||
"wpa02": 0,
|
||||
}
|
||||
|
||||
for row in rows:
|
||||
|
||||
hash_value = str(
|
||||
row["hash22000"]
|
||||
)
|
||||
|
||||
if hash_value.startswith(
|
||||
"WPA*01*"
|
||||
):
|
||||
statistics["wpa01"] += 1
|
||||
|
||||
elif hash_value.startswith(
|
||||
"WPA*02*"
|
||||
):
|
||||
statistics["wpa02"] += 1
|
||||
|
||||
return statistics
|
||||
|
||||
def get_database_statistics(
|
||||
conn,
|
||||
):
|
||||
statistics = {}
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM access_points
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["access_points"] = cursor.fetchone()[0]
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM handshakes
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["handshakes"] = cursor.fetchone()[0]
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM handshakes
|
||||
WHERE
|
||||
hash22000 IS NOT NULL
|
||||
AND TRIM(hash22000) != ''
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["hashes"] = cursor.fetchone()[0]
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(DISTINCT hash22000)
|
||||
FROM handshakes
|
||||
WHERE
|
||||
hash22000 IS NOT NULL
|
||||
AND TRIM(hash22000) != ''
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["unique_hashes"] = cursor.fetchone()[0]
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM credentials
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["credentials"] = cursor.fetchone()[0]
|
||||
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT COUNT(DISTINCT c.access_point_id)
|
||||
FROM credentials AS c
|
||||
JOIN handshakes AS h
|
||||
ON h.id = c.handshake_id
|
||||
"""
|
||||
)
|
||||
|
||||
statistics["cracked_access_points"] = cursor.fetchone()[0]
|
||||
|
||||
return statistics
|
||||
|
||||
# ============================================================
|
||||
# Hashcat --show parser
|
||||
# ============================================================
|
||||
|
||||
def _normalize_mac(
|
||||
value,
|
||||
):
|
||||
value = str(
|
||||
value or ""
|
||||
).strip().lower()
|
||||
|
||||
value = value.replace(
|
||||
":",
|
||||
""
|
||||
)
|
||||
|
||||
value = value.replace(
|
||||
"-",
|
||||
""
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
def _normalize_hashcat_essid(
|
||||
value,
|
||||
):
|
||||
value = str(
|
||||
value or ""
|
||||
).strip()
|
||||
|
||||
if (
|
||||
value.startswith("$HEX[")
|
||||
and value.endswith("]")
|
||||
):
|
||||
encoded = value[5:-1]
|
||||
|
||||
if not encoded:
|
||||
return ""
|
||||
|
||||
if (
|
||||
len(encoded) % 2 != 0
|
||||
or re.fullmatch(
|
||||
r"[0-9a-fA-F]+",
|
||||
encoded
|
||||
) is None
|
||||
):
|
||||
raise ValueError(
|
||||
"ESSID $HEX value must be hexadecimal."
|
||||
)
|
||||
|
||||
return bytes.fromhex(
|
||||
encoded
|
||||
).hex()
|
||||
|
||||
return value.encode(
|
||||
"utf-8"
|
||||
).hex()
|
||||
|
||||
def _parse_handshake_hash(
|
||||
value,
|
||||
):
|
||||
value = str(
|
||||
value or ""
|
||||
).strip()
|
||||
|
||||
if not value:
|
||||
raise ValueError(
|
||||
"HASH is empty."
|
||||
)
|
||||
|
||||
parts = value.split(
|
||||
"*"
|
||||
)
|
||||
|
||||
if len(parts) < 6:
|
||||
raise ValueError(
|
||||
"HASH is not a valid WPA hash."
|
||||
)
|
||||
|
||||
handshake_type = (
|
||||
f"{parts[0]}*{parts[1]}"
|
||||
)
|
||||
|
||||
if handshake_type not in SUPPORTED_HANDSHAKE_TYPES:
|
||||
raise ValueError(
|
||||
"Unsupported handshake type: "
|
||||
f"{handshake_type}."
|
||||
)
|
||||
|
||||
hash_value = parts[2].strip().lower()
|
||||
ap_bssid = _normalize_mac(
|
||||
parts[3]
|
||||
)
|
||||
client_mac = _normalize_mac(
|
||||
parts[4]
|
||||
)
|
||||
essid = parts[5].strip().lower()
|
||||
|
||||
if not hash_value:
|
||||
raise ValueError(
|
||||
"HASH value is empty."
|
||||
)
|
||||
|
||||
if (
|
||||
len(hash_value) != 32
|
||||
or re.fullmatch(
|
||||
r"[0-9a-f]{32}",
|
||||
hash_value
|
||||
) is None
|
||||
):
|
||||
raise ValueError(
|
||||
"HASH value must be 32 hexadecimal characters."
|
||||
)
|
||||
|
||||
if not MAC_PATTERN.fullmatch(
|
||||
ap_bssid
|
||||
):
|
||||
raise ValueError(
|
||||
"AP_BSSID is not a valid MAC address."
|
||||
)
|
||||
|
||||
if not MAC_PATTERN.fullmatch(
|
||||
client_mac
|
||||
):
|
||||
raise ValueError(
|
||||
"CLIENT_MAC is not a valid MAC address."
|
||||
)
|
||||
|
||||
if essid and (
|
||||
len(essid) % 2 != 0
|
||||
or re.fullmatch(
|
||||
r"[0-9a-fA-F]+",
|
||||
essid
|
||||
) is None
|
||||
):
|
||||
raise ValueError(
|
||||
"ESSID must be hexadecimal."
|
||||
)
|
||||
|
||||
return {
|
||||
"handshake_type": handshake_type,
|
||||
"hash": hash_value.lower(),
|
||||
"ap_bssid": ap_bssid,
|
||||
"client_mac": client_mac,
|
||||
"essid": essid,
|
||||
}
|
||||
|
||||
|
||||
def parse_hashcat_show_line(
|
||||
line,
|
||||
line_number,
|
||||
):
|
||||
raw = str(
|
||||
line
|
||||
).rstrip(
|
||||
"\r\n"
|
||||
)
|
||||
|
||||
if not raw.strip():
|
||||
raise ValueError(
|
||||
"Empty line."
|
||||
)
|
||||
|
||||
parts = raw.split(
|
||||
":",
|
||||
4
|
||||
)
|
||||
|
||||
if len(parts) != 5:
|
||||
raise ValueError(
|
||||
"Expected format: "
|
||||
"HASH:AP_BSSID:CLIENT_MAC:ESSID:PASSWORD."
|
||||
)
|
||||
|
||||
hash_value = parts[0].strip().lower()
|
||||
ap_bssid = _normalize_mac(
|
||||
parts[1]
|
||||
)
|
||||
client_mac = _normalize_mac(
|
||||
parts[2]
|
||||
)
|
||||
essid = parts[3].strip()
|
||||
password = parts[4]
|
||||
|
||||
if not hash_value:
|
||||
raise ValueError(
|
||||
"HASH is empty."
|
||||
)
|
||||
|
||||
if (
|
||||
len(hash_value) != 32
|
||||
or re.fullmatch(
|
||||
r"[0-9a-f]{32}",
|
||||
hash_value
|
||||
) is None
|
||||
):
|
||||
raise ValueError(
|
||||
"HASH value must be 32 hexadecimal characters."
|
||||
)
|
||||
|
||||
if not MAC_PATTERN.fullmatch(
|
||||
ap_bssid
|
||||
):
|
||||
raise ValueError(
|
||||
"AP_BSSID is not a valid MAC address."
|
||||
)
|
||||
|
||||
if not MAC_PATTERN.fullmatch(
|
||||
client_mac
|
||||
):
|
||||
raise ValueError(
|
||||
"CLIENT_MAC is not a valid MAC address."
|
||||
)
|
||||
|
||||
essid_hex = _normalize_hashcat_essid(
|
||||
essid
|
||||
)
|
||||
|
||||
return {
|
||||
"line_number": line_number,
|
||||
"hash22000": hash_value,
|
||||
"hash": hash_value,
|
||||
"ap_bssid": ap_bssid,
|
||||
"client_mac": client_mac,
|
||||
"essid": essid_hex,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Handshake matching
|
||||
# ============================================================
|
||||
|
||||
def find_matching_handshake(
|
||||
conn,
|
||||
parsed,
|
||||
):
|
||||
from reports.database import query_all
|
||||
|
||||
rows = query_all(
|
||||
conn,
|
||||
"""
|
||||
SELECT
|
||||
h.id,
|
||||
h.access_point_id,
|
||||
h.hash22000
|
||||
FROM handshakes AS h
|
||||
WHERE
|
||||
h.hash22000 IS NOT NULL
|
||||
AND TRIM(h.hash22000) != ''
|
||||
ORDER BY
|
||||
h.id
|
||||
"""
|
||||
)
|
||||
|
||||
for row in rows:
|
||||
|
||||
try:
|
||||
handshake = _parse_handshake_hash(
|
||||
row["hash22000"]
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if (
|
||||
handshake["hash"]
|
||||
== parsed["hash"]
|
||||
and handshake["ap_bssid"]
|
||||
== parsed["ap_bssid"]
|
||||
and handshake["client_mac"]
|
||||
== parsed["client_mac"]
|
||||
):
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Credential operations
|
||||
# ============================================================
|
||||
|
||||
def credential_exists(
|
||||
conn,
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
):
|
||||
from reports.database import query_one
|
||||
|
||||
row = query_one(
|
||||
conn,
|
||||
"""
|
||||
SELECT id
|
||||
FROM credentials
|
||||
WHERE
|
||||
access_point_id = ?
|
||||
AND handshake_id = ?
|
||||
AND password = ?
|
||||
LIMIT 1
|
||||
""",
|
||||
(
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
)
|
||||
)
|
||||
|
||||
return row is not None
|
||||
|
||||
|
||||
def add_credential(
|
||||
conn,
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
):
|
||||
from reports.database import execute
|
||||
|
||||
now = datetime.now(
|
||||
timezone.utc
|
||||
).isoformat().replace(
|
||||
"+00:00",
|
||||
"Z"
|
||||
)
|
||||
|
||||
cursor = execute(
|
||||
conn,
|
||||
"""
|
||||
INSERT INTO credentials
|
||||
(
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
source,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
)
|
||||
""",
|
||||
(
|
||||
access_point_id,
|
||||
handshake_id,
|
||||
password,
|
||||
"hashcat",
|
||||
now,
|
||||
)
|
||||
)
|
||||
|
||||
execute(
|
||||
conn,
|
||||
"""
|
||||
UPDATE access_points
|
||||
SET
|
||||
is_cracked = 1
|
||||
WHERE
|
||||
id = ?
|
||||
""",
|
||||
(
|
||||
access_point_id,
|
||||
)
|
||||
)
|
||||
|
||||
return cursor.lastrowid
|
||||
|
||||
# ============================================================
|
||||
# Import validation
|
||||
# ============================================================
|
||||
|
||||
def validate_hashcat_show_file(
|
||||
conn,
|
||||
lines,
|
||||
):
|
||||
statistics = {
|
||||
"total_lines": 0,
|
||||
"valid_lines": 0,
|
||||
"invalid_lines": 0,
|
||||
"matched_handshakes": 0,
|
||||
"missing_handshakes": 0,
|
||||
"new_credentials": 0,
|
||||
"duplicate_credentials": 0,
|
||||
"already_existing": 0,
|
||||
"errors": [],
|
||||
"missing": [],
|
||||
"items": [],
|
||||
}
|
||||
|
||||
seen = set()
|
||||
seen_credentials = set()
|
||||
|
||||
for line_number, line in enumerate(
|
||||
lines,
|
||||
start=1
|
||||
):
|
||||
statistics["total_lines"] += 1
|
||||
|
||||
try:
|
||||
parsed = parse_hashcat_show_line(
|
||||
line,
|
||||
line_number
|
||||
)
|
||||
|
||||
key = (
|
||||
parsed["hash"],
|
||||
parsed["ap_bssid"],
|
||||
parsed["client_mac"],
|
||||
parsed["essid"],
|
||||
parsed["password"],
|
||||
)
|
||||
|
||||
if key in seen:
|
||||
raise ValueError(
|
||||
"Duplicate line in input file."
|
||||
)
|
||||
|
||||
seen.add(
|
||||
key
|
||||
)
|
||||
|
||||
handshake = find_matching_handshake(
|
||||
conn,
|
||||
parsed
|
||||
)
|
||||
|
||||
if handshake is None:
|
||||
statistics["missing_handshakes"] += 1
|
||||
|
||||
statistics["missing"].append(
|
||||
{
|
||||
"line": line_number,
|
||||
"hash":
|
||||
parsed["hash"],
|
||||
"ap_bssid":
|
||||
parsed["ap_bssid"],
|
||||
"client_mac":
|
||||
parsed["client_mac"],
|
||||
"essid":
|
||||
parsed["essid"],
|
||||
"password":
|
||||
parsed["password"],
|
||||
}
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
statistics["matched_handshakes"] += 1
|
||||
|
||||
credential_key = (
|
||||
handshake["access_point_id"],
|
||||
handshake["id"],
|
||||
parsed["password"],
|
||||
)
|
||||
|
||||
if credential_key in seen_credentials:
|
||||
|
||||
statistics["duplicate_credentials"] += 1
|
||||
status = "Duplicate credential"
|
||||
|
||||
else:
|
||||
|
||||
seen_credentials.add(
|
||||
credential_key
|
||||
)
|
||||
|
||||
exists = credential_exists(
|
||||
conn,
|
||||
handshake["access_point_id"],
|
||||
handshake["id"],
|
||||
parsed["password"]
|
||||
)
|
||||
|
||||
if exists:
|
||||
statistics["already_existing"] += 1
|
||||
status = "Already exists"
|
||||
|
||||
else:
|
||||
statistics["new_credentials"] += 1
|
||||
status = "New credential"
|
||||
|
||||
statistics["valid_lines"] += 1
|
||||
|
||||
statistics["items"].append(
|
||||
{
|
||||
"line": line_number,
|
||||
"access_point_id":
|
||||
handshake["access_point_id"],
|
||||
"handshake_id":
|
||||
handshake["id"],
|
||||
"password":
|
||||
parsed["password"],
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError as error:
|
||||
|
||||
statistics["invalid_lines"] += 1
|
||||
|
||||
statistics["errors"].append(
|
||||
{
|
||||
"line": line_number,
|
||||
"reason": str(
|
||||
error
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return statistics
|
||||
|
||||
# ============================================================
|
||||
# Atomic import
|
||||
# ============================================================
|
||||
|
||||
def import_hashcat_show(
|
||||
conn,
|
||||
lines,
|
||||
):
|
||||
lines = list(
|
||||
lines
|
||||
)
|
||||
|
||||
validation = validate_hashcat_show_file(
|
||||
conn,
|
||||
lines
|
||||
)
|
||||
|
||||
if validation["errors"]:
|
||||
|
||||
return {
|
||||
**validation,
|
||||
"committed": False,
|
||||
"result": "Rejected",
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
for item in validation["items"]:
|
||||
|
||||
if item["status"] != "New credential":
|
||||
continue
|
||||
|
||||
add_credential(
|
||||
conn,
|
||||
item["access_point_id"],
|
||||
item["handshake_id"],
|
||||
item["password"]
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
**validation,
|
||||
"committed": True,
|
||||
"result": "Imported",
|
||||
}
|
||||
|
||||
except Exception:
|
||||
|
||||
conn.rollback()
|
||||
|
||||
raise
|
||||
Reference in New Issue
Block a user