fix(c2): append trailing slash to mythic /graphql URL to avoid 301 redirect
Nginx fronting Mythic 3.x redirects /graphql → /graphql/ (HTTP 301). The sprint 8 SSRF defense (allow_redirects=False) is correct and intentional; the fix is the URL, not the redirect policy. Upstream reference: mythic_utilities.get_http_transport hardcodes /graphql/ with trailing slash. Updated _GQL_URL in all three adapter test files to match. Added TestMythicAdapterUrlConstruction regression test to prevent silent regression on future refactors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
# Contract pinned from MythicMeta/Mythic_Scripting master @ 2026-06-10 (raw.githubusercontent.com/MythicMeta/Mythic_Scripting/master/mythic/mythic.py)
|
# Contract pinned from MythicMeta/Mythic_Scripting master @ 2026-06-10 (raw.githubusercontent.com/MythicMeta/Mythic_Scripting/master/mythic/mythic.py)
|
||||||
|
# Sprint 10 correction: /graphql/ MUST have trailing slash (matches mythic_utilities.get_http_transport).
|
||||||
"""Mythic 3.x C2 adapter.
|
"""Mythic 3.x C2 adapter.
|
||||||
|
|
||||||
Transport: POST https://<host>:7443/graphql
|
Transport: POST https://<host>:7443/graphql/
|
||||||
Header: apitoken: <token>
|
Header: apitoken: <token>
|
||||||
Backend: Hasura-proxied Postgres behind nginx.
|
Backend: Hasura-proxied Postgres behind nginx.
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class MythicAdapter(C2Adapter):
|
|||||||
"""Real Mythic 3.x adapter using GraphQL over HTTP."""
|
"""Real Mythic 3.x adapter using GraphQL over HTTP."""
|
||||||
|
|
||||||
def __init__(self, url: str, api_token: str, verify_tls: bool = False) -> None:
|
def __init__(self, url: str, api_token: str, verify_tls: bool = False) -> None:
|
||||||
self._url = url.rstrip("/") + "/graphql"
|
self._url = url.rstrip("/") + "/graphql/"
|
||||||
self._token = api_token
|
self._token = api_token
|
||||||
self._verify = verify_tls
|
self._verify = verify_tls
|
||||||
if not verify_tls:
|
if not verify_tls:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
@@ -143,9 +143,19 @@ class TestMythicAdapterNoRedirects:
|
|||||||
"""Adapter must not follow HTTP redirects (allow_redirects=False)."""
|
"""Adapter must not follow HTTP redirects (allow_redirects=False)."""
|
||||||
with rm_module.Mocker() as m:
|
with rm_module.Mocker() as m:
|
||||||
# Simulate a redirect response; requests-mock won't auto-follow it.
|
# Simulate a redirect response; requests-mock won't auto-follow it.
|
||||||
m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/graphql"})
|
m.post(_GQL_URL, status_code=301, headers={"Location": "https://evil.example/graphql/"})
|
||||||
# With allow_redirects=False the 301 is treated as a non-2xx → raise_for_status raises.
|
# With allow_redirects=False the 301 is treated as a non-2xx → raise_for_status raises.
|
||||||
with pytest.raises(C2Error):
|
with pytest.raises(C2Error):
|
||||||
adapter.list_callbacks()
|
adapter.list_callbacks()
|
||||||
# Exactly one request was made — no follow-up to Location.
|
# Exactly one request was made — no follow-up to Location.
|
||||||
assert len(m.request_history) == 1
|
assert len(m.request_history) == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestMythicAdapterUrlConstruction:
|
||||||
|
def test_mythic_adapter_url_has_trailing_slash(self):
|
||||||
|
a1 = MythicAdapter(url="https://x:7443", api_token="t")
|
||||||
|
assert a1._url == "https://x:7443/graphql/"
|
||||||
|
a2 = MythicAdapter(url="https://x:7443/", api_token="t")
|
||||||
|
assert a2._url == "https://x:7443/graphql/"
|
||||||
|
a3 = MythicAdapter(url="https://x:7443///", api_token="t")
|
||||||
|
assert a3._url == "https://x:7443/graphql/"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from backend.app.services.c2.adapter import C2Error, C2HistoricalTask
|
|||||||
from backend.app.services.c2.mythic import MythicAdapter
|
from backend.app.services.c2.mythic import MythicAdapter
|
||||||
|
|
||||||
_BASE_URL = "https://mythic.lab:7443"
|
_BASE_URL = "https://mythic.lab:7443"
|
||||||
_GQL_URL = _BASE_URL + "/graphql"
|
_GQL_URL = _BASE_URL + "/graphql/"
|
||||||
_TOKEN = "fake-api-token"
|
_TOKEN = "fake-api-token"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user