2026-10-24 Pillow CVE-2026-112900 Ghostscript Argument Injection | Sentinel Hunts

Threat Overview

Pillow disclosed CVE-2026-112900 on 17 October 2026, with renewed technical reporting on 23 October. Pillow versions 2.6.0 through 12.x pass a caller-influenced EPS filename to Ghostscript after -f without normalizing the path. Filenames beginning with – may be interpreted as Ghostscript switches; command execution requires a leading @ argument-file filename and attacker control of the referenced argument file. The vulnerability was fixed in Pillow 13.0.0 released 16 October. A proof of concept exists; no verified in-the-wild exploitation was established in reviewed reporting. This is a filename/argument-injection issue, not proof that every crafted EPS image triggers RCE.

References

Pillow GHSA-w889-685w-c8fh, 2026-10-17; updated 2026-10-21

The Hacker News, Pillow image library code execution report, 2026-10-23

Pillow 13.0.0 release, 2026-10-16

Impacted Systems

Vendor/product: Python Pillow PIL.EpsImagePlugin.Ghostscript() with Ghostscript installed. Affected versions: Pillow >=2.6.0 and <13.0.0. Fixed: Pillow 13.0.0 or later (validate downstream distribution backports). Platform/deployment: Linux or Windows Python application servers, image conversion services, upload/thumbnail/document pipelines and containerized workloads that process EPS via Ghostscript. Prerequisites: attacker influence over the filename passed to Pillow; RCE requires an @-prefixed argument-file path and attacker control of the referenced text file. Exposure: externally reachable upload/conversion service or other untrusted filename entry point; no special TCP port intrinsic to Pillow. Unaffected by this specific path: workloads without Ghostscript/EPS processing, and workflows that do not pass attacker-influenced filenames to this API.

Why this matters

Pillow is a widely used transitive Python dependency; application teams may not recognize that EPS processing spawns an external interpreter. The observable Python→Ghostscript→shell chain can provide high-fidelity post-exploitation evidence.

Exploitation Status

Maintainer-confirmed: vulnerable code path, affected version range, fix 13.0.0. Researcher-confirmed: reproducible argument injection and command execution when strict prerequisites are met. Not confirmed: active exploitation against production organizations or published attack infrastructure.

What this hunt looks for

This package defines searches for Sentinel DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, optional CommonSecurityLog with upload filename/URL visibility, and Windows SecurityEvent 4688 fallback. Exact application logs or EPS file contents are not assumed ingested. No customer logs were reviewed or queried during preparation; the sections below describe what would be reviewed where the relevant data source is ingested into Microsoft Sentinel.

Required logs

DeviceProcessEvents; DeviceProcessEvents + DeviceNetworkEvents; DeviceFileEvents; SecurityEvent; CommonSecurityLog

Hunt 01 — Ghostscript Spawns a Shell

Signal: High. Telemetry: DeviceProcessEvents. Strong post-exploitation indicator: gs or gswin child launching shell/command interpreter.

DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where InitiatingProcessFileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe","gswin32.exe","gswin64.exe")
| where FileName in~ ("sh","bash","dash","zsh","cmd.exe","powershell.exe","pwsh.exe","python","python3","python.exe")
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine,InitiatingProcessAccountName
| order by TimeGenerated desc

Hunt 02 — Python-Launched Ghostscript with Argument File

Signal: High. Telemetry: DeviceProcessEvents. Pillow prerequisite pattern: Python parent and Ghostscript command-line argument-file token.

DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe","gswin32.exe","gswin64.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe","pythonw.exe")
| where ProcessCommandLine contains "@" and ProcessCommandLine contains " -f"
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine,AccountName
| order by TimeGenerated desc

Hunt 03 — Ghostscript Unexpected Option Arguments

Signal: Follow-up. Telemetry: DeviceProcessEvents. Follow-up on Ghostscript called by Python with options that could reflect filename injection; tune to legitimate workflows.

DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| where ProcessCommandLine has_any ("-sDEVICE=","-sOutputFile=","-dNOSAFER","-sPermitFileReading=","-sPermitFileWriting=")
| project TimeGenerated,DeviceName,ProcessCommandLine,InitiatingProcessCommandLine,AccountName
| order by TimeGenerated desc

Hunt 04 — Ghostscript Process Makes Public Network Connection

Signal: High. Telemetry: DeviceProcessEvents + DeviceNetworkEvents. Correlate exact Ghostscript process ID and creation time with outbound traffic.

let GS=DeviceProcessEvents
| where TimeGenerated >= ago(14d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| project DeviceId,DeviceName,GSProcessId=ProcessId,GSStart=ProcessCreationTime,GSCommand=ProcessCommandLine,Parent=InitiatingProcessFileName;
DeviceNetworkEvents
| where TimeGenerated >= ago(14d) and RemoteIPType =~ "Public"
| join kind=inner GS on DeviceId,$left.InitiatingProcessId==$right.GSProcessId
| where InitiatingProcessCreationTime==GSStart
| project TimeGenerated,DeviceName,Parent,GSCommand,RemoteIP,RemotePort,RemoteUrl,ActionType
| order by TimeGenerated desc

Hunt 05 — Python-to-Ghostscript First Seen on Host

Signal: Follow-up. Telemetry: DeviceProcessEvents. Baseline discovery: first Python-spawned Ghostscript on a device in last 3 days.

let Old=DeviceProcessEvents
| where TimeGenerated between (ago(30d)..ago(3d))
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| summarize by DeviceId;
DeviceProcessEvents
| where TimeGenerated >= ago(3d)
| where FileName in~ ("gs","gs.exe","gswin32c.exe","gswin64c.exe")
| where InitiatingProcessFileName in~ ("python","python3","python.exe","python3.exe")
| join kind=leftanti Old on DeviceId
| project TimeGenerated,DeviceName,FileName,ProcessCommandLine,InitiatingProcessFileName,InitiatingProcessCommandLine
| order by TimeGenerated desc

Hunt 06 — Suspicious EPS Filename Creation

Signal: Follow-up. Telemetry: DeviceFileEvents. Creation of EPS or argument-file artifacts with option-like names; correlate to known upload directories.

DeviceFileEvents
| where TimeGenerated >= ago(14d)
| where ActionType in~ ("FileCreated","FileRenamed")
| where FileName startswith "@" or (FileName startswith "-" and FileName endswith ".eps")
| project TimeGenerated,DeviceName,ActionType,FileName,FolderPath,InitiatingProcessFileName,InitiatingProcessCommandLine
| order by TimeGenerated desc

Hunt 07 — Windows Native 4688 Python-to-Ghostscript

Signal: Follow-up. Telemetry: SecurityEvent. Fallback when Defender process table absent and SecurityEvent 4688 command lines are available.

SecurityEvent
| where TimeGenerated >= ago(14d) and EventID==4688
| where NewProcessName has_any ("gswin32c.exe","gswin64c.exe","gs.exe")
| where ParentProcessName has_any ("python.exe","pythonw.exe")
| project TimeGenerated,Computer,Account,NewProcessName,ParentProcessName,CommandLine
| order by TimeGenerated desc

Hunt 08 — HTTP Uploads with Option-Like EPS Filenames

Signal: Follow-up. Telemetry: CommonSecurityLog. Only when URL/request logging preserves upload filenames; not a generic EPS signature.

CommonSecurityLog
| where TimeGenerated >= ago(14d)
| extend Url=tostring(column_ifexists("RequestURL","")),Ext=tostring(column_ifexists("AdditionalExtensions","")),Msg=tostring(column_ifexists("Message",""))
| where Url has ".eps" or Ext has ".eps" or Msg has ".eps"
| where Url contains "%40" or Url contains "@" or Ext contains "filename=\"@" or Msg contains "filename=\"@"
| project TimeGenerated,SourceIP,DestinationIP,Url,DeviceVendor,DeviceProduct,DeviceAction,Ext,Msg
| order by TimeGenerated desc

Detection Notes

Highest signal: Ghostscript spawning a shell, and Python→Ghostscript with @ argument-file semantics or unexpected Ghostscript options. Normal image conversion creates Python→Ghostscript, so that ancestry alone is a baseline lead. The literal @ can be absent if telemetry truncates command lines. File creation queries are weak without verified upload directories. WAF logs rarely capture multipart filename parameters; absent matches cannot exclude exploitation. A crafted filename is necessary, but the image body itself need not be malicious.

Leave a Reply