Skip to content

Commit 778ba92

Browse files
committed
Merge branch '3078-secure-endpoints' into develop
Issue #3078 PR #3203
2 parents 0e1adcc + 4bd5e5c commit 778ba92

38 files changed

+307
-71
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from http import HTTPStatus
2+
from typing import Dict
3+
4+
import requests
5+
6+
from common.types import OTP, AgentID
7+
8+
from .i_monkey_island_requests import IMonkeyIslandRequests
9+
10+
11+
class InvalidRequestError(Exception):
12+
pass
13+
14+
15+
class AgentRequests(IMonkeyIslandRequests):
16+
def __init__(self, server_address, agent_id: AgentID, otp: OTP):
17+
self.addr = f"https://{server_address}/"
18+
self.token = None
19+
self.agent_id = agent_id
20+
self.otp = otp
21+
22+
def login(self):
23+
self.token = self._try_register_otp()
24+
25+
def _try_register_otp(self):
26+
resp = requests.post( # noqa: DUO123
27+
self.addr + "api/agent-otp-login",
28+
json={"agent_id": str(self.agent_id), "otp": self.otp.get_secret_value()},
29+
verify=False,
30+
)
31+
if resp.status_code == HTTPStatus.CONFLICT:
32+
# A user has already been registered
33+
return self.get_token_from_server()
34+
35+
if resp.status_code == HTTPStatus.BAD_REQUEST:
36+
raise InvalidRequestError()
37+
38+
token = resp.json()["response"]["user"]["authentication_token"]
39+
return token
40+
41+
def get_token_from_server(self):
42+
return self.token
43+
44+
def get(self, url, data=None):
45+
return requests.get( # noqa: DUO123
46+
self.addr + url,
47+
headers=self.get_auth_header(),
48+
params=data,
49+
verify=False,
50+
)
51+
52+
def post(self, url, data):
53+
return requests.post( # noqa: DUO123
54+
self.addr + url, data=data, headers=self.get_auth_header(), verify=False
55+
)
56+
57+
def put(self, url, data):
58+
return requests.put( # noqa: DUO123
59+
self.addr + url, data=data, headers=self.get_auth_header(), verify=False
60+
)
61+
62+
def put_json(self, url, json: Dict):
63+
return requests.put( # noqa: DUO123
64+
self.addr + url, json=json, headers=self.get_auth_header(), verify=False
65+
)
66+
67+
def post_json(self, url, json: Dict):
68+
return requests.post( # noqa: DUO123
69+
self.addr + url, json=json, headers=self.get_auth_header(), verify=False
70+
)
71+
72+
def patch(self, url, data: Dict):
73+
return requests.patch( # noqa: DUO123
74+
self.addr + url, data=data, headers=self.get_auth_header(), verify=False
75+
)
76+
77+
def delete(self, url):
78+
return requests.delete( # noqa: DUO123
79+
self.addr + url, headers=self.get_auth_header(), verify=False
80+
)
81+
82+
def get_auth_header(self):
83+
return {"Authentication-Token": self.token}

envs/monkey_zoo/blackbox/test_blackbox.py

+102-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
import pytest
88
import requests
99

10+
from common.types import OTP
1011
from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import CommunicationAnalyzer
1112
from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer
13+
from envs.monkey_zoo.blackbox.island_client.agent_requests import AgentRequests
1214
from envs.monkey_zoo.blackbox.island_client.i_monkey_island_requests import IMonkeyIslandRequests
1315
from envs.monkey_zoo.blackbox.island_client.monkey_island_client import (
16+
GET_AGENT_EVENTS_ENDPOINT,
1417
GET_AGENTS_ENDPOINT,
1518
GET_MACHINES_ENDPOINT,
1619
ISLAND_LOG_ENDPOINT,
@@ -93,8 +96,8 @@ def monkey_island_requests(island) -> IMonkeyIslandRequests:
9396
def island_client(monkey_island_requests):
9497
client_established = False
9598
try:
96-
requests = ReauthorizingMonkeyIslandRequests(monkey_island_requests)
97-
island_client_object = MonkeyIslandClient(requests)
99+
reauthorizing_island_requests = ReauthorizingMonkeyIslandRequests(monkey_island_requests)
100+
island_client_object = MonkeyIslandClient(reauthorizing_island_requests)
98101
client_established = island_client_object.get_api_status()
99102
except Exception:
100103
logging.exception("Got an exception while trying to establish connection to the Island.")
@@ -186,6 +189,103 @@ def make_request():
186189
assert response_codes.count(HTTPStatus.TOO_MANY_REQUESTS) == 1
187190

188191

192+
UUID = "00000000-0000-0000-0000-000000000000"
193+
AGENT_BINARIES_ENDPOINT = "/api/agent-binaries/os"
194+
AGENT_EVENTS_ENDPOINT = "/api/agent-events"
195+
AGENT_HEARTBEAT_ENDPOINT = f"/api/agent/{UUID}/heartbeat"
196+
PUT_LOG_ENDPOINT = f"/api/agent-logs/{UUID}"
197+
GET_AGENT_PLUGINS_ENDPOINT = "/api/agent-plugins/host/type/name"
198+
GET_AGENT_SIGNALS_ENDPOINT = f"/api/agent-signals/{UUID}"
199+
200+
201+
def test_island__cannot_access_nonisland_endpoints(island):
202+
island_requests = MonkeyIslandRequests(island)
203+
island_requests.login()
204+
205+
assert island_requests.get(AGENT_BINARIES_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
206+
assert (
207+
island_requests.post(AGENT_EVENTS_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
208+
)
209+
assert (
210+
island_requests.post(AGENT_HEARTBEAT_ENDPOINT, data=None).status_code
211+
== HTTPStatus.FORBIDDEN
212+
)
213+
assert island_requests.put(PUT_LOG_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
214+
assert island_requests.get(GET_AGENT_PLUGINS_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
215+
assert (
216+
island_requests.get("/api/agent-plugins/plugin-type/plugin-name/manifest").status_code
217+
== HTTPStatus.FORBIDDEN
218+
)
219+
assert island_requests.get(GET_AGENT_SIGNALS_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
220+
assert island_requests.post(GET_AGENTS_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
221+
222+
223+
GET_AGENT_OTP_ENDPOINT = "/api/agent-otp"
224+
REQUESTS_AGENT_ID = "00000000-0000-0000-0000-000000000001"
225+
TERMINATE_AGENTS_ENDPOINT = "/api/agent-signals/terminate-all-agents"
226+
CLEAR_SIMULATION_DATA_ENDPOINT = "/api/clear-simulation-data"
227+
MONKEY_EXPLOITATION_ENDPOINT = "/api/exploitations/monkey"
228+
GET_ISLAND_LOG_ENDPOINT = "/api/island/log"
229+
ISLAND_MODE_ENDPOINT = "/api/island/mode"
230+
ISLAND_RUN_ENDPOINT = "/api/local-monkey"
231+
GET_NODES_ENDPOINT = "/api/nodes"
232+
PROPAGATION_CREDENTIALS_ENDPOINT = "/api/propagation-credentials"
233+
GET_RANSOMWARE_REPORT_ENDPOINT = "/api/report/ransomware"
234+
REMOTE_RUN_ENDPOINT = "/api/remote-monkey"
235+
GET_REPORT_STATUS_ENDPOINT = "/api/report-generation-status"
236+
RESET_AGENT_CONFIG_ENDPOINT = "/api/reset-agent-configuration"
237+
GET_SECURITY_REPORT_ENDPOINT = "/api/report/security"
238+
GET_ISLAND_VERSION_ENDPOINT = "/api/island/version"
239+
PUT_AGENT_CONFIG_ENDPOINT = "/api/agent-configuration"
240+
241+
242+
def test_agent__cannot_access_nonagent_endpoints(island):
243+
island_requests = MonkeyIslandRequests(island)
244+
island_requests.login()
245+
response = island_requests.get(GET_AGENT_OTP_ENDPOINT)
246+
print(f"response: {response.json()}")
247+
otp = response.json()["otp"]
248+
249+
agent_requests = AgentRequests(island, REQUESTS_AGENT_ID, OTP(otp))
250+
agent_requests.login()
251+
252+
assert agent_requests.get(GET_AGENT_EVENTS_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
253+
assert agent_requests.get(PUT_LOG_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
254+
assert (
255+
agent_requests.post(TERMINATE_AGENTS_ENDPOINT, data=None).status_code
256+
== HTTPStatus.FORBIDDEN
257+
)
258+
assert agent_requests.get(GET_AGENTS_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
259+
assert (
260+
agent_requests.post(CLEAR_SIMULATION_DATA_ENDPOINT, data=None).status_code
261+
== HTTPStatus.FORBIDDEN
262+
)
263+
assert agent_requests.get(MONKEY_EXPLOITATION_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
264+
assert agent_requests.get(GET_ISLAND_LOG_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
265+
assert agent_requests.get(ISLAND_MODE_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
266+
assert agent_requests.put(ISLAND_MODE_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
267+
assert agent_requests.post(ISLAND_RUN_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
268+
assert agent_requests.get(GET_MACHINES_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
269+
assert agent_requests.get(GET_NODES_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
270+
assert (
271+
agent_requests.put(PROPAGATION_CREDENTIALS_ENDPOINT, data=None).status_code
272+
== HTTPStatus.FORBIDDEN
273+
)
274+
assert agent_requests.get(GET_RANSOMWARE_REPORT_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
275+
assert agent_requests.get(REMOTE_RUN_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
276+
assert agent_requests.post(REMOTE_RUN_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
277+
assert agent_requests.get(GET_REPORT_STATUS_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
278+
assert (
279+
agent_requests.post(RESET_AGENT_CONFIG_ENDPOINT, data=None).status_code
280+
== HTTPStatus.FORBIDDEN
281+
)
282+
assert agent_requests.get(GET_SECURITY_REPORT_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
283+
assert agent_requests.get(GET_ISLAND_VERSION_ENDPOINT).status_code == HTTPStatus.FORBIDDEN
284+
assert (
285+
agent_requests.put(PUT_AGENT_CONFIG_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
286+
)
287+
288+
189289
# NOTE: These test methods are ordered to give time for the slower zoo machines
190290
# to boot up and finish starting services.
191291
# noinspection PyUnresolvedReferences

monkey/monkey_island/cc/resources/agent_binaries.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class AgentBinaries(AbstractResource):
1515
def __init__(self, agent_binary_repository: IAgentBinaryRepository):
1616
self._agent_binary_repository = agent_binary_repository
1717

18-
# Used by monkey. can't secure.
18+
# Can't be secured, used in manual run commands
1919
def get(self, os):
2020
"""
2121
Gets the agent binary for the specified OS

monkey/monkey_island/cc/resources/agent_events.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from typing import Iterable, Optional, Sequence, Tuple, Type
44

55
from flask import request
6+
from flask_security import auth_token_required, roles_accepted
67

78
from common.agent_event_serializers import EVENT_TYPE_FIELD, AgentEventSerializerRegistry
89
from common.agent_events import AbstractAgentEvent, AgentEventRegistry
910
from common.event_queue import IAgentEventQueue
1011
from common.types import JSONSerializable
1112
from monkey_island.cc.flask_utils import AbstractResource
1213
from monkey_island.cc.repositories import IAgentEventRepository
14+
from monkey_island.cc.services.authentication_service import AccountRole
1315

1416
logger = logging.getLogger(__name__)
1517

@@ -29,7 +31,8 @@ def __init__(
2931
self._agent_event_repository = agent_event_repository
3032
self._agent_event_registry = agent_event_registry
3133

32-
# Agents need this. Can't secure.
34+
@auth_token_required
35+
@roles_accepted(AccountRole.AGENT.name)
3336
def post(self):
3437
events = request.json
3538

@@ -45,6 +48,8 @@ def post(self):
4548

4649
return {}, HTTPStatus.NO_CONTENT
4750

51+
@auth_token_required
52+
@roles_accepted(AccountRole.ISLAND_INTERFACE.name)
4853
def get(self):
4954
try:
5055
type_, success = self._parse_event_filter_args()

monkey/monkey_island/cc/resources/agent_heartbeat.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
from json import JSONDecodeError
33

44
from flask import request
5+
from flask_security import auth_token_required, roles_accepted
56

67
from common import AgentHeartbeat as AgentHeartbeatObject
78
from common.types import AgentID
89
from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic
910
from monkey_island.cc.flask_utils import AbstractResource
11+
from monkey_island.cc.services.authentication_service import AccountRole
1012

1113

1214
class AgentHeartbeat(AbstractResource):
@@ -15,7 +17,8 @@ class AgentHeartbeat(AbstractResource):
1517
def __init__(self, island_event_queue: IIslandEventQueue):
1618
self._island_event_queue = island_event_queue
1719

18-
# Used by the agent. Can't secure.
20+
@auth_token_required
21+
@roles_accepted(AccountRole.AGENT.name)
1922
def post(self, agent_id: AgentID):
2023
try:
2124
heartbeat = AgentHeartbeatObject(**request.json)

monkey/monkey_island/cc/resources/agent_logs.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from http import HTTPStatus
33

44
from flask import request
5-
from flask_security import auth_token_required, roles_required
5+
from flask_security import auth_token_required, roles_accepted
66

77
from common.types import AgentID
88
from monkey_island.cc.flask_utils import AbstractResource
@@ -19,7 +19,7 @@ def __init__(self, agent_log_repository: IAgentLogRepository):
1919
self._agent_log_repository = agent_log_repository
2020

2121
@auth_token_required
22-
@roles_required(AccountRole.ISLAND_INTERFACE.name)
22+
@roles_accepted(AccountRole.ISLAND_INTERFACE.name)
2323
def get(self, agent_id: AgentID):
2424
try:
2525
log_contents = self._agent_log_repository.get_agent_log(agent_id)
@@ -29,6 +29,8 @@ def get(self, agent_id: AgentID):
2929

3030
return log_contents, HTTPStatus.OK
3131

32+
@auth_token_required
33+
@roles_accepted(AccountRole.AGENT.name)
3234
def put(self, agent_id: AgentID):
3335
log_contents = request.json
3436

monkey/monkey_island/cc/resources/agent_plugins.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
from http import HTTPStatus
33

44
from flask import make_response
5+
from flask_security import auth_token_required, roles_accepted
56

67
from common import OperatingSystem
78
from common.agent_plugins import AgentPluginType
89
from monkey_island.cc.flask_utils import AbstractResource
910
from monkey_island.cc.repositories import IAgentPluginRepository, UnknownRecordError
11+
from monkey_island.cc.services.authentication_service import AccountRole
1012

1113
logger = logging.getLogger(__name__)
1214

@@ -17,7 +19,8 @@ class AgentPlugins(AbstractResource):
1719
def __init__(self, agent_plugin_repository: IAgentPluginRepository):
1820
self._agent_plugin_repository = agent_plugin_repository
1921

20-
# Used by monkey. can't secure.
22+
@auth_token_required
23+
@roles_accepted(AccountRole.AGENT.name)
2124
def get(self, host_os: str, plugin_type: str, name: str):
2225
"""
2326
Get the plugin of the specified operating system, type and name.

monkey/monkey_island/cc/resources/agent_plugins_manifest.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from http import HTTPStatus
33

44
from flask import make_response
5+
from flask_security import auth_token_required, roles_accepted
56

67
from common import HARD_CODED_EXPLOITER_MANIFESTS
78
from common.agent_plugins import AgentPluginManifest, AgentPluginType
@@ -13,6 +14,7 @@
1314
)
1415
from monkey_island.cc.flask_utils import AbstractResource
1516
from monkey_island.cc.repositories import IAgentPluginRepository
17+
from monkey_island.cc.services.authentication_service import AccountRole
1618

1719
logger = logging.getLogger(__name__)
1820

@@ -23,7 +25,8 @@ class AgentPluginsManifest(AbstractResource):
2325
def __init__(self, agent_plugin_repository: IAgentPluginRepository):
2426
self._agent_plugin_repository = agent_plugin_repository
2527

26-
# Used by monkey. can't secure.
28+
@auth_token_required
29+
@roles_accepted(AccountRole.AGENT.name)
2730
def get(self, plugin_type: str, name: str):
2831
"""
2932
Get the plugin manifest of the specified type and name.

monkey/monkey_island/cc/resources/agent_signals/agent_signals.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import logging
22
from http import HTTPStatus
33

4+
from flask_security import auth_token_required, roles_accepted
5+
46
from common.types import AgentID
57
from monkey_island.cc.flask_utils import AbstractResource
68
from monkey_island.cc.services import AgentSignalsService
9+
from monkey_island.cc.services.authentication_service import AccountRole
710

811
logger = logging.getLogger(__name__)
912

@@ -17,7 +20,8 @@ def __init__(
1720
):
1821
self._agent_signals_service = agent_signals_service
1922

20-
# Used by Agent. Can't secure.
23+
@auth_token_required
24+
@roles_accepted(AccountRole.AGENT.name)
2125
def get(self, agent_id: AgentID):
2226
agent_signals = self._agent_signals_service.get_signals(agent_id)
2327
return agent_signals.dict(simplify=True), HTTPStatus.OK

monkey/monkey_island/cc/resources/agent_signals/terminate_all_agents.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from json import JSONDecodeError
44

55
from flask import request
6-
from flask_security import auth_token_required, roles_required
6+
from flask_security import auth_token_required, roles_accepted
77

88
from monkey_island.cc.event_queue import IIslandEventQueue, IslandEventTopic
99
from monkey_island.cc.flask_utils import AbstractResource
@@ -23,7 +23,7 @@ def __init__(
2323
self._island_event_queue = island_event_queue
2424

2525
@auth_token_required
26-
@roles_required(AccountRole.ISLAND_INTERFACE.name)
26+
@roles_accepted(AccountRole.ISLAND_INTERFACE.name)
2727
def post(self):
2828
try:
2929
terminate_all_agents = TerminateAllAgentsObject(**request.json)

0 commit comments

Comments
 (0)