Security Advanced

Red Team Web App Tactics — Phishing into Apps, Lateral Movement, Persistence

How professional adversary simulation operates against web applications — initial access via phishing, persistence inside accounts, lateral movement through connected systems.

KC Ramo DjangoZen Team May 10, 2026 25 min read 495 views

Pentest vs red team

Pentests find specific bugs in scope. Red teams simulate full adversaries — phishing users, evading detection, establishing persistence, achieving specific business-impact objectives. The goal isn't a long bug list; it's understanding whether your security operations actually detect and respond to a sophisticated adversary.

This tutorial walks through how a web-app-focused red team operates, with defensive takeaways at each stage.

Stage 1 — Reconnaissance with intent

Beyond technical recon (tutorial 7), the human reconnaissance:

  • Organization chart — LinkedIn, regulatory filings, press releases. Who's the CFO? Who manages IT? Who's a developer with access?
  • Communication patterns — Twitter, conference talks, Slack communities. How does the company talk? What's the internal vernacular?
  • Tech stack inference — job postings reveal what they use ("Django, AWS, Stripe, Snowflake experience preferred")
  • Vendor relationships — what services do they use? Identifiable via DNS, email signatures, vendor mentions
  • Timing — when's quarterly close? When does the CEO travel? When are security teams thinly staffed (holidays, conferences)?

Crafting the pretext

Effective phishing pretexts are specific. Examples successfully used:

  • "Hi [Finance Manager], following up from [actual conference target attended last month] — please review the attached invoice for the vendor we discussed."
  • "Hello, this is [name of actual IT vendor]. We have a critical patch for your VPN. Please log in here to apply."
  • "DjangoZen security alert: unusual login from [user's actual hometown]. Verify here."

The more specific to the target's actual context, the higher the success rate. Spear phishing at this level returns 30-70% click rates in unprepared environments.

Defense

  • Periodic OSINT review — search what your company looks like from the outside
  • Threat hunting — assume adversaries have done this; look for indicators in your logs
  • Employee training focused on "specific to me" lures, not generic warnings
  • Reporting culture — easy to report suspicious emails, no shame in being targeted

Stage 2 — Initial access via web app

For modern targets, the most common initial vectors:

Targeted phishing → web app login

Email sends user to a fake login page (perfect clone of real). User enters credentials and MFA code. Attacker forwards in real-time to real login page, completes auth, captures session cookie.

Modern tooling: Evilginx2, Modlishka. Transparent reverse-proxy attack frameworks. Bypass TOTP MFA almost trivially.

Phishing → OAuth consent

Email links to a real OAuth authorize URL for an app the attacker registered. User authenticates with their real provider (Google, Microsoft), grants consent. Attacker now has a long-lived access token.

Credential stuffing → existing weak account

Username from one breach + password from another breach matches your user. No phishing needed.

Browser exploit / malware → cookie theft

User browses to attacker-controlled page. Browser exploit or malware grabs cookies (no MFA needed since the session is already authenticated).

Defenses against initial access

  • Phishing-resistant MFA (WebAuthn / FIDO2) — defeats Evilginx-style transparent phishing because the auth challenge is bound to the actual domain
  • Approval workflows for OAuth apps — block unknown apps from being authorized by users
  • Conditional access policies — require additional verification for logins from new devices/locations
  • Browser security baseline — managed browser, security extensions, alerts on dangerous downloads
  • Session cookie hardening — short lifetimes, IP binding, secure attributes

Stage 3 — Persistence inside accounts

Once an account is compromised, attackers want to maintain access even if the password is changed or session is revoked.

Add a new MFA method

Attacker adds their own TOTP enrollment to the account. Even if the user resets their password, attacker still passes MFA.

Defense: notify on MFA changes, require step-up auth for MFA modifications, periodic audit of registered MFA methods.

Add a recovery email

Attacker sets their own email as a recovery option. If user does full account reset, recovery email is the bypass.

Defense: require recent password verification to add recovery options, notify on changes.

Add an OAuth app

Attacker authorizes their own OAuth app with broad scopes. Long-lived token, not affected by password change.

Defense: admin approval for OAuth apps, periodic review of authorized apps per user, scope minimization.

Add an API key / personal access token

If the user has a developer account, attacker generates a personal access token with broad scopes. Doesn't expire with password change.

Defense: API key expiration, scope minimization, audit of issued tokens, anomaly detection on API usage.

Modify connected services

If the app supports webhooks, integrations, third-party connections: attacker establishes a backdoor connection that delivers events to attacker infrastructure.

Defense: audit of integrations, approval workflows for new integrations, periodic verification of integration endpoints.

Stage 4 — Privilege escalation

Once persisted as a low-privilege user, attackers escalate.

Role abuse

User has more permissions than their job requires (overprovisioned access). Attacker exploits the excess.

Defense: principle of least privilege, periodic access reviews, just-in-time elevation.

Application-level bugs

The compromised user can access features that elevate. "Edit my profile" forms that accept role parameters. Internal APIs without authorization checks.

Defense: server-side authorization on every endpoint, not just client-side UI hiding.

Social engineering inside

Compromised internal account asks help desk for "my password reset" or "my MFA reset." Help desk obliges. Now attacker has more permanent access.

Defense: out-of-band verification for credential/MFA changes, never reset based solely on "I'm locked out" requests.

Token escalation

Low-privilege OAuth token used to authorize a higher-privilege app in the same flow.

Defense: explicit re-authentication for elevated scopes, scope auditing.

Stage 5 — Lateral movement through connected services

Modern SaaS environments interconnect. Compromise of one often pivots to others.

Single Sign-On as pivot

Attacker has access to your SSO provider's session. Now logged into Slack, Google Workspace, GitHub, AWS console, etc., all via the SSO.

Defense: separate, stronger auth for high-stakes services (AWS console requires additional MFA even after SSO), audit logs cross-correlated.

CI/CD takeover

Developer account compromised. Attacker pushes code to a branch, opens PR, exploits CI/CD permissions to deploy malicious code or steal secrets from build environment.

Defense: required code review (no bypass), production deploy approval workflows, ephemeral CI credentials, secret scanning in repos and builds.

Cloud account pivot

Application compromise → instance metadata service → IAM role credentials → cloud account access. (Covered in tutorial 5.)

Defense: scoped IAM roles, IMDSv2 mandatory, network segmentation between app and metadata service.

Vendor / partner account pivot

Compromise of an account that has vendor admin access. Attacker uses your access to your vendor (a SaaS you bought, a service you use) to attack the vendor's other customers, or to harvest data passing through.

Defense: scoped permissions on vendor accounts, audit of cross-account access, vendor SOC 2 review.

Stage 6 — Action on objective

Whatever the actual goal:

  • Data exfiltration — bulk export of customer data, source code, IP
  • Financial fraud — wire transfers, ACH manipulation, BEC
  • Operational disruption — wipe systems, disable services
  • Persistence for long-term intelligence — sit silently, observe, exfiltrate strategically
  • Reputation damage — public defacement, public leak with PR component

Detection at this stage

If you didn't detect earlier, this is your last chance. Indicators:

  • Bulk data downloads
  • Unusual database queries
  • Large outbound network traffic
  • Mass deletion or modification
  • Unscheduled administrative actions
  • Activity from accounts that should be dormant

Centralized logging + anomaly detection finds these — if you're looking.

Defensive maturity — what red teams reveal

After a red team engagement, the failures cluster into categories:

"We didn't have logs for that"

Missing telemetry. The attack happened, but you can't see it because logging wasn't enabled for that path.

"The alert fired but no one noticed"

Alert fatigue, no on-call rotation, alerts going to dead email distribution lists.

"We saw the alert, didn't escalate"

Triage failed. The first responder dismissed it as noise.

"We escalated, but had no playbook"

No documented response, no clear authority, conflicting decisions.

"We followed the playbook, but it was wrong"

Outdated procedures, missing escalation contacts, broken assumptions.

Each is fixable, but only if you've tested. Red team exercises reveal which category you fall into. Then you fix the broken layer and test again.

Defenses against the full chain

Stacking the controls from this tutorial against the full attack chain:

Stage Best defense
OSINT Periodic external recon against self
Phishing Phishing-resistant MFA + employee training
Initial access MFA, conditional access, session hardening
Persistence MFA change notifications, OAuth review, API key audit
Privilege escalation Least privilege, application-layer auth checks
Lateral movement Network segmentation, separate auth for high-stakes
Action Detection at every previous stage

No single layer stops everything. Combined, they create choke points that slow attackers and give detection time to fire.

Thinking like a red team

Red teaming simulates a real adversary end to end — not just finding a vulnerability but chaining initial access, movement, and persistence into a realistic attack, the way a genuine intruder would. The value for defenders is that it tests not just whether a flaw exists but whether your detection and response actually work against an attacker who behaves like a real one. Understanding red-team tactics helps you see your environment as an adversary does: as a series of steps from a foothold to an objective, each of which is an opportunity for you to detect and stop them if your defenses are layered and monitored.

From foothold to objective

A real intrusion rarely jumps straight to the goal. It starts with initial access — often phishing a user — then moves laterally through the environment, escalating privileges and pivoting between systems, and establishes persistence so the access survives. Each stage is a chance to detect the intruder if you are watching for the right signals: unusual authentication, unexpected internal connections, new persistence mechanisms. Defending against the full attack chain, rather than only the initial vulnerability, means instrumenting and monitoring each stage so that even an attacker who gets a foothold is caught as they try to move toward what they actually want.

Detection as the real defense

The lesson red teaming teaches most clearly is that prevention will sometimes fail, and detection is what limits the damage when it does. An attacker who gets in but is detected and contained quickly does far less harm than one who roams undetected for months. Investing in the ability to notice an intrusion in progress — logging the right events, alerting on anomalous behavior, having a response plan — is what turns a breach from a catastrophe into an incident. Red-team exercises are valuable precisely because they test this detection-and-response capability under realistic conditions, revealing the blind spots before a real adversary exploits them.

Phishing-resistant authentication with passkeys

Stage 2 above shows why transparent reverse-proxy phishing defeats one-time-code MFA: the user types a TOTP or SMS code into a proxied page, and the proxy replays it to the real site inside the window it is valid. WebAuthn/FIDO2 breaks this attack at the protocol level. The authenticator signs a challenge that includes the origin the browser is actually talking to, and the private key never leaves the device. A key registered for https://app.example.com will not produce a valid assertion for https://app-example.com or any proxy origin, so a relayed login simply fails. This is not "stronger MFA" — it is a different trust model, and it is the single highest-leverage control against the phishing chain in this tutorial.

Two deployment shapes matter for a web app. Roaming security keys (hardware keys) are the strongest option for administrators and developers — the highest-value accounts in every stage above. Passkeys (discoverable credentials synced through a platform keychain) give ordinary users a phishing-resistant option without carrying hardware, at the cost of trusting the sync provider. Most organisations require hardware keys for privileged roles and allow passkeys for everyone else.

Registering a credential in Django

The webauthn package (the maintained py_webauthn library) implements the server side of the ceremony. Registration issues options tied to your relying-party ID and verifies the attestation the browser returns. Store the resulting credential ID, public key and signature counter against the user.

from webauthn import generate_registration_options, verify_registration_response
from webauthn.helpers.structs import AuthenticatorSelectionCriteria, UserVerificationRequirement
from webauthn.helpers import options_to_json

RP_ID = "app.example.com"          # must match the site origin exactly
RP_ORIGIN = "https://app.example.com"

def begin_registration(request):
    user = request.user
    options = generate_registration_options(
        rp_id=RP_ID,
        rp_name="myproject",
        user_name=user.get_username(),
        user_id=str(user.pk).encode(),
        authenticator_selection=AuthenticatorSelectionCriteria(
            user_verification=UserVerificationRequirement.REQUIRED,
        ),
    )
    request.session["webauthn_challenge"] = options.challenge  # bind to this session
    return JsonResponse(json.loads(options_to_json(options)))

def finish_registration(request):
    body = json.loads(request.body)
    verification = verify_registration_response(
        credential=body,
        expected_challenge=request.session.pop("webauthn_challenge"),
        expected_rp_id=RP_ID,
        expected_origin=RP_ORIGIN,       # origin binding is what defeats the proxy
    )
    WebAuthnCredential.objects.create(
        user=request.user,
        credential_id=verification.credential_id,
        public_key=verification.credential_public_key,
        sign_count=verification.sign_count,
    )
    return JsonResponse({"status": "ok"})

The authentication ceremony mirrors this: generate_authentication_options issues a challenge, and verify_authentication_response checks the signature against the stored public key with the same expected_origin and expected_rp_id. Two correctness details matter in production. First, the signature counter: if a verified assertion reports a counter lower than or equal to the value you stored, treat it as a possible cloned authenticator and refuse the login. Second, keep the challenge in server-side session state (as above) rather than a hidden form field, so it cannot be replayed.

Rollout without locking people out

  • Enroll before you enforce. Ship passkey registration as an option first, drive adoption, then flip enforcement on per role. Enforce hardware keys for staff, admin and any account that can deploy code long before you touch general users.
  • Register two authenticators. Require a backup key at enrollment so a lost device is a support ticket, not an account-recovery emergency — recovery flows are exactly the weak point Stage 3 and Stage 4 abuse.
  • Make recovery as strong as login. A phishing-resistant primary factor is worthless if "lost my key" drops the account back to an emailed link. Route recovery through verified hardware, an admin-approved manual process, or in-person verification.
  • Remove weaker factors once coverage is high. As long as SMS or TOTP remains an accepted factor, the proxy attack still works. The security benefit only fully lands when you retire the fallbacks for that population.

Detecting account takeover and unusual admin activity

Prevention fails eventually; the sections above assume it will. Detection is what bounds the damage, and account takeover has a recognisable signature if you are logging the right events. The goal is a small number of high-fidelity signals wired to a response, not a wall of noise nobody reads (the "the alert fired but no one noticed" failure named earlier).

The events worth logging

Log authentication and account-management events as structured records, separate from application request logs, and ship them somewhere the compromised account cannot edit. Django's built-in signals give you most of the authentication surface for free.

from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.dispatch import receiver

@receiver(user_logged_in)
def log_login(sender, request, user, **kwargs):
    SecurityEvent.objects.create(
        kind="login.success",
        user=user,
        ip=client_ip(request),
        user_agent=request.META.get("HTTP_USER_AGENT", ""),
        # asn/country resolved from an offline GeoIP database, no external calls
        country=geo_country(client_ip(request)),
    )

@receiver(user_login_failed)
def log_failure(sender, credentials, request, **kwargs):
    SecurityEvent.objects.create(
        kind="login.failure",
        username=credentials.get("username", ""),
        ip=client_ip(request) if request else "",
    )

Beyond login success and failure, emit an event for every action an attacker uses to entrench: password change, MFA method added or removed, recovery email or phone changed, API token created, OAuth grant authorized, role or group membership changed, and webhook or integration endpoint modified. These are the Stage 3 and Stage 4 persistence actions — logging them is what makes them detectable.

Signals that indicate takeover

Signal Why it matters Tuning to cut false positives
Impossible travel Two logins from locations too far apart for the elapsed time Whitelist corporate VPN egress; treat mobile carrier IP ranges loosely
New device + sensitive action in one session Password/MFA change from a never-seen device fingerprint is classic ATO Score, don't block, on device newness alone; escalate on the combination
MFA method added shortly after login from new IP Attacker enrolling their own authenticator for persistence Very low false-positive rate — page a human on this one
Credential-stuffing shape Many distinct usernames, few attempts each, from one IP/ASN Aggregate by ASN over a window, not per-account lockout
Dormant account reactivation Account with no activity for 90+ days suddenly authenticates and acts Compare against the account's own baseline, not a global one
Admin action outside baseline hours Bulk export or user creation at 03:00 from an admin who works 09:00–17:00 Per-user working-hours profile; weight by sensitivity of the action

Most of these are cheap to compute on the SecurityEvent stream. Impossible travel, for example, is a windowed self-join comparing consecutive logins per user:

-- flag logins where the implied travel speed exceeds ~900 km/h
WITH ordered AS (
  SELECT user_id, created_at, country, lat, lon,
         LAG(created_at) OVER w AS prev_at,
         LAG(lat)        OVER w AS prev_lat,
         LAG(lon)        OVER w AS prev_lon
  FROM security_event
  WHERE kind = 'login.success'
  WINDOW w AS (PARTITION BY user_id ORDER BY created_at)
)
SELECT user_id, created_at, prev_at
FROM ordered
WHERE prev_at IS NOT NULL
  AND haversine_km(prev_lat, prev_lon, lat, lon)
      / GREATEST(EXTRACT(EPOCH FROM created_at - prev_at) / 3600, 0.01) > 900;

Watching the admin surface specifically

Administrative accounts are the objective of most of the escalation and lateral-movement stages above, so instrument them harder than ordinary users. Django's admin emits LogEntry rows for every add, change and delete through the admin site — surface them, don't just let them accumulate. Alert when a new user is granted is_staff or is_superuser, when a permission or group is broadened, and on bulk actions (a single admin deleting or exporting thousands of rows). Compare the count of privileged accounts against a known-good baseline on a schedule; a new superuser that no change ticket explains is a Stage 3 persistence finding, not a curiosity.

Least privilege and segmentation to contain movement

Stages 4 and 5 only pay off because a foothold reaches far. The countermeasure is to make any single compromised account or service worth as little as possible. This is architectural work, done before an incident, and it is what turns a full breach into a contained one.

Least privilege in the application

  • Authorize on the server, on every endpoint. The "edit my profile accepts a role parameter" bug in Stage 4 is a missing server-side check. Never rely on the UI hiding a control. In Django, gate views with permission checks (PermissionRequiredMixin, @permission_required) and enforce object-level ownership in the queryset itself — filter by request.user rather than trusting an ID from the request.
  • Model roles as permissions, not as a superuser flag. Broad is_superuser grants are the overprovisioning Stage 4 exploits. Prefer narrow, named permissions grouped into roles that map to real job functions.
  • Just-in-time elevation. Grant standing access at the lowest tier and require an explicit, logged, time-boxed elevation for sensitive actions. A token stolen from an idle session then carries no privilege by default.
  • Re-authenticate for sensitive actions. Require a fresh phishing-resistant assertion (not just a live session) to change MFA, mint an API token, add a webhook, or change another user's role — the exact actions the persistence stage abuses.

Segmentation to break the pivot

  • Separate, stronger auth for high-stakes systems. As the SSO-pivot section notes, an SSO session should not silently unlock the cloud console or the production database. Require an additional hardware-key step at those boundaries.
  • Network segmentation. The app tier should not have a route to the CI/CD control plane, the backup store, or every database in the estate. Deny by default between segments and allow only the specific flows each service needs.
  • Scoped, short-lived service credentials. Prefer per-service credentials with narrow scopes and short lifetimes over one broad key shared across services. Enforce IMDSv2 so an app-layer SSRF cannot trivially read instance role credentials (the cloud-pivot path).
  • Blast-radius review. For each privileged role and service account, write down what an attacker holding it could reach. If the answer is "everything", that is the finding — split it before a red team or a real adversary does.

Finding and removing persistence

When an account is confirmed compromised, resetting the password is not containment. The persistence mechanisms in Stage 3 survive a password change by design: a rogue MFA enrollment, a recovery email, an authorized OAuth app, a personal access token, a webhook, or a scheduled task. Eviction means finding and revoking each one. Build the inventory queries now, while it is calm, so they are ready during an incident.

An eviction checklist for one account

  • Revoke sessions first. Invalidate all active sessions for the user so the current foothold drops immediately.
  • Enumerate and remove authenticators. List every registered MFA method and passkey; remove any the user does not recognise, then have them re-enroll from a trusted device.
  • Reset recovery channels. Check the recovery email and phone; a rogue recovery address is a silent re-entry path.
  • Revoke API tokens and OAuth grants. Delete personal access tokens and third-party app authorizations the user cannot account for — these ignore password changes entirely.
  • Audit webhooks and integrations. Any endpoint pointing at infrastructure you don't control is a data-exfiltration backdoor; disable and re-verify.
  • Check for new privileged accounts. An attacker who reached Stage 4 may have created a fresh admin so that evicting the original account changes nothing.

Inventory queries you can run today

Django flushes a user's sessions when their password changes, but you should be able to do it explicitly, and you should be able to list what exists across the account regardless of whether a password reset happened.

from django.contrib.sessions.models import Session
from django.utils import timezone

def revoke_all_sessions(user):
    # delete only sessions belonging to this user
    for s in Session.objects.filter(expire_date__gte=timezone.now()):
        if str(s.get_decoded().get("_auth_user_id")) == str(user.pk):
            s.delete()

def account_persistence_report(user):
    return {
        "mfa_methods":   list(user.webauthn_credentials.values("id", "created_at")),
        "recovery_email": user.profile.recovery_email,
        "api_tokens":    list(user.api_tokens.values("id", "name", "scopes", "created_at")),
        "oauth_grants":  list(user.oauth_grants.values("client_id", "scopes")),
        "webhooks":      list(Webhook.objects.filter(created_by=user)
                                            .values("id", "target_url", "events")),
        "is_staff":      user.is_staff,
        "is_superuser":  user.is_superuser,
    }

Two account-wide sweeps catch the persistence that hides outside a single user record. New privileged accounts:

-- superusers created in the last 30 days: reconcile each against a change ticket
SELECT id, username, date_joined, last_login
FROM auth_user
WHERE is_superuser = true
  AND date_joined > now() - interval '30 days'
ORDER BY date_joined DESC;

-- API tokens with broad scope and no recent use are prime persistence
SELECT t.id, t.user_id, t.name, t.scopes, t.created_at, t.last_used_at
FROM api_token t
WHERE t.scopes @> ARRAY['admin']
  AND (t.last_used_at IS NULL OR t.last_used_at < now() - interval '14 days');

Scheduled tasks and other quiet footholds

Server-side schedulers are a favourite persistence spot because they run without anyone logging in. Whatever runs your periodic jobs — cron on the host, systemd timers, a Celery beat schedule, or database-backed schedules — treat the full list as an asset to reconcile. Enumerate every scheduled job, diff it against a known-good manifest kept in version control, and alert on any entry that appears out of band. The same applies to OS-level footholds outside the app: unexpected authorized SSH keys for the deploy user, new entries in ~/.ssh/authorized_keys or /srv/app startup scripts, and unfamiliar systemd units. If a job or key is not in the manifest and no change record explains it, treat it as hostile until proven otherwise.

Scoping an authorised red-team engagement

Everything above is what a red team tests. Running one safely and legally is its own discipline: without explicit written authorisation, simulated-attack activity is indistinguishable from a crime, and an out-of-scope action can cause real outage or data loss. The paperwork is not bureaucracy — it is what makes the exercise defensible and safe.

Rules of engagement

The rules of engagement (RoE) document is agreed and signed before any activity begins. At minimum it fixes:

  • Scope — the exact domains, IP ranges, applications and accounts that are in bounds, and an explicit out-of-bounds list (production payment rails, customer data stores, third-party SaaS you don't own, partner systems).
  • Objectives — the business-impact goals the team is testing toward (e.g. "reach a position from which a fraudulent payout could be initiated"), so the exercise measures detection and response, not bug count.
  • Techniques allowed and forbidden — e.g. phishing of employees permitted, but no denial-of-service, no destructive actions, no exfiltration of real customer PII (use planted canary records instead).
  • Timing window — start and end dates, and any blackout periods such as quarter-end close or a major launch.
  • Deconfliction and stop conditions — a named contact on both sides reachable in real time, and the conditions under which the exercise pauses immediately (evidence of a real concurrent breach, unexpected production impact, safety risk).
  • Data handling — how any accessed data is stored, encrypted and destroyed after the engagement, and reporting obligations if genuinely sensitive data is encountered.

Authorisation and sign-off

  • Written authorisation from someone with the authority to grant it — the "get-out-of-jail letter" the testing team carries, naming the systems and the window. This is a legal prerequisite, not a formality.
  • Third-party consent. If the scope touches a cloud provider or SaaS you rent rather than own, you need their permission too; many require advance notice for testing on their infrastructure. Testing systems you do not own or have consent for is out of bounds regardless of your own sign-off.
  • Legal and HR awareness for social-engineering components, since phishing simulations involve employees and must respect employment and privacy law.
  • Trusted-agent list. Keep the set of people who know the test is happening deliberately small so the detection-and-response test is honest, but ensure at least one senior trusted agent can confirm authorisation instantly if a responder escalates to law enforcement or triggers a costly containment.

Don't commission a full engagement before the basics are in place. As the closing note warns, a red team burning its budget on findings an internal audit should have caught tells you little. Engage when you have done the hardening and genuinely want to know whether it holds.

Purple-team exercises

A pure red-team engagement is adversarial and quiet by design: the defenders don't know it's happening, and the value is the realism. That realism has a cost — a single end-to-end run tells you whether you were caught this time, but not much about why a control failed or whether a fix works. Purple teaming closes that loop by having the offensive (red) and defensive (blue) sides work together, deliberately and transparently, to improve detection.

How a purple-team exercise runs

  • Pick a specific behaviour to exercise — for instance, "attacker adds a new MFA method for persistence" or "bulk export of customer records," mapped to a known technique so both sides share vocabulary.
  • Red executes the behaviour in a controlled, announced way against the agreed scope.
  • Blue watches in real time to see whether the telemetry exists, whether an alert fires, and whether triage escalates it correctly.
  • Diagnose the gap on the spot. Missing log source? Alert never written? Alert fired but routed to a dead distribution list? Each maps directly to the failure categories in the maturity section above.
  • Fix and immediately re-run. Add the log source or detection rule, then repeat the same behaviour to confirm it now fires. The tight iterate-and-verify loop is the whole point.

When to choose which

Question you're answering Exercise that fits
Would our SOC actually catch a realistic adversary, end to end? Red team (covert, objective-driven)
Do we have detection for this specific technique, and does it work? Purple team (collaborative, per-behaviour)
Does a named flaw exist in this application, in scope? Penetration test
Are our controls configured to a baseline across the estate? Configuration/audit review

A common maturity path is to run purple-team exercises to build and validate detections technique by technique, then commission a covert red team to confirm the whole operation holds together under realistic conditions. The purple work makes the eventual red-team engagement worth its cost, because you are no longer paying a specialist team to discover gaps you could have closed yourself.

Closing — engage red teams when you're ready

Red team exercises are expensive (€30K-€200K+ for serious engagements) and embarrassing (they will find things). They're also the only realistic test of your operational security.

Don't engage one before you've done the basics. They'll waste their time and yours finding obvious bugs that internal audits should catch. Engage when you've done the work and want to see if it's actually enough.

Tutorial 10 covers what to do when the red team — or a real attacker — succeeds.

Found a mistake in this tutorial? Let us know and we will fix it.