ZIP
CONCATENATION
EOCD
PARSER GAP
DetectionBlue TeamSOCOctober 12, 2026•9 min read

Detecting ZIP Concatenation and Archive Evasion

Two archives in one file will be read differently by different tools. Attackers rely on the scanner seeing one and the victim opening the other.

Scroll

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

SignalWhereATT&CK
More than one EOCD signature (PK 05 06) in one fileGateway, file scanT1027
Different file listings from two ZIP parsers for the same fileSandbox, gatewayT1027
Archive nested inside an archive, or archive with a mismatched extensionEmail gatewayT1027
ZIP with trailing data, or data before the first local headerFile scanT1027
Password-protected archive with the password in the message bodyEmail gatewayT1566.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

Vetted sources

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 EngineeringManaged Detection & ResponseThreat HuntingIncident ResponsePurple Teaming
  • 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.
Written by: Mjolnir Threat Research (MTAC)  |  Published: October 12, 2026  |  Last reviewed: October 12, 2026