feat(prober-cert): schema for active TLS cert capture
Adds storage for TLS certificate details collected from attacker-run servers by the active prober (sibling to the existing JARM probe). - AttackerIdentity.tls_cert_sha256 / Campaign.tls_cert_sha256: JSON list[str] columns mirroring ja3_hashes / hassh_hashes for federation gossip. - ingester clause 9b: emits a 'tls_certificate' fingerprint bounty when a prober event carries subject_cn (disjoint from the existing sniffer-gated clause). - Prober-side capture (ssl.wrap_socket follow-up after JARM) and profiler rollup land in sibling commits.
This commit is contained in:
@@ -154,6 +154,14 @@ class AttackerIdentity(SQLModel, table=True):
|
||||
hassh_hashes: Optional[str] = Field(
|
||||
default=None, sa_column=Column("hassh_hashes", Text, nullable=True)
|
||||
)
|
||||
# JSON list[str] — SHA-256 fingerprints of leaf certs presented by
|
||||
# attacker-run TLS servers, captured by the active prober alongside
|
||||
# JARM. Same federation-gossip rationale as ja3_hashes/hassh_hashes:
|
||||
# a self-signed cert reused across C2 nodes is an instant cluster-link
|
||||
# signal, and TEXT keeps MySQL indexable via prefix length.
|
||||
tls_cert_sha256: Optional[str] = Field(
|
||||
default=None, sa_column=Column("tls_cert_sha256", Text, nullable=True)
|
||||
)
|
||||
# Payload SimHash list — 64-bit ints serialized as hex strings.
|
||||
# SimHashes are Hamming-comparable, which is the entire reason
|
||||
# they're a list (not a set).
|
||||
|
||||
@@ -53,6 +53,9 @@ class Campaign(SQLModel, table=True):
|
||||
hassh_hashes: Optional[str] = Field(
|
||||
default=None, sa_column=Column("hassh_hashes", Text, nullable=True)
|
||||
)
|
||||
tls_cert_sha256: Optional[str] = Field(
|
||||
default=None, sa_column=Column("tls_cert_sha256", Text, nullable=True)
|
||||
)
|
||||
payload_simhashes: Optional[str] = Field(
|
||||
default=None, sa_column=Column("payload_simhashes", Text, nullable=True)
|
||||
)
|
||||
|
||||
@@ -499,6 +499,30 @@ async def _extract_bounty(
|
||||
},
|
||||
})
|
||||
|
||||
# 9b. TLS certificate from active prober (sibling of the JARM probe;
|
||||
# captured by a follow-up ssl.wrap_socket() against attacker-run TLS
|
||||
# servers). Disjoint from clause 8 above which is the sniffer path.
|
||||
_prober_subject_cn = _fields.get("subject_cn")
|
||||
if _prober_subject_cn and log_data.get("service") == "prober":
|
||||
await repo.add_bounty({
|
||||
"decky": log_data.get("decky"),
|
||||
"service": "prober",
|
||||
"attacker_ip": _fields.get("target_ip", "Unknown"),
|
||||
"bounty_type": "fingerprint",
|
||||
"payload": {
|
||||
"fingerprint_type": "tls_certificate",
|
||||
"subject_cn": _prober_subject_cn,
|
||||
"issuer": _fields.get("issuer"),
|
||||
"self_signed": _fields.get("self_signed"),
|
||||
"not_before": _fields.get("not_before"),
|
||||
"not_after": _fields.get("not_after"),
|
||||
"sans": _fields.get("sans"),
|
||||
"cert_sha256": _fields.get("cert_sha256"),
|
||||
"target_ip": _fields.get("target_ip"),
|
||||
"target_port": _fields.get("target_port"),
|
||||
},
|
||||
})
|
||||
|
||||
# 10. HASSHServer fingerprint from active prober
|
||||
_hassh = _fields.get("hassh_server_hash")
|
||||
if _hassh and log_data.get("service") == "prober":
|
||||
|
||||
Reference in New Issue
Block a user