Bedside
A medical-AI training platform. The chain is two separate deserialization bugs, bridged by a container that shares the host’s network stack — which is how a service the outside world can’t even reach becomes the way forward.
Spoiler-light notes — no flags, credentials or copy-paste exploits. Solve it yourself first.
Recon
A full TCP scan gives the usual shape: SSH, one web front end, and one port that is filtered rather than closed. Filtered means something is listening and a firewall rule is keeping you out — that’s not a dead end, it’s a note to yourself for later.
The web front end is a small marketing site. Its robots.txt disallows two directories that
don’t exist on that host, which is a strong hint the interesting content lives on a vhost
rather than under a path. Vhost fuzzing finds a second one: a file-upload portal for a
medical imaging pipeline.
Two details on that portal matter:
- The response headers advertise the library doing the server-side conversion. A version number in a header is a gift; even just the library name is enough to start reading.
- The copy says uploads “may be converted to standardized formats” before training. Server-side conversion means a parser is involved — and parsers have bugs.
The interesting part
The box’s theme is unsafe deserialization, twice — once in the document-conversion path, once in the model-loading path.
Leg one: the conversion library resolves a resource referenced by the document and loads it from disk, with the path coming from the document itself instead of being confined to the library’s own data directory. Chain two uploads — your payload file, then a document that points at it — and parsing the document runs your code. That lands you as a service account inside a container.
The container is the pivot. It runs with the host’s network stack, so “localhost” from inside it is the host’s localhost — and the port that was filtered from the outside is sitting right there. Behind it is an internal image viewer built on a module CDN, which again announces its exact version in a header. Its versioned-asset route skips the path canonicalisation the normal route applies, and a couple of query flags switch off both the module transformation and the normalising redirect that would otherwise neutralise a traversal. That reads a file you shouldn’t have: a passwordless private key belonging to a developer on the host.
Worth internalising: curl canonicalises ../ client-side unless you pass --path-as-is.
Without it your traversal is resolved on your own machine and never reaches the target — a
quiet way to conclude “not vulnerable” when you haven’t actually sent the request.
Reaching the user
The container account is deliberately shell-less (nologin) and can’t read the developer’s
home directory — so the key had to come out through the traversal rather than being read
in place. Pull it, drop it on your box, and SSH in as the developer.
Root
sudo -l shows NOPASSWD on a single Python script — the exact command, no wildcard, so
there’s no argument-injection angle. Instead, look at what the script loads: a model
checkpoint, from a hardcoded directory, using the unsafe deserialization flag. That
directory sits on a volume the container service account can write to.
So: build a checkpoint in the container format the loader expects, plant it from the container, and trigger the script as root. The deserialization happens before anything validates the result, so it doesn’t matter that your file isn’t a real model — the payload fires and the “loaded: []” afterwards is just noise.
One operational snag: the sudo rule sets use_pty, so a non-interactive invocation is
refused outright. You have to force PTY allocation on the SSH command itself.
Takeaways
- Two deserialization sinks on one box — neither exotic, both “a dependency doing what it was told”.
- A container with host networking erases a boundary you were relying on. It’s how a filtered port becomes reachable.
- “Filtered” ≠ “closed”. It means something is there.
- Version strings in response headers turn a research task into a lookup.
- Client-side URL canonicalisation will silently defeat path traversal — use
--path-as-is. - Unsafe model loading converts “can write to a data directory” into root.
- Decoy branches are real: the trainer contained an elaborate staging/promotion mechanism that looked like the intended path and wasn’t. Understanding why it wasn’t was most of the work.