A concatenated ZIP joins two or more archives into one file. Each has its own central directory and end-of-central-directory (EOCD) record, and different unpacking tools read different ones. A gateway that inspects the first may pass a file whose real payload sits in the last, which the user’s tool opens. The artefact to hunt is a ZIP structure that does not have exactly one directory.
Signals at a Glance
| Signal | Where | ATT&CK |
|---|---|---|
| More than one EOCD signature (PK 05 06) in one file | Gateway, file scan | T1027 |
| Different file listings from two ZIP parsers for the same file | Sandbox, gateway | T1027 |
| Archive nested inside an archive, or archive with a mismatched extension | Email gateway | T1027 |
| ZIP with trailing data, or data before the first local header | File scan | T1027 |
| Password-protected archive with the password in the message body | Email gateway | T1566.001 |
Counting Directories
The cheapest check is structural: count the EOCD signatures and compare the entry lists from a strict and a lenient parser. Use it in a scanning script, a sandbox pre-filter or a mail-flow hook:
#!/usr/bin/env python3
# Flags ZIP files with multiple EOCD records or parser disagreement.
import sys, zipfile
EOCD = b"PK\x05\x06"
def check(path):
data = open(path, "rb").read()
eocd_count = data.count(EOCD)
findings = []
if eocd_count != 1:
findings.append("eocd_count=%d" % eocd_count)
try:
with zipfile.ZipFile(path) as z:
names = set(z.namelist())
except zipfile.BadZipFile:
findings.append("python_zipfile_rejects")
names = set()
# Re-parse each tail slice split at an EOCD boundary to compare listings.
offsets, start = [], 0
while True:
i = data.find(EOCD, start)
if i < 0:
break
offsets.append(i + 22) # minimal EOCD length; comment length ignored here
start = i + 4
if len(offsets) > 1:
findings.append("segments=%d" % len(offsets))
return findings, names
if __name__ == "__main__":
for p in sys.argv[1:]:
f, n = check(p)
if f:
print(p, f, "entries_seen=%d" % len(n))
# tune/validate: encrypted or self-extracting ZIPs can legitimately have unusual structure.
The same idea works as a content rule at the gateway or on a file store:
rule hunt_zip_multiple_eocd
{
meta:
description = "File beginning with a ZIP local header that contains more than one EOCD record"
note = "hunting rule - validate against known-good archives before blocking"
strings:
$lfh = { 50 4B 03 04 }
$eocd = { 50 4B 05 06 }
condition:
$lfh at 0 and #eocd > 1 and filesize < 50MB
}
Handling the Result
- Quarantine rather than deliver any file with multiple EOCD records, and send a sample to a sandbox that extracts with several tools.
- Compare extracted file sets across parsers, and treat any difference as a high-confidence evasion indicator.
- Cap archive nesting depth and refuse password-protected archives from unknown senders where the business allows, or detonate with the supplied password.
- Watch the endpoint side too: an archive tool writing an executable or script to a user path followed by execution is the same chain described on the malicious attachment page.
Use the PKWARE ZIP application note for the format, your gateway vendor's archive-handling documentation, public YARA collections for archive anomalies and SigmaHQ archive-tool process rules. Baseline your own environment — backup and installer tooling sometimes produces odd ZIP structures.
Build Detections That Fire
Mjolnir Security helps blue teams turn threat intelligence into high-fidelity detections across endpoint, network, identity and cloud telemetry.
- Detection Engineering We build and tune detections mapped to ATT&CK from your own telemetry.
- Threat Hunting as a Service Hypothesis-driven hunts for the behaviours on this page.
- 24/7 Incident Response Containment when a detection fires. Call +1 833 403 5875.
Stay ahead of emerging threats. Get notified when we publish new intelligence reports and advisories.
Subscribe to Alerts