Okta Potentially Stolen Session


Description

This rule looks for the same session being used from two devices, indicating a compromised session token.

Query · python

import json
from datetime import timedelta
from difflib import SequenceMatcher

from panther_detection_helpers.caching import get_string_set, put_string_set
from panther_okta_helpers import okta_alert_context

FUZZ_RATIO_MIN = 0.95
PREVIOUS_SESSION = {}
# the number of days an Okta session is valid for (configured in Okta)
SESSION_TIMEOUT = timedelta(days=1).total_seconds()
EVENT_TYPES = ("user.authentication.sso", "user.session.start")


def rule(event):
    # pylint: disable=global-statement
    # ensure previous session info is avaialable in the alert_context for investigation
    global PREVIOUS_SESSION

    session_id = event.deep_get("authenticationContext", "externalSessionId", default="unknown")
    dt_hash = event.deep_get("debugContext", "debugData", "dtHash", default="unknown")

    # Some events by Okta admins may appear to have changed IPs
    # and user agents due to internal Okta behavior:
    # https://support.okta.com/help/s/article/okta-integrations-showing-as-rawuseragent-with-okta-ips
    # As such, we ignore certain client ids known to originate from Okta:
    # https://developer.okta.com/docs/api/openapi/okta-myaccount/myaccount/tag/OktaApplications/
    if event.deep_get("client", "id") in [
        "okta.b58d5b75-07d4-5f25-bf59-368a1261a405"  # Admin Console
    ]:
        return False

    # Filter only on app access and session start events
    if event.get("eventType") not in EVENT_TYPES or (
        session_id == "unknown" or dt_hash == "unknown"
    ):
        return False

    key = session_id + "-" + dt_hash

    # lookup if we've previously stored the session cookie
    PREVIOUS_SESSION = get_string_set(key)

    # For unit test mocks we need to eval the string to a set
    if isinstance(PREVIOUS_SESSION, str):
        PREVIOUS_SESSION = set(json.loads(PREVIOUS_SESSION))

    # If the sessionID has not been seen before, store information about it
    if not PREVIOUS_SESSION:
        put_string_set(
            key,
            [
                str(event.deep_get("securityContext", "asNumber")),
                event.deep_get("client", "ipAddress"),
                # clearly label the user agent string so we can find it during the comparison
                "user_agent:" + event.deep_get("client", "userAgent", "rawUserAgent"),
                event.deep_get("client", "userAgent", "browser"),
                event.deep_get("client", "userAgent", "os"),
                event.get("p_event_time"),
                "sign_on_mode:"
                + event.deep_get("debugContext", "debugData", "signOnMode", default="unknown"),
                "threat_suspected:"
                + event.deep_get(
                    "debugContext", "debugData", "threat_suspected", default="unknown"
                ),
            ],
            epoch_seconds=event.event_time_epoch() + SESSION_TIMEOUT,
        )

    # if the session cookie was seen before
    else:
        # we use a fuzz match to compare the current and prev user agent.
        # We cannot do a direct match since Okta can occasionally maintain
        # a session across browser upgrades.

        # the user-agent was tagged during storage so we can find it, remove that tag
        [prev_ua] = [x for x in PREVIOUS_SESSION if "user_agent:" in x] or ["prev_ua_not_found"]
        prev_ua = prev_ua.split("_agent:")[1]

        diff_ratio = SequenceMatcher(
            None,
            event.deep_get("client", "userAgent", "rawUserAgent", default="ua_not_found"),
            prev_ua,
        ).ratio()

        # is this session being used from a new IP and a different browser
        if (
            str(event.deep_get("client", "ipAddress", default="ip_not_found"))
            not in PREVIOUS_SESSION
            and diff_ratio < FUZZ_RATIO_MIN
        ):
            # make the fuzz ratio available in the alert context
            PREVIOUS_SESSION.add("Fuzz Ratio: " + str(diff_ratio))
            return True

    return False


def title(event):
    return (
        f"Potentially Stolen Okta Session - "
        f"{event.deep_get('actor', 'displayName', default='Unknown_user')}"
    )


def alert_context(event):
    context = okta_alert_context(event)
    context["previous_session"] = str(PREVIOUS_SESSION)
    return context

Analyst notes

Confirm the session is used on two devices, one of which is unknown. Lock the users Okta account and clear the users sessions in down stream apps.

Raw source Okta Potentially Stolen Session · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: okta_potentially_stolen_session.py
RuleID: Okta.PotentiallyStolenSession
DisplayName: Okta Potentially Stolen Session
Enabled: true
LogTypes:
  - Okta.SystemLog
Tags:
  - Identity & Access Management
  - Okta
Reports:
  MITRE ATT&CK:
    - TA0006:T1539
Severity: High
Description: This rule looks for the same session being used from two devices, indicating a compromised session token.
Runbook: Confirm the session is used on two devices, one of which is unknown. Lock the users Okta account and clear the users sessions in down stream apps.
Reference: https://sec.okta.com/sessioncookietheft
SummaryAttributes:
  - eventType
  - severity
  - p_any_ip_addresses
  - p_any_domain_names
Tests:
  - Name: Same device and OS
    ExpectedResult: false
    Mocks:
      - objectName: get_string_set
        returnValue: >
          [
              "263297",
              "1.2.3.4",
              "user_agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
              "CHROME",
              "Linux"
          ]
    Log:
      {
        "actor":
          {
            "alternateId": "admin",
            "displayName": "unknown",
            "id": "unknown",
            "type": "User",
          },
        "authenticationContext":
          { "authenticationStep": 0, "externalSessionId": "123456789" },
        "client":
          {
            "device": "Computer",
            "geographicalContext":
              {
                "city": "Dois Irmaos",
                "country": "Brazil",
                "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                "postalCode": "93950",
                "state": "Rio Grande do Sul",
              },
            "ipAddress": "1.2.3.4",
            "userAgent":
              {
                "browser": "CHROME",
                "os": "Linux",
                "rawUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
              },
            "zone": "null",
          },
        "debugContext":
          {
            "debugData":
              {
                "loginResult": "VERIFICATION_ERROR",
                "requestId": "redacted",
                "requestUri": "redacted",
                "threatSuspected": "false",
                "url": "redacted",
                "dtHash": "kzpx58a99d2oam082rlu588wgy1mb0zfi1e1l63f9cjx4uxc455k4t6xdiwbxian",
              },
          },
        "displayMessage": "User login to Okta",
        "eventType": "user.session.start",
        "legacyEventType": "core.user_auth.login_failed",
        "outcome": { "reason": "VERIFICATION_ERROR", "result": "FAILURE" },
        "p_any_domain_names": ["rnvtelecom.com.br"],
        "p_any_ip_addresses": ["redacted"],
        "p_event_time": "redacted",
        "p_log_type": "Okta.SystemLog",
        "p_parse_time": "redacted",
        "p_row_id": "redacted",
        "p_source_id": "redacted",
        "p_source_label": "Okta",
        "published": "redacted",
        "request":
          {
            "ipChain":
              [
                {
                  "geographicalContext":
                    {
                      "city": "Dois Irmaos",
                      "country": "Brazil",
                      "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                      "postalCode": "93950",
                      "state": "Rio Grande do Sul",
                    },
                  "ip": "redacted",
                  "version": "V4",
                },
              ],
          },
        "securityContext":
          {
            "asNumber": 263297,
            "asOrg": "renovare telecom",
            "domain": "rnvtelecom.com.br",
            "isProxy": false,
            "isp": "renovare telecom",
          },
        "severity": "INFO",
        "transaction": { "detail": {}, "id": "redacted", "type": "WEB" },
        "uuid": "redacted",
        "version": "0",
      }
  - Name: Different device & ASN
    ExpectedResult: true
    Mocks:
      - objectName: get_string_set
        returnValue: >
          [
              "123456",
              "4.3.2.1",
              "user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
              "CHROME",
              "MacOS"
          ]
    Log:
      {
        "actor":
          {
            "alternateId": "admin",
            "displayName": "Bobert",
            "id": "unknown",
            "type": "User",
          },
        "authenticationContext":
          { "authenticationStep": 0, "externalSessionId": "123456789" },
        "client":
          {
            "device": "Computer",
            "geographicalContext":
              {
                "city": "Dois Irmaos",
                "country": "Brazil",
                "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                "postalCode": "93950",
                "state": "Rio Grande do Sul",
              },
            "ipAddress": "1.2.3.4",
            "userAgent":
              {
                "browser": "CHROME",
                "os": "Linux",
                "rawUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
              },
            "zone": "null",
          },
        "debugContext":
          {
            "debugData":
              {
                "dtHash": "kzpx58a99d2oam082rlu588wgy1mb0zfi1e1l63f9cjx4uxc455k4t6xdiwbxian",
                "loginResult": "VERIFICATION_ERROR",
                "requestId": "redacted",
                "requestUri": "redacted",
                "threatSuspected": "false",
                "url": "redacted",
              },
          },
        "displayMessage": "User login to Okta",
        "eventType": "user.session.start",
        "legacyEventType": "core.user_auth.login_failed",
        "outcome": { "reason": "VERIFICATION_ERROR", "result": "FAILURE" },
        "p_any_domain_names": ["rnvtelecom.com.br"],
        "p_any_ip_addresses": ["redacted"],
        "p_event_time": "redacted",
        "p_log_type": "Okta.SystemLog",
        "p_parse_time": "redacted",
        "p_row_id": "redacted",
        "p_source_id": "redacted",
        "p_source_label": "Okta",
        "published": "redacted",
        "request":
          {
            "ipChain":
              [
                {
                  "geographicalContext":
                    {
                      "city": "Dois Irmaos",
                      "country": "Brazil",
                      "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                      "postalCode": "93950",
                      "state": "Rio Grande do Sul",
                    },
                  "ip": "redacted",
                  "version": "V4",
                },
              ],
          },
        "securityContext":
          {
            "asNumber": 263297,
            "asOrg": "renovare telecom",
            "domain": "rnvtelecom.com.br",
            "isProxy": false,
            "isp": "renovare telecom",
          },
        "severity": "INFO",
        "transaction": { "detail": {}, "id": "redacted", "type": "WEB" },
        "uuid": "redacted",
        "version": "0",
      }
  - Name: Different ASN & same device
    ExpectedResult: false
    Mocks:
      - objectName: get_string_set
        returnValue: >
          [
              "654321",
              "1.2.3.4",
              "user_agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
              "CHROME",
              "Linux"
          ]
    Log:
      {
        "actor":
          {
            "alternateId": "admin",
            "displayName": "Bobert",
            "id": "unknown",
            "type": "User",
          },
        "authenticationContext":
          { "authenticationStep": 0, "externalSessionId": "123456789" },
        "client":
          {
            "device": "Computer",
            "geographicalContext":
              {
                "city": "Dois Irmaos",
                "country": "Brazil",
                "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                "postalCode": "93950",
                "state": "Rio Grande do Sul",
              },
            "ipAddress": "1.2.3.4",
            "userAgent":
              {
                "browser": "CHROME",
                "os": "Linux",
                "rawUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36",
              },
            "zone": "null",
          },
        "debugContext":
          {
            "debugData":
              {
                "loginResult": "VERIFICATION_ERROR",
                "requestId": "redacted",
                "requestUri": "redacted",
                "threatSuspected": "false",
                "url": "redacted",
                "dtHash": "kzpx58a99d2oam082rlu588wgy1mb0zfi1e1l63f9cjx4uxc455k4t6xdiwbxian",
              },
          },
        "displayMessage": "User login to Okta",
        "eventType": "user.session.start",
        "legacyEventType": "core.user_auth.login_failed",
        "outcome": { "reason": "VERIFICATION_ERROR", "result": "FAILURE" },
        "p_any_domain_names": ["rnvtelecom.com.br"],
        "p_any_ip_addresses": ["redacted"],
        "p_event_time": "redacted",
        "p_log_type": "Okta.SystemLog",
        "p_parse_time": "redacted",
        "p_row_id": "redacted",
        "p_source_id": "redacted",
        "p_source_label": "Okta",
        "published": "redacted",
        "request":
          {
            "ipChain":
              [
                {
                  "geographicalContext":
                    {
                      "city": "Dois Irmaos",
                      "country": "Brazil",
                      "geolocation": { "lat": -29.6116, "lon": -51.0933 },
                      "postalCode": "93950",
                      "state": "Rio Grande do Sul",
                    },
                  "ip": "redacted",
                  "version": "V4",
                },
              ],
          },
        "securityContext":
          {
            "asNumber": 263297,
            "asOrg": "renovare telecom",
            "domain": "rnvtelecom.com.br",
            "isProxy": false,
            "isp": "renovare telecom",
          },
        "severity": "INFO",
        "transaction": { "detail": {}, "id": "redacted", "type": "WEB" },
        "uuid": "redacted",
        "version": "0",
      }
  - Name: Okta internal event should be ignored
    ExpectedResult: false
    Mocks:
      - objectName: get_string_set
        returnValue: >
          [
              "123456",
              "4.3.2.1",
              "user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
              "CHROME",
              "MacOS"
          ]
    Log:
      {
        "actor":
          {
            "alternateId": "admin",
            "displayName": "Bobert",
            "id": "unknown",
            "type": "User",
          },
        "authenticationContext":
          { "authenticationStep": 0, "externalSessionId": "123456789" },
        "client":
          {
            "device": "Unknown",
            "geographicalContext":
              {
                "city": "Boardman",
                "country": "United States",
                "geolocation": { "lat": 45.8234, "lon": -119.7257 },
                "postalCode": "97818",
                "state": "Oregon",
              },
            "id": "okta.b58d5b75-07d4-5f25-bf59-368a1261a405",
            "ipAddress": "44.238.82.114",
            "userAgent":
              {
                "browser": "UNKNOWN",
                "os": "Unknown",
                "rawUserAgent": "Okta-Integrations",
              },
            "zone": "null",
          },
        "debugContext":
          {
            "debugData":
              {
                "loginResult": "VERIFICATION_ERROR",
                "requestId": "redacted",
                "requestUri": "redacted",
                "threatSuspected": "false",
                "url": "redacted",
              },
          },
        "displayMessage": "User login to Okta",
        "eventType": "user.session.start",
        "legacyEventType": "core.user_auth.login_failed",
        "outcome": { "reason": "VERIFICATION_ERROR", "result": "FAILURE" },
        "p_any_domain_names": ["rnvtelecom.com.br"],
        "p_any_ip_addresses": ["redacted"],
        "p_event_time": "redacted",
        "p_log_type": "Okta.SystemLog",
        "p_parse_time": "redacted",
        "p_row_id": "redacted",
        "p_source_id": "redacted",
        "p_source_label": "Okta",
        "published": "redacted",
        "request":
          {
            "ipChain":
              [
                {
                  "geographicalContext":
                    {
                      "city": "Boardman",
                      "country": "United States",
                      "geolocation": { "lat": 45.8234, "lon": -119.7257 },
                      "postalCode": "97818",
                      "state": "Oregon",
                    },
                  "ip": "44.238.82.114",
                  "version": "V4",
                },
              ],
          },
        "securityContext": {},
        "severity": "INFO",
        "transaction": { "detail": {}, "id": "redacted", "type": "WEB" },
        "uuid": "redacted",
        "version": "0",
      }


# ------ paired body: okta_potentially_stolen_session.py ------

import json
from datetime import timedelta
from difflib import SequenceMatcher

from panther_detection_helpers.caching import get_string_set, put_string_set
from panther_okta_helpers import okta_alert_context

FUZZ_RATIO_MIN = 0.95
PREVIOUS_SESSION = {}
# the number of days an Okta session is valid for (configured in Okta)
SESSION_TIMEOUT = timedelta(days=1).total_seconds()
EVENT_TYPES = ("user.authentication.sso", "user.session.start")


def rule(event):
    # pylint: disable=global-statement
    # ensure previous session info is avaialable in the alert_context for investigation
    global PREVIOUS_SESSION

    session_id = event.deep_get("authenticationContext", "externalSessionId", default="unknown")
    dt_hash = event.deep_get("debugContext", "debugData", "dtHash", default="unknown")

    # Some events by Okta admins may appear to have changed IPs
    # and user agents due to internal Okta behavior:
    # https://support.okta.com/help/s/article/okta-integrations-showing-as-rawuseragent-with-okta-ips
    # As such, we ignore certain client ids known to originate from Okta:
    # https://developer.okta.com/docs/api/openapi/okta-myaccount/myaccount/tag/OktaApplications/
    if event.deep_get("client", "id") in [
        "okta.b58d5b75-07d4-5f25-bf59-368a1261a405"  # Admin Console
    ]:
        return False

    # Filter only on app access and session start events
    if event.get("eventType") not in EVENT_TYPES or (
        session_id == "unknown" or dt_hash == "unknown"
    ):
        return False

    key = session_id + "-" + dt_hash

    # lookup if we've previously stored the session cookie
    PREVIOUS_SESSION = get_string_set(key)

    # For unit test mocks we need to eval the string to a set
    if isinstance(PREVIOUS_SESSION, str):
        PREVIOUS_SESSION = set(json.loads(PREVIOUS_SESSION))

    # If the sessionID has not been seen before, store information about it
    if not PREVIOUS_SESSION:
        put_string_set(
            key,
            [
                str(event.deep_get("securityContext", "asNumber")),
                event.deep_get("client", "ipAddress"),
                # clearly label the user agent string so we can find it during the comparison
                "user_agent:" + event.deep_get("client", "userAgent", "rawUserAgent"),
                event.deep_get("client", "userAgent", "browser"),
                event.deep_get("client", "userAgent", "os"),
                event.get("p_event_time"),
                "sign_on_mode:"
                + event.deep_get("debugContext", "debugData", "signOnMode", default="unknown"),
                "threat_suspected:"
                + event.deep_get(
                    "debugContext", "debugData", "threat_suspected", default="unknown"
                ),
            ],
            epoch_seconds=event.event_time_epoch() + SESSION_TIMEOUT,
        )

    # if the session cookie was seen before
    else:
        # we use a fuzz match to compare the current and prev user agent.
        # We cannot do a direct match since Okta can occasionally maintain
        # a session across browser upgrades.

        # the user-agent was tagged during storage so we can find it, remove that tag
        [prev_ua] = [x for x in PREVIOUS_SESSION if "user_agent:" in x] or ["prev_ua_not_found"]
        prev_ua = prev_ua.split("_agent:")[1]

        diff_ratio = SequenceMatcher(
            None,
            event.deep_get("client", "userAgent", "rawUserAgent", default="ua_not_found"),
            prev_ua,
        ).ratio()

        # is this session being used from a new IP and a different browser
        if (
            str(event.deep_get("client", "ipAddress", default="ip_not_found"))
            not in PREVIOUS_SESSION
            and diff_ratio < FUZZ_RATIO_MIN
        ):
            # make the fuzz ratio available in the alert context
            PREVIOUS_SESSION.add("Fuzz Ratio: " + str(diff_ratio))
            return True

    return False


def title(event):
    return (
        f"Potentially Stolen Okta Session - "
        f"{event.deep_get('actor', 'displayName', default='Unknown_user')}"
    )


def alert_context(event):
    context = okta_alert_context(event)
    context["previous_session"] = str(PREVIOUS_SESSION)
    return context

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.