# [HIGH] How Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCF's Backstage Repository

**Source:** StepSecurity
**Published:** 2025-12-15
**Article:** https://www.stepsecurity.io/blog/how-harden-runner-detected-the-sha1-hulud-supply-chain-attack-in-cncfs-backstage-repository

## Threat Profile

Back to Blog Threat Intel How Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCF's Backstage Repository A case study on detecting npm supply chain attacks through runtime monitoring and baseline anomaly detection Varun Sharma View LinkedIn December 3, 2025
Share on X Share on X Share on LinkedIn Share on Facebook Follow our RSS feed 
Table of Contents Loading nav... 
Introduction On November 23-24, 2025, the npm ecosystem experienced one of its largest coordinated supply chain att…

## Indicators of Compromise (high-fidelity only)

- **Domain (defanged):** `bun.sh`
- **Domain (defanged):** `oss.trufflehog.org`
- **Domain (defanged):** `keychecker.trufflesecurity.com`
- **Domain (defanged):** `webhook.site`
- **SHA256:** `46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09`
- **SHA256:** `62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0`
- **SHA256:** `f099c5d9ec417d4445a0328ac0ada9cde79fc37410914103ae9c609cbc0ee068`
- **SHA256:** `cbb9bc5a8496243e02f3cc080efbe3e4a1430ba0671f2e43a202bf45b05479cd`
- **SHA256:** `a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a`
- **SHA256:** `b74caeaa75e077c99f7d44f46daaf9796a3be43ecf24f2a1fd381844669da777`
- **SHA256:** `dc67467a39b70d1cd4c1f7f7a459b35058163592f4a9e8fb4dffcbba98ef210c`
- **SHA256:** `4b2399646573bb737c4969563303d8ee2e9ddbd1b271f1ca9e35ea78062538db`
- **SHA1:** `d1829b4708126dcc7bea7437c04d1f10eacd4a16`
- **SHA1:** `d60ec97eea19fffb4809bc35b91033b52490ca11`
- **SHA1:** `3d7570d14d34b0ba137d502f042b27b0f37a59fa`

## MITRE ATT&CK Techniques

- **T1059.001** — PowerShell
- **T1027** — Obfuscated Files or Information
- **T1195.002** — Compromise Software Supply Chain
- **T1071** — Application Layer Protocol
- **T1204.002** — User Execution: Malicious File
- **T1195.002** — Supply Chain Compromise: Compromise Software Supply Chain
- **T1105** — Ingress Tool Transfer
- **T1059.007** — Command and Scripting Interpreter: JavaScript
- **T1133** — External Remote Services
- **T1543** — Create or Modify System Process
- **T1078** — Valid Accounts
- **T1546** — Event Triggered Execution

## Kill chain phases observed

_(none detected from narrative keywords)_

## Recommended hunts

### Sha1-Hulud npm Worm — Egress to bun.sh / oss.trufflehog.org / keychecker.trufflesecurity.com from npm/node context

`UC_652_5` · phase: **install** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(DNS.query) as query values(DNS.dest) as dest values(DNS.src) as src from datamodel=Network_Resolution.DNS where (DNS.query IN ("bun.sh","oss.trufflehog.org","keychecker.trufflesecurity.com") OR DNS.query="*.bun.sh" OR DNS.query="*.trufflehog.org" OR DNS.query="*.trufflesecurity.com") by host DNS.src DNS.query | `drop_dm_object_name(DNS)` | stats min(firstTime) as firstTime max(lastTime) as lastTime dc(query) as distinct_iocs values(query) as ioc_domains values(dest) as dest_ips by host src | where distinct_iocs >= 1 | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | sort - lastTime
```

**Defender KQL:**
```kql
let _iocs = dynamic(["bun.sh","oss.trufflehog.org","keychecker.trufflesecurity.com"]);
let _npm_ctx = dynamic(["node.exe","node","npm.exe","npm-cli.js","yarn.exe","yarn","pnpm.exe","pnpm","bun.exe","bun","bash","sh","runner.listener.exe","runner.worker.exe"]);
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where RemoteUrl has_any (_iocs)
     or RemoteUrl endswith ".bun.sh"
     or RemoteUrl endswith ".trufflehog.org"
     or RemoteUrl endswith ".trufflesecurity.com"
| extend NpmContext = InitiatingProcessFileName in~ (_npm_ctx)
| summarize FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
            DistinctIocDomains = dcount(RemoteUrl),
            IocsHit = make_set(RemoteUrl, 10),
            InitiatingProcs = make_set(InitiatingProcessFileName, 10),
            SampleCmd = any(InitiatingProcessCommandLine),
            RemoteIPs = make_set(RemoteIP, 10),
            AnyNpmContext = max(tolong(NpmContext))
          by DeviceId, DeviceName, InitiatingProcessAccountName
| where DistinctIocDomains >= 2 or AnyNpmContext == 1
| order by FirstSeen desc
```

### Sha1-Hulud npm Worm — Self-Hosted GitHub Actions Runner Registration with Name 'SHA1HULUD'

`UC_652_6` · phase: **install** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Processes.process) as process values(Processes.process_path) as process_path values(Processes.parent_process_name) as parent_process_name values(Processes.parent_process) as parent_process from datamodel=Endpoint.Processes where (Processes.process="*SHA1HULUD*" OR (Processes.process_name IN ("config.cmd","config.sh","Runner.Listener.exe","Runner.Listener") AND Processes.process="*--name*" AND (Processes.process="*runner-registration*" OR Processes.process="*--token*"))) by host Processes.user Processes.process_name | `drop_dm_object_name(Processes)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | sort - lastTime
```

**Defender KQL:**
```kql
DeviceProcessEvents
| where Timestamp > ago(30d)
| where ProcessCommandLine has "SHA1HULUD"
     or InitiatingProcessCommandLine has "SHA1HULUD"
     or (FileName in~ ("config.cmd","config.sh","Runner.Listener.exe","Runner.Listener")
         and ProcessCommandLine has "--name"
         and ProcessCommandLine has_any ("runner-registration","--token","--unattended"))
| where AccountName !endswith "$"
| project Timestamp, DeviceName, AccountName,
          FileName, FolderPath, ProcessCommandLine,
          ParentImage = InitiatingProcessFileName,
          ParentCmd   = InitiatingProcessCommandLine,
          GrandParent = InitiatingProcessParentFileName,
          SHA256
| order by Timestamp desc
```

### Sha1-Hulud npm Worm — Drop of setup_bun.js / bun_environment.js / discussion.yaml by node or shell

`UC_652_7` · phase: **install** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Filesystem.file_path) as file_path values(Filesystem.process_name) as process_name values(Filesystem.process_path) as process_path from datamodel=Endpoint.Filesystem where (Filesystem.file_name IN ("setup_bun.js","bun_environment.js") OR Filesystem.file_path="*\\.github\\workflows\\discussion.yaml" OR Filesystem.file_path="*/.github/workflows/discussion.yaml") AND Filesystem.process_name IN ("node.exe","node","npm.exe","npm-cli.js","yarn.exe","yarn","pnpm.exe","pnpm","bun.exe","bun","bash","sh","cmd.exe","powershell.exe") by host Filesystem.user Filesystem.file_name | `drop_dm_object_name(Filesystem)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | sort - lastTime
```

**Defender KQL:**
```kql
let _npm_writers = dynamic(["node.exe","node","npm.exe","npm-cli.js","yarn.exe","yarn","pnpm.exe","pnpm","bun.exe","bun","bash","sh","cmd.exe","powershell.exe","pwsh.exe"]);
DeviceFileEvents
| where Timestamp > ago(14d)
| where ActionType in ("FileCreated","FileModified","FileRenamed")
| where FileName in~ ("setup_bun.js","bun_environment.js")
     or FolderPath has @"\.github\workflows\discussion.yaml"
     or FolderPath has "/.github/workflows/discussion.yaml"
| where InitiatingProcessFileName in~ (_npm_writers)
| project Timestamp, DeviceName, InitiatingProcessAccountName,
          FileName, FolderPath, SHA256,
          InitiatingProcessFileName, InitiatingProcessCommandLine,
          InitiatingProcessParentFileName
| order by Timestamp desc
```

### PowerShell encoded / obfuscated command

`UC_PS_OBFUSCATED` · phase: **exploit** · confidence: **High**

**Splunk SPL (CIM):**
```spl
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
    from datamodel=Endpoint.Processes
    where Processes.process_name IN ("powershell.exe","pwsh.exe")
      AND (Processes.process="*-enc *" OR Processes.process="*EncodedCommand*"
        OR Processes.process="*FromBase64String*" OR Processes.process="*-nop*"
        OR Processes.process="*-w hidden*" OR Processes.process="*Invoke-Expression*"
        OR Processes.process="*IEX(*" OR Processes.process="*DownloadString*"
        OR Processes.process="*Net.WebClient*")
    by Processes.dest, Processes.user, Processes.process_name, Processes.process, Processes.parent_process_name
| `drop_dm_object_name(Processes)`
```

**Defender KQL:**
```kql
DeviceProcessEvents
| where Timestamp > ago(7d)
| where AccountName !endswith "$"
| where FileName in~ ("powershell.exe","pwsh.exe")
| where ProcessCommandLine matches regex @"(?i)(-enc|encodedcommand|frombase64string|-nop|-w\s+hidden|invoke-expression|iex\s*\(|downloadstring|net\.webclient)"
| project Timestamp, DeviceName, AccountName, ProcessCommandLine,
          InitiatingProcessFileName, InitiatingProcessCommandLine
```

### Trusted vendor binary / installer launching unusual children

`UC_SUPPLY_CHAIN` · phase: **exploit** · confidence: **Medium**

**Splunk SPL (CIM):**
```spl
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
    from datamodel=Endpoint.Processes
    where Processes.parent_process_name IN ("setup.exe","installer.exe","update.exe")
      AND Processes.process_name IN ("powershell.exe","cmd.exe","rundll32.exe","regsvr32.exe","mshta.exe","wscript.exe","cscript.exe","wmic.exe","bitsadmin.exe")
    by Processes.dest, Processes.user, Processes.parent_process_name, Processes.process_name, Processes.process
| `drop_dm_object_name(Processes)`
```

**Defender KQL:**
```kql
DeviceProcessEvents
| where Timestamp > ago(7d)
| where AccountName !endswith "$"
| where InitiatingProcessFileName in~ ("setup.exe","installer.exe","update.exe")
| where FileName in~ ("powershell.exe","cmd.exe","rundll32.exe","regsvr32.exe","mshta.exe","wscript.exe","cscript.exe","wmic.exe","bitsadmin.exe")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine
```

### Article-specific behavioural hunt — How Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCF's Backstag

`UC_652_4` · phase: **exploit** · confidence: **High**

**Splunk SPL (CIM):**
```spl
``` Article-specific bespoke detection — How Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCF's Backstag ```
| tstats `summariesonly` count earliest(_time) AS firstTime latest(_time) AS lastTime
    from datamodel=Endpoint.Processes
    where (Processes.process_name IN ("bun.sh"))
    by Processes.dest, Processes.user, Processes.process_name,
       Processes.process, Processes.parent_process_name, Processes.process_path
| `drop_dm_object_name(Processes)`
| `security_content_ctime(firstTime)`
| append [
| tstats `summariesonly` count
    from datamodel=Endpoint.Filesystem
    where Filesystem.action IN ("created","modified")
      AND (Filesystem.file_name IN ("bun.sh"))
    by Filesystem.dest, Filesystem.user, Filesystem.process_name,
       Filesystem.file_path, Filesystem.file_name
| `drop_dm_object_name(Filesystem)`
]
```

**Defender KQL:**
```kql
// Article-specific bespoke detection — How Harden Runner Detected the Sha1-Hulud Supply Chain Attack in CNCF's Backstag
// Hunts the actual binaries / paths / commandline fragments named
// in the article instead of a generic technique-class template.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where (FileName in~ ("bun.sh"))
| project Timestamp, DeviceName, AccountName, FileName,
          FolderPath, ProcessCommandLine,
          InitiatingProcessFileName, InitiatingProcessCommandLine
| order by Timestamp desc

// File-creation events for the named binaries / paths
DeviceFileEvents
| where Timestamp > ago(30d)
| where ActionType in ("FileCreated","FileModified")
| where (FileName in~ ("bun.sh"))
| project Timestamp, DeviceName, AccountName, FolderPath,
          FileName, ActionType, InitiatingProcessFileName,
          InitiatingProcessCommandLine
| order by Timestamp desc
```

### IOC-driven hunts (use shared templates)

These are standard IOC-substitution hunts — the canonical SPL and KQL live once in [`_TEMPLATES.md`](../_TEMPLATES.md), so we don't repeat the same boilerplate on every CVE / hash / network-IOC briefing.

- **Network connections to article IPs / domains** ([template](../_TEMPLATES.md#network-ioc)) — phase: **c2**, confidence: **High**
  - IP / domain IOC(s): `bun.sh`, `oss.trufflehog.org`, `keychecker.trufflesecurity.com`, `webhook.site`

- **File hash IOCs — endpoint file/process match** ([template](../_TEMPLATES.md#hash-ioc)) — phase: **install**, confidence: **High**
  - file hash IOC(s): `46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09`, `62ee164b9b306250c1172583f138c9614139264f889fa99614903c12755468d0`, `f099c5d9ec417d4445a0328ac0ada9cde79fc37410914103ae9c609cbc0ee068`, `cbb9bc5a8496243e02f3cc080efbe3e4a1430ba0671f2e43a202bf45b05479cd`, `a3894003ad1d293ba96d77881ccd2071446dc3f65f434669b49b3da92421901a`, `b74caeaa75e077c99f7d44f46daaf9796a3be43ecf24f2a1fd381844669da777`, `dc67467a39b70d1cd4c1f7f7a459b35058163592f4a9e8fb4dffcbba98ef210c`, `4b2399646573bb737c4969563303d8ee2e9ddbd1b271f1ca9e35ea78062538db` _(+3 more)_


## Why this matters

Severity classified as **HIGH** based on: IOCs present, 8 use case(s) fired, 12 technique(s) inferred. Read the full article for actor attribution, tooling details, and any defanged IOCs in the body that aren't visible in the RSS summary.
