79 Gadgets Hidden in a Single Deserialization Flaw: CVE-2026-25632 - Maze

Back to Resources
April 14, 2026 Security

79 Gadgets Hidden in a Single Deserialization Flaw: CVE-2026-25632

NL

NUNO LOPES

We're constantly trying to push the boundaries of what AI can do for defense. And to understand the best defense, you often have to understand how the offense works. That's what we set out to do.

CVE-2026-25632 is a critical deserialization vulnerability in EPyT-Flow, a Python toolkit for simulating water distribution networks. We started here on purpose. It had no public proof-of-concept and predated current AI training data, making it a clean test of whether AI could reason about exploitation from first principles.

The original advisory, reported by @syphonetic, identified the unsafe __type__ field and flagged subprocess.run as the gadget. CVSS 10.0. Fixed in version 0.16.1.

That report was accurate, but it was also just the surface.

Using Claude, we mapped the entire Python standard library for callables that could be weaponized through the same primitive. We found 79 gadgets, not one. Twelve give you remote code execution. The CVE already gives full RCE. More gadgets don't make it harder to patch, but they make it much harder to secure if you don't. Twenty-five give you SSRF. And the deployment environment, water network simulation infrastructure, puts SCADA systems one network hop away.

More importantly, we discovered two previously unknown bypasses in the OWASP Core Rule Set. We responsibly disclosed both, worked with the CRS maintainers on fixes, and waited for those fixes to ship before publishing.

This post documents the full attack surface we mapped, the CRS bypasses we disclosed, and how defenders should be thinking about this class of vulnerability.

This research was part of a broader project on how AI accelerates exploit development. Claude built the proof-of-concept exploits, the vulnerable Docker lab environment, and identified the WAF bypass techniques. Read more about how AI is changing the speed of exploit development here.

What is EPyT-Flow?

EPyT-Flow is a Python toolkit for water distribution network simulation, published on PyPI and maintained by the WaterFutures research group. It's used in utility planning, infrastructure research, and operational technology environments. The project is peer-reviewed (published in JOSS), actively maintained with 27 releases since May 2024, and exposes a REST API for external application integration.

That REST API is where the problem lives.

The OT context matters here. An EPyT-Flow application server typically sits on a network segment with access to SCADA systems. Modbus, DNP3, and OPC-UA services are one hop away. An attacker who can make that server open a network connection to the wrong place doesn't need remote code execution to cause damage. They just need SSRF.

How it works

The vulnerable code lives in epyt_flow/serialization.py. When the REST API deserializes a JSON body, it looks for a __type__ key and does this:

type_info = data["__type__"]                    # e.g. ["subprocess", "run"]
module = importlib.import_module(type_info[0])  # imports subprocess
cls = getattr(module, type_info[1])             # gets subprocess.run
result = cls(**remaining_kwargs)                # calls it with the rest of the JSON keys

An attacker controls which module to import, which callable to invoke, and which keyword arguments to pass. All from a single JSON field. It's an arbitrary constructor call primitive: any Python callable that accepts **kwargs is fair game.

No authentication required, no multi-step chain, just one JSON field.

This was honestly pretty easy

The first exploit writes itself once you see the __type__ pattern:

{"__type__": ["subprocess", "run"], "args": ["id"], "capture_output": true}

Send that to the API endpoint. Result:

uid=0(root) gid=0(root) groups=0(root)

That's full remote code execution, running as root, from a single HTTP request.

Any engineer reading the source code would get here. This isn't the interesting part. The interesting part is what happens when you stop looking at just subprocess.run and start asking: what else can we call?

The full attack surface: 79 gadgets

subprocess.run is the path everyone would reach for first. It's also the path the original advisory already documented, and it already gives you full RCE. But the __type__ primitive doesn't care what you call. It imports any module, grabs any attribute, and invokes it with your keyword arguments. So the real question isn't how many paths exist. It's how many survive your defenses.

We built a scanner to find out. 51 stdlib modules. 1,156 callables tested through a four-phase enumeration pipeline. Each callable classified not by its name or module, but by what an attacker can actually achieve with it.

The scanner was built with Claude using AI-assisted heuristics. Read more about how we used AI to accelerate this research here.

CategoryCountWhat it meansExamples
SSRF25Force the server to open a network connection to any reachable hosthttp.client.HTTPConnection, smtplib.SMTP, ftplib.FTP, logging.handlers.SocketHandler
FILE_WRITE18Create or move files on the server's filesystemlogging.FileHandler, sqlite3.connect, shutil.copy, os.rename
RCE12Execute arbitrary commandssubprocess.run, subprocess.Popen, os.popen, imaplib.IMAP4_stream
FILE_READ10Read files from the serverbuiltins.open, zipfile.ZipFile, tarfile.open, gzip.open
INFO7Leak system informationshutil.disk_usage
FILE_DELETE5Delete filesshutil.rmtree, os.remove, os.unlink
LIB_LOAD2Load a native .so/.dll libraryctypes.CDLL, ctypes.PyDLL

79 total from scanner output. 16 live-verified after manual curation, at least one per category. All paths are pre-authentication.

A few things worth noting: the FILE_WRITE paths (logging.FileHandler, sqlite3.connect) create attacker-named files but don't write attacker-controlled content, making them useful for staging a path but not for writing a web shell directly. shutil.copy is the exception: it copies an existing file to an attacker-chosen destination, which is useful for moving something into a web root or an executable path.

The Ones That Surprised Us

imaplib.IMAP4_stream is Python's IMAP-over-subprocess client. It opens an IMAP connection by spawning a process, and its command parameter feeds directly into subprocess.Popen. Call it through the deserializer and you get RCE with zero mention of "subprocess" anywhere in your payload. A hidden execution path inside a mail protocol library.

ctypes.CDLL and shelve.open don't touch subprocess at all. ctypes.CDLL loads a native .so file and runs its initialization code. shelve.open deserializes a Python pickle file, which executes arbitrary Python on load. Both need a prior FILE_WRITE step to stage the malicious file first (use shutil.copy to move it into place), then invoke the loader. Two-step chains, no subprocess involved.

Why This Matters for Defenders

Your first instinct might be to blocklist subprocess. That eliminates 7 of the 12 RCE paths. It eliminates zero of the 25 SSRF paths. This is why the gadget mapping matters: not because 79 is a scarier number than 1, but because it shows the defensive gap.

The SSRF paths, smtplib.SMTP, ftplib.FTP, logging.handlers.SocketHandler, and 22 others, contain no dangerous-sounding keywords. No signature-based rule will flag "smtplib" as malicious. But on a network where SCADA controllers are reachable, making the server open an outbound connection to an internal host is often all an attacker needs.

And 79 is the stdlib floor. A deployment with third-party packages installed has a larger surface.

How it was fixed

The EPyT-Flow maintainers addressed CVE-2026-25632 in version 0.16.1 by removing the __type__ dispatch entirely. One patch, all 79 gadgets eliminated at once.

For the CRS bypasses we found, we worked directly with the OWASP Core Rule Set maintainers:

CRS 932236: The maintainers added uconv and piconv to the command blocklist in PR #4544, merged March 13, 2026. This bypass is fixed in CRS versions after that date.

CRS 932200: The maintainers confirmed the glob regex blind spot but decided not to patch at PL2, as the existing PL3 rule 932190 already covers the pattern.

If you're running EPyT-Flow, upgrade to 0.16.1 immediately. If upgrading isn't immediate, allowlist valid __type__ values at the application layer.

Do not rely on WAF rules alone. We achieved a full bypass of ModSecurity 3.0.14 + OWASP CRS v4.24.0 at Paranoia Level 2 with custom deserialization rules. Read more about the full WAF bypass methodology here.

Our disclosure: two novel OWASP CRS bypasses

The CVE itself was reported by @syphonetic. Our original contribution is what we found during the research: two previously unknown gaps in the OWASP Core Rule Set, the most widely used open-source WAF ruleset.

We reported both to the CRS project, worked with their maintainers on fixes, and waited until the fixes shipped before publishing this post.

CRS 932236 (Issue #4542): The PL2 command blocklist was missing uconv and piconv, both of which can read and output file contents like cat. An attacker could use either command to exfiltrate data through the WAF without triggering any detection rules. Confirmed by CRS maintainers and fixed in PR #4544, merged March 13, 2026.

CRS 932200 (Issue #4543): A regex blind spot in glob detection. The rule's lazy quantifier fails to detect glob characters at the start of filenames (/etc/?asswd bypasses detection while /etc/passw? is caught). This isn't a regex bug. It's a fundamental limitation of syntactic pattern matching: the rule was written to catch globs at the end of path components and never anticipated one at the start. Confirmed by CRS maintainers. Accepted without a PL2 fix, as PL3 rule 932190 already covers the pattern.

Read more about the WAF bypass chain and how AI reasoned through each defensive layer here.

Disclosure timeline

DateWhoEvent
February 4, 2026EPyT-Flow maintainersVersion 0.16.1 released with fix
March 3, 2026Maze (Nuno Lopes)Attack surface mapping completed: 79 exploitation paths identified, two CRS bypasses discovered
March 11, 2026Maze (Nuno Lopes)CRS issues #4542 and #4543 reported to OWASP
March 13, 2026OWASP CRS maintainersPR #4544 merged (uconv/piconv added to 932236)
April 14, 2026MazePublic disclosure
April 14, 2026 Security
How AI Changes the Speed of Public Exploits
Read more
April 9, 2026 Product
A Fix Without an Owner Is Just a Suggestion
Read more
March 12, 2026 Product
Exploitability: The Fastest Way to Fewer False Positives
Read more
February 25, 2026 Product
AI Remediation Developers Actually Want to Use
Read more
January 20, 2026 Security
2025: The Year Vulnerabilities Broke Every Record
Read more
January 19, 2026 Product
Matt Johansen's First Look at Maze
Read more
January 15, 2026 Product
Maze Data Sheet
Read more
January 5, 2026 Security
Vulnerability Déjà Vu: Why the Same Bug Keeps Coming Back
Read more
December 29, 2025 Security
The Cross-Platform False Positive Problem: Why Vulnerability Scanners Flag Windows CVEs on Linux
Read more
December 22, 2025 Security
The Language Barrier: Why Security and Engineering Are Never Aligned
Read more
December 4, 2025 Product
An Analyst's Take on Maze: AI That Actually Moves the Needle on Vulnerability Management
Read more
December 4, 2025 Product
Should CISOs Build or Buy?
Read more
November 27, 2025 Security
Checkbox Security - Compliance Driven Security is Bound to Fail
Read more
November 25, 2025 Security
The Hidden Problem With CVSS: The Same CVE Gets Different Scores
Read more
November 12, 2025 Product
Meet Maze: AI Agents That Bring Clarity to Vulnerability Chaos
Read more
October 22, 2025 Company
Maze Named a Cloud Security Segment Leader in the 2025 Latio Cloud Security Report
Read more
August 1, 2025 Security Automation
Why we can't just auto-fix all our vulnerabilities away, yet
Read more
June 26, 2025 Case Studies
AI Vulnerability Analysis in Action: CVE-2025-27363
Read more
June 19, 2025 Product
From Rules to Reasoning: The Shift That Made Maze Possible
Read more
June 12, 2025 Company
The Vulnerability Management Problem
Read more
June 10, 2025 Company
Launching Maze: AI Agents for Vulnerability Management
Read more