fix(c2): redteam-friendly verify_tls default (sprint 10) #12

Merged
knacky merged 6 commits from sprint/10-c2-tls-default into main 2026-06-21 19:44:33 +00:00
Owner

Summary

  • Sprint 10 — verify_tls default flip + /graphql trailing slash : deux fixes qui débloquent la première connexion réelle contre un Mythic auto-hébergé.

Fix 1 — verify_tls default

Mimic est un BAS lab tool dont le workflow dominant est un Mythic auto-hébergé avec certificat self-signed. L'ancien default verify_tls=True à chaque couche cassait silencieusement la première connexion Test (CERTIFICATE_VERIFY_FAILED) tant que l'opérateur n'avait pas remarqué la checkbox. Sprint 10 aligne le default sur le workflow réel.

  • Surface chirurgicale : 5 fichiers backend (model, API, factory, adapter, migration), 1 fichier frontend (C2ConfigCard.tsx), 1 ligne SPEC.md.
  • Helper text durci : "Uncheck only for lab Mythic with self-signed certificates. Disabling verification exposes the API token to MITM." — tradeoff sécu spell-out direct, opt-in conscient.
  • Migration 0008 : server_default flippé à sa.false() via op.batch_alter_table (SQLite). Round-trip upgrade → downgrade → upgrade testé. Les lignes existantes ne sont pas mutées (valeur stockée préservée).
  • urllib3 spam suppression : urllib3.disable_warnings(InsecureRequestWarning) gated sur not verify_tls dans MythicAdapter.__init__. Pas de suppression module-level.

Fix 2 — /graphql/graphql/ (HTTP 301 fix)

Bug surface en user testing avec un Mythic réel à https://192.168.10.60:7443 : test_connection retournait HTTP 301. Cause confirmée à 92 % par un workflow diagnostic (lecture adapter + WebFetch source canonique Mythic_Scripting), puis confirmée par l'utilisateur via curl -kv (Location header : /graphql/).

  • Notre adapter sprint 8 a hardcodé "/graphql" sans slash final. Le client officiel mythic_utilities.get_http_transport utilise f"{...}:{port}/graphql/" (slash REQUIS). Nginx Mythic 3.x retourne 301 pour la canonicalisation.
  • Le allow_redirects=False du sprint 8 (défense SSRF intentionnelle, gardée) faisait surface le 301 raw au lieu de le suivre.
  • Fix : 1 caractère dans mythic.py:119 + maj du commentaire de pinning + 3 fichiers de tests + 1 nouveau test de régression.

Reviews : code-review APPROVED + 1 NIT non-bloquant ; design-review APPROVED (0 finding) ; spec-review APPROVED post-amendement (SPEC.md:85 ligne du default mise à jour).

Test plan

  • Backend : 469 / 469 pytest (a grandi de 1 — nouveau test de régression test_mythic_adapter_url_has_trailing_slash). ruff + mypy --strict clean.
  • Frontend : 212 / 212 vitest. tsc --noEmit + eslint --max-warnings=0 clean.
  • Migration round-trip : alembic upgrade headdowngrade -1upgrade head sur SQLite fresh, OK.
  • E2E : non re-joué (mur de credentials Vite proxy persistant). Test manuel ci-dessous.

Comment tester en local

# Avec la branche checkout et MIMIC_ENCRYPTION_KEY set :
export MIMIC_ENCRYPTION_KEY=$(python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())')
export MIMIC_C2_ADAPTER=fake   # pour tester sans Mythic live
make build && make start
make create-admin USER=alice PASS=changeme8   # premier setup
# http://127.0.0.1:5000

Scénarios :

  1. Default vérifié à la création — ouvre un engagement en édition → carte "C2 configuration" → la checkbox "Verify TLS certificate" est décochée par défaut, helper text MITM visible juste en-dessous.
  2. Test connection avec self-signed (cas dominant) — colle l'URL d'un Mythic self-signed https://<lab>:7443, paste un token, laisse la box décochée, SaveTest connection → réponse { ok: true } (FakeAdapter répond toujours OK, en prod-live le cert self-signed n'est plus rejeté). Tail backend stderr : 0 InsecureRequestWarning (suppression engagée).
  3. Verify forced (cas explicite prod) — recoche la box, Save, Test connection contre un cert self-signed → réponse { ok: false, error: "<SSLError CERTIFICATE_VERIFY_FAILED>" }. Prouve que le flag est toujours honoré bout-en-bout.
  4. Migration — sur une DB fresh : alembic upgrade head puis pragma table_info(c2_config) (SQLite) → verify_tls default 0. Puis INSERT INTO c2_config (engagement_id, url, api_token_encrypted) VALUES (1, 'https://x', 'y') → la row aura verify_tls = 0.
  5. Rows existantes intouchées — une row sauvegardée avec verify_tls = 1 AVANT cette migration reste à 1. L'opérateur qui veut basculer doit explicitement re-Save la config avec la box décochée.
  6. RBAC unchanged — un user SOC reçoit toujours 403 sur /api/engagements/<id>/c2-config*. Aucun changement.

Fichiers touchés

Path Change
SPEC.md:85 verify_tls défaut truedéfaut false + justification
backend/app/models/c2_config.py:22 default=Truedefault=False
backend/app/api/c2.py:87 fallback TrueFalse
backend/app/services/c2/factory.py:9 verify_tls: bool = TrueFalse
backend/app/services/c2/mythic.py:18,19,116,122-125 TrueFalse + urllib3 import + disable_warnings(InsecureRequestWarning) gated sur not verify_tls
backend/migrations/versions/0008_c2_verify_tls_default_false.py NEW — server_default flippé via batch_alter_table
backend/tests/test_c2_config.py assertion is False (renamée default_false) ; tests qui PUT verify_tls=True explicitement laissés intacts
frontend/src/components/C2ConfigCard.tsx useState(false) + delete-reset false + helper text MITM en text-graphite mirror FormField hint

Security tradeoff acknowledged

Default sécu-relevant flippé en pleine conscience :

  • Mimic est un BAS / red-team lab tool dont le 99e percentile d'usage est Mythic self-signed.
  • Toggle reste visible et opt-in, helper text spell-out le risque MITM directement.
  • Decision lockée dans tasks/todo.md § Decisions + ce PR body.
  • Plugin security-guidance@claude-code-plugins a flagged 3 fois (HIGH × frontend useState, HIGH × API fallback, MEDIUM × adapter/factory/model/migration). Conditions de "proceed without changes" du plugin elles-mêmes remplies : user a explicitement demandé le flip + tradeoff surfacé dans l'UI.
  • Rows existantes verify_tls=true non mutées par la migration — defense in depth pour qui aurait déjà bouclé sa config en mode strict.

NITs reportés (non-bloquants, polish sprint éventuel)

  • test_c2_config.py:209 — renamer test_get_config_verify_tls_default_false en test_get_config_verify_tls_roundtrip_false (le test PUT explicitement la valeur, ne teste pas le default ORM). Optionnel ; cosmétique.
  • mythic.py:122-125urllib3.disable_warnings est process-global. Alternative cleaner : warnings.catch_warnings() dans _post(). Plus de code pour bénéfice marginal (aucun autre caller urllib3 dans Mimic). Skipped.
  • Symétrie test_migration_0008_c2.py absent vs 0006/0007 ont chacun le leur. 0008 est 10 lignes, round-trip déjà confirmé manuellement, faible risque. Skipped.

Commits

c31f985 docs(sprint-10): plan flip verify_tls default to false (redteam-friendly)
774ae05 fix(frontend): default verify_tls checkbox to unchecked + add helper text
ee5fda6 fix(frontend): make verify_tls helper text spell out the MITM tradeoff
0fab40b feat(backend): default verify_tls to false for lab/redteam Mythic
91533c2 docs(spec): align c2_config.verify_tls default with sprint 10 (false)
208c6e6 fix(c2): append trailing slash to mythic /graphql URL to avoid 301 redirect

🤖 Generated with Claude Code

## Summary - **Sprint 10 — verify_tls default flip + /graphql trailing slash** : deux fixes qui débloquent la première connexion réelle contre un Mythic auto-hébergé. ### Fix 1 — `verify_tls` default Mimic est un **BAS lab tool** dont le workflow dominant est un Mythic auto-hébergé avec certificat **self-signed**. L'ancien default `verify_tls=True` à chaque couche cassait silencieusement la première connexion Test (CERTIFICATE_VERIFY_FAILED) tant que l'opérateur n'avait pas remarqué la checkbox. Sprint 10 aligne le default sur le workflow réel. - **Surface chirurgicale** : 5 fichiers backend (model, API, factory, adapter, migration), 1 fichier frontend (`C2ConfigCard.tsx`), 1 ligne `SPEC.md`. - **Helper text durci** : "Uncheck only for lab Mythic with self-signed certificates. **Disabling verification exposes the API token to MITM.**" — tradeoff sécu spell-out direct, opt-in conscient. - **Migration `0008`** : `server_default` flippé à `sa.false()` via `op.batch_alter_table` (SQLite). Round-trip `upgrade → downgrade → upgrade` testé. Les lignes existantes **ne sont pas mutées** (valeur stockée préservée). - **urllib3 spam suppression** : `urllib3.disable_warnings(InsecureRequestWarning)` gated sur `not verify_tls` dans `MythicAdapter.__init__`. Pas de suppression module-level. ### Fix 2 — `/graphql` → `/graphql/` (HTTP 301 fix) Bug surface en user testing avec un Mythic réel à `https://192.168.10.60:7443` : `test_connection` retournait `HTTP 301`. Cause confirmée à 92 % par un workflow diagnostic (lecture adapter + WebFetch source canonique Mythic_Scripting), puis confirmée par l'utilisateur via `curl -kv` (Location header : `/graphql/`). - Notre adapter sprint 8 a hardcodé `"/graphql"` sans slash final. Le client officiel `mythic_utilities.get_http_transport` utilise `f"{...}:{port}/graphql/"` (slash REQUIS). Nginx Mythic 3.x retourne `301` pour la canonicalisation. - Le `allow_redirects=False` du sprint 8 (défense SSRF intentionnelle, **gardée**) faisait surface le 301 raw au lieu de le suivre. - **Fix** : 1 caractère dans `mythic.py:119` + maj du commentaire de pinning + 3 fichiers de tests + 1 nouveau test de régression. **Reviews** : code-review APPROVED + 1 NIT non-bloquant ; design-review APPROVED (0 finding) ; spec-review APPROVED post-amendement (`SPEC.md:85` ligne du default mise à jour). ## Test plan - **Backend** : **469 / 469** pytest (a grandi de 1 — nouveau test de régression `test_mythic_adapter_url_has_trailing_slash`). `ruff` + `mypy --strict` clean. - **Frontend** : **212 / 212** vitest. `tsc --noEmit` + `eslint --max-warnings=0` clean. - **Migration round-trip** : `alembic upgrade head` → `downgrade -1` → `upgrade head` sur SQLite fresh, OK. - **E2E** : non re-joué (mur de credentials Vite proxy persistant). Test manuel ci-dessous. ## Comment tester en local ```bash # Avec la branche checkout et MIMIC_ENCRYPTION_KEY set : export MIMIC_ENCRYPTION_KEY=$(python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())') export MIMIC_C2_ADAPTER=fake # pour tester sans Mythic live make build && make start make create-admin USER=alice PASS=changeme8 # premier setup # http://127.0.0.1:5000 ``` Scénarios : 1. **Default vérifié à la création** — ouvre un engagement en édition → carte "C2 configuration" → la checkbox **"Verify TLS certificate" est décochée par défaut**, helper text MITM visible juste en-dessous. 2. **Test connection avec self-signed (cas dominant)** — colle l'URL d'un Mythic self-signed `https://<lab>:7443`, paste un token, **laisse la box décochée**, `Save` → `Test connection` → réponse `{ ok: true }` (FakeAdapter répond toujours OK, en prod-live le cert self-signed n'est plus rejeté). Tail backend stderr : **0 `InsecureRequestWarning`** (suppression engagée). 3. **Verify forced (cas explicite prod)** — recoche la box, `Save`, `Test connection` contre un cert self-signed → réponse `{ ok: false, error: "<SSLError CERTIFICATE_VERIFY_FAILED>" }`. Prouve que le flag est toujours honoré bout-en-bout. 4. **Migration** — sur une DB fresh : `alembic upgrade head` puis `pragma table_info(c2_config)` (SQLite) → `verify_tls` default `0`. Puis `INSERT INTO c2_config (engagement_id, url, api_token_encrypted) VALUES (1, 'https://x', 'y')` → la row aura `verify_tls = 0`. 5. **Rows existantes intouchées** — une row sauvegardée avec `verify_tls = 1` AVANT cette migration reste à `1`. L'opérateur qui veut basculer doit explicitement re-Save la config avec la box décochée. 6. **RBAC unchanged** — un user SOC reçoit toujours `403` sur `/api/engagements/<id>/c2-config*`. Aucun changement. ## Fichiers touchés | Path | Change | |---|---| | `SPEC.md:85` | `verify_tls défaut true` → `défaut false` + justification | | `backend/app/models/c2_config.py:22` | `default=True` → `default=False` | | `backend/app/api/c2.py:87` | fallback `True` → `False` | | `backend/app/services/c2/factory.py:9` | `verify_tls: bool = True` → `False` | | `backend/app/services/c2/mythic.py:18,19,116,122-125` | `True` → `False` + `urllib3` import + `disable_warnings(InsecureRequestWarning)` gated sur `not verify_tls` | | `backend/migrations/versions/0008_c2_verify_tls_default_false.py` | NEW — `server_default` flippé via `batch_alter_table` | | `backend/tests/test_c2_config.py` | assertion `is False` (renamée `default_false`) ; tests qui PUT `verify_tls=True` explicitement laissés intacts | | `frontend/src/components/C2ConfigCard.tsx` | `useState(false)` + delete-reset `false` + helper text MITM en `text-graphite` mirror FormField hint | ## Security tradeoff acknowledged Default sécu-relevant flippé en pleine conscience : - Mimic est un BAS / red-team lab tool dont le 99e percentile d'usage est Mythic self-signed. - Toggle reste visible et opt-in, helper text spell-out le risque MITM directement. - Decision lockée dans `tasks/todo.md` § Decisions + ce PR body. - Plugin `security-guidance@claude-code-plugins` a flagged 3 fois (HIGH × frontend useState, HIGH × API fallback, MEDIUM × adapter/factory/model/migration). Conditions de "proceed without changes" du plugin elles-mêmes remplies : user a explicitement demandé le flip + tradeoff surfacé dans l'UI. - Rows existantes `verify_tls=true` **non mutées** par la migration — defense in depth pour qui aurait déjà bouclé sa config en mode strict. ## NITs reportés (non-bloquants, polish sprint éventuel) - `test_c2_config.py:209` — renamer `test_get_config_verify_tls_default_false` en `test_get_config_verify_tls_roundtrip_false` (le test PUT explicitement la valeur, ne teste pas le default ORM). Optionnel ; cosmétique. - `mythic.py:122-125` — `urllib3.disable_warnings` est process-global. Alternative cleaner : `warnings.catch_warnings()` dans `_post()`. Plus de code pour bénéfice marginal (aucun autre caller urllib3 dans Mimic). Skipped. - Symétrie `test_migration_0008_c2.py` absent vs 0006/0007 ont chacun le leur. 0008 est 10 lignes, round-trip déjà confirmé manuellement, faible risque. Skipped. ## Commits ``` c31f985 docs(sprint-10): plan flip verify_tls default to false (redteam-friendly) 774ae05 fix(frontend): default verify_tls checkbox to unchecked + add helper text ee5fda6 fix(frontend): make verify_tls helper text spell out the MITM tradeoff 0fab40b feat(backend): default verify_tls to false for lab/redteam Mythic 91533c2 docs(spec): align c2_config.verify_tls default with sprint 10 (false) 208c6e6 fix(c2): append trailing slash to mythic /graphql URL to avoid 301 redirect ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
knacky added 5 commits 2026-06-21 19:07:24 +00:00
Flip useState(true) → useState(false) and delete-reset from true → false
so a freshly-created C2 config does not block self-signed Mythic instances.
Add hint paragraph below the checkbox matching FormField hint style
(text-[12px] text-graphite): "Leave unchecked for lab Mythic with
self-signed certificates."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flip default to False at model, API fallback, adapter, and factory layers.
add migration 0008 flipping c2_config.verify_tls server_default to false.
suppress urllib3 InsecureRequestWarning when verify_tls is off.
adapt c2 tests to new verify_tls default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
knacky added 1 commit 2026-06-21 19:25:12 +00:00
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>
knacky merged commit 65857134ef into main 2026-06-21 19:44:33 +00:00
knacky deleted branch sprint/10-c2-tls-default 2026-06-21 19:44:33 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: knacky/mimic#12