{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# NS-2026-1008 - analysis notebook\n", "\n", "**Controlled simulated environment.** All identities, infrastructure, telemetry,\n", "indicators and incidents are synthetic (RFC 5737 addresses). Nothing here is\n", "real.\n", "\n", "This notebook reproduces the core analysis of the case study from the raw\n", "telemetry in `data/` using only the Python standard library. Every number it\n", "prints appears in `incident-report/incident-report.md` and on the case-study\n", "page. If the dataset changes, this notebook and those documents are stale -\n", "that is the contract (`scripts/build_notebook.py --check`)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Authentication: a failure burst ending in success" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "failed deploy attempts from attacker : 11\n", "first deploy failure : Oct 8 08:41:29\n", "successful login : Oct 8 08:43:17\n", "window : 08:41:29 -> 08:43:17 (12 calls)\n" ] } ], "source": [ "import json, glob, collections, os\n", "\n", "AUTH = \"data/auth/app01_auth.log\"\n", "ATTACKER = \"198.51.100.24\"\n", "\n", "lines = open(AUTH).read().splitlines()\n", "fails = [l for l in lines if \"Failed password for deploy from \" + ATTACKER in l]\n", "ok = [l for l in lines if \"Accepted password for deploy from \" + ATTACKER in l]\n", "\n", "print(\"failed deploy attempts from attacker :\", len(fails))\n", "print(\"first deploy failure :\", fails[0][:15].strip())\n", "print(\"successful login :\", ok[0][:15].strip())\n", "print(\"window :\",\n", " fails[0].split()[2], \"->\", ok[0].split()[2], \"(%d calls)\" % (len(fails) + len(ok)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Eleven failed password attempts against `deploy` from one address in under\n", "two minutes, then a success. That pattern is a successful password-guessing\n", "attack, not a forgotten password (AUTH-001..AUTH-014)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Privilege escalation: sudo from a service account" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Oct 8 07:42:03 app-01 sudo: ops : TTY=pts/1 ; PWD=/home/ops ; USER=root ; COMMAND=/usr/bin/apt-get -y update\n", "Oct 8 08:45:10 app-01 sudo: deploy : TTY=pts/0 ; PWD=/home/deploy ; USER=root ; COMMAND=/bin/bash\n", "\n", "interactive shells from deploy: 1\n" ] } ], "source": [ "sudo = [l for l in lines if \" sudo: \" in l]\n", "for l in sudo:\n", " print(l)\n", "print()\n", "print(\"interactive shells from deploy:\",\n", " sum(1 for l in sudo if l.split(\"COMMAND=\")[-1].strip() == \"/bin/bash\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The only `deploy` sudo event runs `/bin/bash`, i.e. it hands an interactive\n", "`deploy` session a root shell. A deploy/automation account has no legitimate\n", "reason to do that from an interactive TTY (PRIV-002, PROC-010)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Data access: the database export" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "2026-10-08 08:57:44 UTC [15233-1] app_ro@northstar LOG: connection authorized: user=app_ro database=northstar application_name=psql host=192.0.2.20\n", "2026-10-08 08:58:04 UTC [15233-2] app_ro@northstar LOG: execute COPY: COPY (SELECT c.id, c.email, c.company, o.total_cents, o.placed_at FROM customers c JOIN orders o ON o.customer_id = c.id) TO STDOUT WITH CSV HEADER\n", "2026-10-08 09:03:41 UTC [15233-2] app_ro@northstar LOG: duration: 337281.402 ms rows=412880 bytes=224395201 statement: COPY (SELECT ...) TO STDOUT WITH CSV HEADER\n", "2026-10-08 09:03:44 UTC [15233-1] app_ro@northstar LOG: disconnection: time=2026-10-08 08:57:44 UTC database=northstar user=app_ro host=192.0.2.20\n", "2026-10-08 10:38:12 UTC [15301-3] app@northstar LOG: connection authorized: user=app database=northstar application_name=northstar-app host=192.0.2.20\n", "\n", "exported rows : 412880\n", "exported bytes: 224395201\n" ] } ], "source": [ "PG = \"data/application/db01_postgresql.log\"\n", "pg = open(PG).read().splitlines()\n", "for l in pg:\n", " if \"connection authorized\" in l or \"COPY\" in l or \"disconnection\" in l:\n", " print(l.strip())\n", "for l in pg:\n", " if \"bytes=\" in l:\n", " rows = int(l.split(\"rows=\")[1].split()[0])\n", " by = int(l.split(\"bytes=\")[1].split()[0])\n", " print()\n", " print(\"exported rows :\", rows)\n", " print(\"exported bytes:\", by)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`app_ro` is a read-only reporting account. Its password was in the\n", "`.env` file read minutes after access (FILE-001). The COPY exported every\n", "customer row (APP-001..APP-004)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Network: the egress that carried the data" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "top external flows by bytes out:\n", " 2026-10-08T09:08:12.000Z 192.0.2.20 -> 198.51.100.60 :443 224402933 bytes\n", " 2026-10-08T09:11:31.000Z 192.0.2.60 -> 198.51.100.60 :443 12144896 bytes\n", " 2026-10-08T18:28:06.000Z 203.0.113.20 -> 203.0.113.10 :445 1390 bytes\n", " 2026-10-08T01:14:06.000Z 203.0.113.21 -> 203.0.113.10 :22 1305 bytes\n", " 2026-10-08T14:09:06.000Z 203.0.113.226 -> 203.0.113.10 :8443 1207 bytes\n", "\n", "db export bytes : 224395201\n", "largest egress bytes : 224402933\n", "difference (HTTP framing): 7732\n" ] } ], "source": [ "FLOWS = \"data/network/app01_flows.jsonl\"\n", "flows = [json.loads(l) for l in open(FLOWS) if l.strip()]\n", "ext = [f for f in flows if not f[\"dst\"].startswith(\"192.0.2.\")]\n", "ext.sort(key=lambda f: f[\"bytes_out\"], reverse=True)\n", "print(\"top external flows by bytes out:\")\n", "for f in ext[:5]:\n", " print(\" %s %-15s -> %-15s:%s %d bytes\" %\n", " (f[\"ts\"], f[\"src\"], f[\"dst\"], f[\"dport\"], f[\"bytes_out\"]))\n", "\n", "db_bytes = 224395201\n", "top = ext[0]\n", "print()\n", "print(\"db export bytes :\", 224395201)\n", "print(\"largest egress bytes :\", ext[0][\"bytes_out\"])\n", "print(\"difference (HTTP framing):\", ext[0][\"bytes_out\"] - 224395201)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The largest outbound transfer on the whole `app-01` sensor leaves the\n", "environment to a first-seen address. Its volume is the export plus TLS/HTTP\n", "overhead, which ties the database read to the wire (NET-002, PROC-017)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Lateral movement and the Windows staging host" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "2026-10-08T09:04:29.000Z id=4624 svc_backup src=192.0.2.20\n", "2026-10-08T09:07:37.000Z id=4698 \\OneDriveUpdate src=-\n", "2026-10-08T08:05:50.000Z id=4624 j.okafor src=-\n", "2026-10-08T10:34:46.000Z id=4699 \\OneDriveUpdate src=-\n", "\n", "PowerShell script blocks logged: 3\n" ] } ], "source": [ "WS = \"data/windows/ws02_security.jsonl\"\n", "win = [json.loads(l) for l in open(WS) if l.strip()]\n", "for e in win:\n", " if e[\"event_id\"] in (4624, 4698, 4699):\n", " extra = e.get(\"task_name\", e.get(\"target_user\", \"\"))\n", " print(\"%s id=%-4s %-16s src=%s\" % (\n", " e[\"ts\"], e[\"event_id\"], extra, e.get(\"source_ip\", \"-\")))\n", "print()\n", "ps = [json.loads(l) for l in open(\"data/windows/ws02_powershell.jsonl\")]\n", "print(\"PowerShell script blocks logged:\",\n", " sum(1 for e in ps if e.get(\"event_id\") == 4104))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A type-3 network logon to `ws-02` as `svc_backup` from `app-01` (the\n", "compromised host), followed by a scheduled task named to mimic a legitimate\n", "updater. This is lateral movement and a second persistence foothold\n", "(NET-003/NET-006, PERSIST-003, PROC-030..034)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5b. Consolidating the timeline" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "08:41:12 AUTH-001 first failed SSH password attempt\n", "08:43:17 AUTH-014 successful SSH login as deploy\n", "08:43:47 FILE-001 read of the application .env (app_ro password)\n", "08:45:10 PRIV-002 sudo -i: root shell\n", "08:49:15 PERSIST-001 malicious cron drop-in written\n", "08:50:22 PERSIST-002 attacker key appended to authorized_keys\n", "08:58:04 PROC-016 psql COPY of the customer table\n", "09:03:41 APP-003 export complete: 412,880 rows / 224,395,201 bytes\n", "09:03:51 NET-006 WinRM network logon to ws-02 as svc_backup\n", "09:06:59 PERSIST-003 scheduled task OneDriveUpdate created\n", "09:08:12 NET-002 224 MB egress to 198.51.100.60\n", "09:44:12 ACCOUNT-001 (response action) deploy disabled; containment starts\n" ] } ], "source": [ "timeline = [\n", " (\"08:41:12\", \"AUTH-001\", \"first failed SSH password attempt\"),\n", " (\"08:43:17\", \"AUTH-014\", \"successful SSH login as deploy\"),\n", " (\"08:43:47\", \"FILE-001\", \"read of the application .env (app_ro password)\"),\n", " (\"08:45:10\", \"PRIV-002\", \"sudo -i: root shell\"),\n", " (\"08:49:15\", \"PERSIST-001\", \"malicious cron drop-in written\"),\n", " (\"08:50:22\", \"PERSIST-002\", \"attacker key appended to authorized_keys\"),\n", " (\"08:58:04\", \"PROC-016\", \"psql COPY of the customer table\"),\n", " (\"09:03:41\", \"APP-003\", \"export complete: 412,880 rows / 224,395,201 bytes\"),\n", " (\"09:03:51\", \"NET-006\", \"WinRM network logon to ws-02 as svc_backup\"),\n", " (\"09:06:59\", \"PERSIST-003\", \"scheduled task OneDriveUpdate created\"),\n", " (\"09:08:12\", \"NET-002\", \"224 MB egress to 198.51.100.60\"),\n", " (\"09:44:12\", \"ACCOUNT-001 (response action)\", \"deploy disabled; containment starts\"),\n", "]\n", "for ts, eid, what in timeline:\n", " print(ts, \" \", \"%-11s\" % eid, what)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. What the evidence does not show" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Evidence for how the initial password was obtained: 0\n", "Direct internet access to db-01 (firewall): 0\n", "Distinct source addresses behind failed passwords (all accounts): 15\n", " -> the dataset deliberately contains unrelated failed logins as noise\n" ] } ], "source": [ "print(\"Evidence for how the initial password was obtained:\",\n", " len([l for l in lines if \"phish\" in l.lower()]))\n", "print(\"Direct internet access to db-01 (firewall):\",\n", " sum(1 for l in open(\"data/network/firewall.log\") if \"192.0.2.30\" in l and \"203.0.113\" in l))\n", "srcs = {l.split(\"from \")[1].split()[0] for l in lines if \"Failed password\" in l}\n", "print(\"Distinct source addresses behind failed passwords (all accounts):\", len(srcs))\n", "print(\" -> the dataset deliberately contains unrelated failed logins as noise\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The intrusion started with *a* valid password, not necessarily a guessed\n", "one. Nothing in the telemetry explains how the attacker knew it, so that stays\n", "open (U-001). Likewise there is no direct internet path to the database, so\n", "the data left through `app-01` (persistence of the \"no direct DB exposure\"\n", "fact). Every attack login came from one source address; the other failed\n", "logins in the file are deliberate background noise and must not be attributed\n", "to this incident." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Summary\n", "\n", "| Question | Answer | Evidence |\n", "| --- | --- | --- |\n", "| How did they get in? | 11 failures then a success on `deploy` | AUTH-001..014 |\n", "| Did they escalate? | Yes, to root via `sudo -i` | PRIV-002 |\n", "| How did they persist? | cron, SSH key, scheduled task | PERSIST-001/002/003 |\n", "| What did they take? | 412,880 customer rows, 224 MB | APP-003, NET-002 |\n", "| Where did it go? | 198.51.100.60 (collection host) | NET-002 |\n", "| Did they move laterally? | Yes, app-01 -> ws-02 over WinRM | NET-006 |\n", "\n", "Reproduce everything: `bash scripts/reproduce.sh`." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }