Attack Box
Overview
The attack box is the machine the agent works from. In a range with a dedicated attacker it is the sample’s sandbox: every sandbox().exec() lands in it, and targets are reachable only over the range network (the agent/target wall; see Range Sandbox). Declaring one is part of the range definition (Guests). An assumed-breach range has no attack box (the agent starts on a declared target host), so nothing on this page applies to it.
This page covers the default image and its tool contract, the boot defaults, custom images, and the provider seam that lets a deployment supply the whole machine externally.
Default Image
A dedicated attack box with no image: boots the catalog’s attack image, pulled automatically at eval init like any catalog name (see Images). It is a kali-class toolset built by a pinned recipe over the ubuntu-noble golden, so the operating system is Ubuntu 24.04 with the guest control daemon baked in. The build refuses if anything beyond loopback is left listening, which is what the attacker-surface sweep in Security expects of the box.
The tool contract is asserted at image build, so behavioral checks, which run with no egress, may rely on every name here: nmap, ncat, socat, curl, wget, tcpdump, sqlmap, hydra, john, gobuster, ffuf, smbclient, ldapsearch, dig, nc, tmux, msfconsole, msfvenom, the impacket scripts psexec.py, wmiexec.py, secretsdump.py, GetUserSPNs.py, raiseChild.py, lookupsid.py, atexec.py, smbexec.py, and mssqlclient.py, plus impacket and requests importable from python3 and metasploit at major version 6. A missing tool fails the image build, so an eval never sees one.
/etc/attack-image.manifest inside the image records every package and version the feeds delivered. Read it from a pulled copy without booting anything: find the cached file with inspect-ranges images list, then run virt-cat -a <file> /etc/attack-image.manifest (virt-cat installs with libguestfs-tools, which doctor already requires).
Boot Defaults
- Name:
attackerunless the definition declares one; this is the guest name exec targets and ACL endpoints use. - VMM: Cloud Hypervisor, so the most adversarial guest gets the smaller device model (unprivileged per-guest uid, seccomp, credential-checked hybrid vsock; see Security). Declare
vmm: qemuto opt out. - Sizing: 1 cpu and 1024 MB unless
resources:declares otherwise. - Execution identity: commands run as the unprivileged
agentuser (home/home/agent);user=switches, with the environment semantics described in Range Sandbox. - Egress: whatever the definition declares; the shipped examples use
none. See Networks.
Custom Images
Declare image: on the attacker to boot any image instead: another catalog name, or your own golden. The one hard requirement is the guest control daemon, which carries exec, files, and readiness. Derive your image over a published golden, or bake the daemon in with the daemon-bundle artifact (see Images). The tool contract above is a property of the attack image, not of attack boxes generally: behavioral checks that rely on it should keep the default image or assert their own tooling.
External Providers
By default the realizer renders and boots the attack box like any guest. The provider seam lets a deployment substitute an externally-managed VM for that one guest: the range supplies an attachment point on the foothold’s segment and routes sandbox() operations to the surface the provider returns; the provider supplies the machine and owns its lifecycle. Everything else in the range (targets, routers, networking, scoring, evidence capture) is untouched.
The deployment selects the provider, not the range definition: range.yaml has no provider field, and the same published bundle realizes the same way with or without one. The seam covers dedicated attack boxes only; an assumed-breach range’s foothold is a built, verified target host, so the seam does not apply to it.
INSPECT_RANGES_ATTACK_BOX=glovebox_ranges.provider.factory:cell-pool-a \
inspect eval my_task.pyThe variable is read where realization runs: the eval process under the local host backend, the applier’s own environment under a separated one (see Range Host).
Registering a Provider
Registration works like INSPECT_RANGES_HOST: the value <module>.<factory>[:<config>] names an AttackBoxFactory, imported and called at resolution, and the value splits on the first colon, so a config may itself contain colons. The same value persists as the project’s attack-box origin, so any later process (inspect-ranges down, the reaper) can resolve teardown from disk; the provider’s package just has to be installed where that process runs. Factories must be cheap and must not dial anything; remote work belongs in the provider’s boot.
The factory returns an AttackBoxBackend bundle:
from inspect_ranges.attackbox import (
AttackBoxBackend, AttackBoxContext, AttackBoxHandle,
FootholdAttachment, HybridEndpoint,
)
def factory(ctx: AttackBoxContext) -> AttackBoxBackend:
return AttackBoxBackend(
provider=CellProvider(ctx.config, ctx.state_dir),
doctor_probe=lambda: None, # None when healthy, else detail
)Provider Contract
boot(attachment) receives a FootholdAttachment and returns an AttackBoxHandle; teardown(project, reclaim_token) must be idempotent and tolerate a token from a half-completed boot.
Attachment in. The realizer creates one tap per attacker interface inside the range container’s network namespace and hands over the open fds; it never creates a bridge or veth leg in the host namespace, which must stay free of range state (Security). Each AttachedInterface carries the tap fd plus the facts the box must present on that segment: the MAC and address the range’s ACLs and scoring treat as this guest. The provider owns the fds from the moment boot is called: pass them to the VMM, close them at teardown. The taps are non-persistent (they die with the last fd, and with the range container regardless) and carry IFF_VNET_HDR, matching what QEMU and Cloud Hypervisor request when creating taps themselves.
Exec surface out. AttackBoxHandle.exec_endpoint is a unix socket plus the uid its peer must have (SO_PEERCRED-checked at every dial), serving the Cloud Hypervisor hybrid-vsock shape: the CONNECT <port> / OK <port> preamble, then one guest-control request per connection in the daemon wire protocol. The protocol includes the exec witness (exec_events), and the witness is part of the bar: the evidence poller reads the box’s process timeline through this surface every two seconds, and the battery requires the stream to work (a typed ENOSYS/EPROTO refusal is fine for ordinary guests but fails attackbox verify). The simplest conformant implementation is the standard in-guest daemon; a shim passes exactly when the battery passes it. Readiness at boot is daemon liveness: the realizer pings the surface and fails the sample if it never answers.
Lifecycle. The realizer writes the attack-box record before boot runs and completes it (reclaim token, exec socket) after. inspect-ranges down resolves the origin and reclaims the provider’s box before touching any range resource, so the box never outlives its NIC; an origin that does not resolve refuses the teardown rather than stranding a live VM.
Evidence and Doctor
The provider’s box produces no console stream, and the loss is recorded: evidence capture writes one gap record (console-external-attack-box) on console/<guest> and never polls the guest, so the absence is visible offline. Segment pcap and beacon corroboration are unaffected, since capture is bridge-side and the foreign box’s frames cross the same bridges. The exec witness is different: it rides the exec surface the provider supplies, so a conformant box ships its exec/<guest> process timeline like any native guest. Declaring console_stream=True is refused (there is no ingest path).
inspect-ranges doctor gains an Attack box group: resolution of the configured value, the bundle’s own reachability probe, and a standing warning naming the console gap and the containment caveat below.
attackbox verify
A provider is ready when the conformance battery passes. The battery realizes a range through the registered provider and reports every obligation as a row:
INSPECT_RANGES_ATTACK_BOX=their.pkg.factory:cfg \
inspect-ranges attackbox verify --spec dmz_pivot/range.yaml- capabilities: the bundle declares honestly (
console_stream=False). - realize: attach, boot, and strict readiness through the real
up. - foothold-reach: the box holds its reserved address, probed from the shared-segment defender before the sweep.
- attacker-surface: the agent-containment negative checks against the foreign box: a shared-segment defender sweeping the attacker finds zero answers, hypervisor endpoints answer exactly their declared service, reserved segment addresses stay silent.
- session-stability: the exec surface pins one daemon session across dials.
- exec-parity: inspect-ai’s full sandbox
self_checksuite over the surface, with an empty xfail pin; exit codes, truncation, budgets,user=, and timeout kills must all behave. - exec-witness: a marker command run through the surface appears in the box’s
exec/<guest>stream with honest cursor semantics; an always-empty ring fails, and so does a typed refusal. - teardown:
downreclaims the box before the netns, clears the record, re-dial refuses, and a seconddownis a no-op.
--keep leaves the range running for inspection (teardown obligations are skipped and the final down command printed); --json emits the report machine-readably; a failing row exits nonzero. The canonical battery spec is dmz_pivot (a dedicated attack box with a Linux defender sharing its segment, so every obligation engages); any spec with that shape works.
Containment Caveat
A range with an external attack box is only as contained as the external stack: the battery proves behavior at a point in time, not architecture, and the exec surface’s operator controls its implementation. The trust position matches root on a compromised target, and the record says so (the doctor warning, the declared evidence gap). For runs where the realizer’s own containment stack is the requirement, leave the variable unset.