AI Red Teaming Lab 3: MCP Servers — ControlledFS & Mythic C2 — a technical write-up
Lab 3 introduces the Model Context Protocol (MCP) — the standard that lets an LLM client (Claude Desktop, an IDE) discover and call external tools. Exercise 1 builds a defensive MCP server (a sandboxed filesystem), Exercise 2 wires an LLM into the offensive Mythic C2 framework so the model can operate compromised hosts. Together they teach both sides of agentic tooling: how to expose capabilities safely, and what happens when an LLM gets real offensive tools.
Attack path (how the steps chain)
- Learn the tool standard — build the ControlledFS MCP server first: three tools, one sandbox, and the resolve-then-check pattern that keeps an LLM inside its lane.
- Probe the boundary — path-traversal and symlink prompts show what a sandboxed agent blocks, and the confused-deputy discussion shows what it can't.
- Hand the LLM real offensive tools — connect the Mythic C2 MCP server; the model can now see callbacks and task agents.
- Chain the operation —
start_pentestpersona →get_all_agents→ad_recon/run_shell_command→privilege_escalation_peas→execute_mimikatz→run_as_user→run_sharphound/run_kerberoast→ objective (flag on DC01). - Autonomy is the point — one natural-language objective ("emulate APT31, drop the flag on the DC") drives the whole chain without the operator naming a single command.
1. Exercise 1 — ControlledFS-MCP (the defensive side)
1.1 Setup
cd Labs-3/ControlledFS-MCP
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # fastmcp
python server.py # stdio server; normally launched by the client
Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"controlled-fs": {
"command": "/abs/path/to/.venv/bin/python",
"args": ["/abs/path/to/server.py"]
}
}
}
Use the venv's absolute interpreter path — Claude Desktop doesn't inherit your shell's PATH. Restart Claude; the hammer icon should show 3 tools.
1.2 How it works
server.py registers three tools on a FastMCP instance:
list_files, read_file(filename), write_file(filename, content).
The security control is safe_path():
def safe_path(filename: str) -> Path:
path = (SANDBOX / filename).resolve()
if SANDBOX not in path.parents and path != SANDBOX:
raise ValueError("Access denied")
return path
resolve() canonicalizes the path (collapsing .., following symlinks) before the
containment check — so both ../../etc/passwd and a malicious symlink inside the sandbox are caught.
Checking the raw string for ".." would be bypassable; resolve-then-check is the correct pattern.
1.3 Test prompts and expected outputs
- "What files are in the sandbox?" →
list_files→ file listing. - "Read secret.txt" → contents returned.
- "Read ../../../etc/passwd" → tool returns
"Access denied"; the model reports the refusal. - "Create notes.txt containing 'hello'" →
"Written successfully"; verify on disk.
1.4 Discussion points
- Residual weaknesses: no size limits (DoS via huge writes), no auth on the MCP channel, sandbox path is CWD-relative (launching from another directory silently moves the sandbox).
- The LLM is a confused deputy: prompt injection via file contents can steer tool calls — the sandbox is the last line of defense.
- Tool docstrings are the model's "menu" — docstring quality directly determines how well the model uses tools.
2. Exercise 2 — Mythic C2 MCP (the offensive side)
2.1 Setup
# Prereq: a running Mythic server (Docker) + at least one active callback
cd "Labs-3/Mythic_C2 MCP"
uv sync
uv run main.py <mythic_user> <mythic_password> <mythic_host> 7443
Claude Desktop config uses uv --directory ... run main.py user pass host 7443 with the absolute
path to uv. Credentials as CLI positional args are a security smell worth discussing
(visible in process lists).
2.2 The operator's menu
The server exposes 2 prompts and 12 tools. The prompts inject persona/objective:
start_pentest(threat_actor, objective) turns the model into an autonomous operator;
start_recon() frames enumeration. Key tools:
get_all_agents— list active callbacks (ID/host/user).run_shell_command,read_file,execute_powershell(via-EncodedCommand),upload_file.execute_mimikatz— credential dumping;run_as_user— make_token.privilege_escalation_peas— upload + run linPEAS/winPEAS.run_sharphound— BloodHound collection (exe must be pre-staged atC:\Users\Temp\SharpHound.exe).ad_recon— collated AD recon (whoami /all, net user /domain, nltest, net share...).run_kerberoast— uploads Rubeus toC:\Windows\Tempand roasts.
2.3 Example operator prompts
- "List my active agents." → formatted ID/host/user list.
- "Run whoami on agent 1." → shell output wrapped in
---markers. - "Do AD recon on agent 1 and summarize interesting groups." →
ad_recon+ model summary. - Full autonomous scenario: "You are an automated pentester emulating APT31. Objective: add a flag to C:\win.txt on DC01." → the model chains agents → recon → privesc → lateral movement → objective.
2.4 Code insights
- Tools are thin 1:1 wrappers over
mythic.issue_task_and_waitfor_task_output; e.g.read_file→ Mythic'scatcommand. - Errors are swallowed into strings ("Error: Could not execute command...") which keeps the agent loop alive but can make the model hallucinate success — a lesson in designing LLM-facing error contracts.
download_fileexists in the API lib but isn't exposed as a tool — a good extension exercise.- OPSEC notes: predictable staging paths (
C:\Users\Temp,/tmp/linpeas.sh).
3. Troubleshooting
- No tools in Claude → bad path in config, non-absolute interpreter, or JSON syntax error; check
~/Library/Logs/Claude/mcp*.log. - The sample config in
testcontains the lab author's Windows path — replace it wholesale. - Mythic login failures → wrong port (7443), version drift between the
mythicPyPI package and server, or 2FA on the account. - Empty agent list → no active callbacks; detonate a payload first.
- SharpHound/PEAS "file not found" → pre-stage the binary / supply base64 yourself.