Back to all posts
Critical Malware Go RubyGems

BufferZoneCorp:
Go modules and Ruby gems that hijack your CI, still live on GitHub five months after takedown

Ten Go modules and seven Ruby gems from one account steal SSH keys and cloud credentials, plant an SSH backdoor, and rewire GitHub Actions jobs so that later builds pull dependencies through the attacker. The registries have since removed them. The source code was still public on GitHub on October 2, 2026.

CyberXYZ Security Team Threat Intelligence
11 min read
If any BufferZoneCorp Go module or knot-* gem ever entered a build, treat that machine as compromised

The Go payloads run from init() the moment a package is imported, and the gems run theirs during gem install or as soon as they are loaded. Rotate every SSH key, cloud credential, registry token and GitHub token the machine or runner could reach, remove the attacker keys from ~/.ssh/authorized_keys, and check .git/hooks/post-commit in every repository that was checked out on it.

Summary

What happened

On April 20, 2026 at 12:06 UTC a GitHub account called BufferZoneCorp was created. Between 12:28 and 13:26 UTC, about 80 minutes, it published fifteen repositories: Go libraries with ordinary names (go-retryablehttp, grpc-client, net-helper, go-metrics-sdk, log-core) and Ruby gems dressed as Rails tooling (activesupport-logger, devise-jwt-helper, rack-session-store, rails-assets-pipeline). config-loader followed on April 21 and go-stdlog on April 24. Each one has a tidy README and working library code. Most of them also carry a payload.

Socket found the campaign and published a detailed analysis on May 1, 2026, covering the credential theft, the SSH key, the Go proxy and logrus swap, the fake go binary and the HTTPS_PROXY redirect. On May 13 the packages were listed in OSV. Today proxy.golang.org returns no versions for any of the ten modules, and all seven knot-* gems are gone from rubygems.org.

The source code is a different story. When we checked on October 2, 2026, all seventeen repositories were still public on GitHub, with every payload intact. A Go module can be fetched directly from its repository with GOPROXY=direct or GOPRIVATE, and anyone can copy the code into a new module. We reported the account to GitHub the same day, and read every repository in full. This post describes the whole campaign and marks what we found that had not been reported before.

The registries took the packages down in May. The code that steals your keys stayed one git clone away.

Timeline

The packages

The Go modules split into roles. Some steal, some rewire the build, one does reconnaissance, and a few are clean decoys that make the account look like a real publisher.

PackageRoleWhat it does
Gogo-stdlib-extSteals SSH keys, cloud, registry and Git credentials; plants an attacker SSH key; disables Go checksum verification and installs a fake go binary in GitHub Actions.
Gogrpc-client, net-helperSame stealer; adds a post-commit git hook that reports the repository URL. grpc-client also installs a fake kubectl that sends every command line out.
Goconfig-loaderSame stealer and git hook, with the collector left pointing at localhost:9999 unless overridden.
Gogo-retryablehttpIn GitHub Actions, routes all job traffic through an attacker proxy and installs a fake go that leaks GH_TOKEN. Named after HashiCorp's library.
Gogo-metrics-sdk, go-weather-sdkIn GitHub Actions, point GOPROXY at the attacker, turn off the checksum database and delete logrus from go.sum.
Gogo-stdlogReconnaissance: token presence, Docker socket, Kubernetes service account, container capabilities, cloud metadata reachability.
Golog-core, go-envconfigClean at the latest commit. log-core carried the stealer in its first commit.
RubyGemsknot-activesupport-logger, knot-devise-jwt-helper, knot-rack-session-store, knot-rails-assets-pipeline, knot-rspec-formatter-jsonCredential stealer with RUBYOPT persistence in GitHub Actions. Two run at install time from a native-extension build script, three when the gem is loaded. knot-rack-session-store plants an SSH key; knot-rails-assets-pipeline installs the git hook.
RubyGemsknot-date-utils-rb, knot-simple-formatterShip an empty native-extension build script and no payload; decoys that make the pattern look normal.

How the main stealer works

go-stdlib-ext carries the most complete payload. Importing any package from it is enough; nothing has to be called.

1
Import runs it

A package-level init() starts a goroutine that waits 100 ms and then begins. There is no install hook in Go, so the payload runs in go test, go build code generation, and every binary that links the package.

2
Every string is hidden

File paths, the collector URL and the keyword list are byte slices XORed with a short key (sysutil1), and other names are split into fragments such as "GITHUB", "_ENV" and joined at runtime. A plain text search of the source finds none of them.

3
Secrets are collected

It reads the first 4 KB of ~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.aws/credentials, ~/.npmrc, ~/.netrc, ~/.config/gh/hosts.yml, ~/.docker/config.json and ~/.kube/config, plus environment variables selected with a keyword filter (token, secret, pass, aws, github, credential and others). The filter is written with strings.ContainsAny, which matches any single letter of those words, so in practice it takes almost every variable in the environment.

4
Everything is posted out

The bundle, with hostname, username and a CI flag, is sent as JSON to a webhook[.]site URL. The variable PKG_ANALYTICS_URL overrides the destination, so the same build can be pointed at private infrastructure.

5
An SSH backdoor is planted

If the upload succeeds, it appends an ssh-ed25519 key with the comment deploy@buildserver to ~/.ssh/authorized_keys, creating the directory if needed. On a developer laptop or a self-hosted runner, that is persistent remote access.

6
The CI job is rewired

Inside GitHub Actions it writes GONOSUMCHECK=* and GONOSUMDB=* to $GITHUB_ENV, drops a fake go script that reports its arguments and then runs the real toolchain, and adds that directory to the front of $GITHUB_PATH. Every later step in the job runs through it.

go-stdlib-ext/sysutil/sysutil.go (excerpt)func init() { go func() { time.Sleep(100 * time.Millisecond) if err := _submit(); err == nil { // POST keys, credentials, env _syncKeys() // append attacker key to authorized_keys _configureRuntime() // $GITHUB_ENV + fake go on $GITHUB_PATH } }() } f.WriteString(_j("GON", "OSU", "MCHECK=*\n")) f.WriteString(_j("GON", "OSU", "MDB=*\n"))

The CI attacks are the interesting part

Stealing files from a home directory is common. What sets this campaign apart is how much effort goes into GitHub Actions, where two files decide what every later step sees: $GITHUB_ENV sets environment variables for the rest of the job, and $GITHUB_PATH puts directories in front of PATH. A dependency that writes to them during one step controls the steps that follow.

Swapping a dependency through the Go proxy

go-metrics-sdk and go-weather-sdk write GOPROXY=<collector>|direct, GOSUMDB=off, GONOSUMDB=*, GOFLAGS=-mod=mod and a fresh GOMODCACHE to $GITHUB_ENV. Then they delete every github.com/sirupsen/logrus line from the project's go.sum. The effect: the next go command in the job has no recorded hash for logrus, no checksum database to ask, an empty module cache and an attacker-controlled proxy. It fetches logrus from the attacker. These two modules contain no stealer at all; their job is to make the build pull something else. The collector here is a webhook[.]site URL, which cannot serve modules, so as published this step would fail the build rather than swap anything. The mechanism is complete; only the server is missing.

The collector address is written as a list of what look like IP addresses, "104.116.116.112", "115.58.47.47", .... They are not addresses: each number is a character code, and the list decodes to the same webhook[.]site URL.

Tokens through a proxy header

go-retryablehttp takes a different route. It writes HTTPS_PROXY and HTTP_PROXY pointing at the collector into $GITHUB_ENV, so outbound traffic from later steps is directed there, and drops a fake go into the Go build cache that runs curl with X-Token: ${GH_TOKEN}, X-Run-Id and X-Repo sent as proxy headers before handing off to the real toolchain. The URL it requests belongs to a second module path, github[.]com/hashitools/go-retryablehttp, a HashiCorp lookalike that does not exist.

Git hooks and a fake kubectl

grpc-client, net-helper and config-loader walk up from the working directory to the nearest .git and prepend a line to .git/hooks/post-commit that sends the base64-encoded origin URL to the collector after every commit. The hook keeps working after the module is removed. grpc-client also places a fake kubectl first on the CI path that sends each command line, base64-encoded, before running the real one.

The Ruby side

Five of the seven knot-* gems carry a payload. Two run it at install time: they declare a native extension, and RubyGems runs the extension's ext/extconf.rb during gem install, before any application code loads. knot-activesupport-logger does this directly; knot-devise-jwt-helper keeps its version in a base64 blob handed to eval. The other three, knot-rack-session-store, knot-rails-assets-pipeline and knot-rspec-formatter-json, run when the gem is required. The two remaining gems ship an empty build script and no payload, so the native-extension pattern looks normal.

Each payload reads the same files as the Go stealer (SSH keys, cloud and registry credentials) and posts them with Net::HTTP. The default destination is hxxp://localhost:9999/collect; the real one comes from PKG_ANALYTICS_URL. Three things in the Ruby code had not been reported before:

knot-rails-assets-pipeline only runs its stealer when RAILS_ENV looks like production or staging, when GITHUB_ACTIONS is set, or when PKG_TELEMETRY_ENABLED is set, which keeps it quiet on most developer laptops. knot-rspec-formatter-json records which CI system it is running in.

Why this was easy to miss

There is no install script in Go, nothing runs from go.mod, and every sensitive string in these modules is XORed, split into fragments or encoded as numbers. The code that touches $GITHUB_ENV only runs when that variable exists, so a scan on a laptop sees a library that does nothing. Static checks have to look for the shape of the behaviour, not the strings.

Indicators of compromise

TypeIndicatorContext
Collector URLhxxps://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055fDefault destination in the Go modules (XOR-encoded or written as fake IPs). Also listed in the OSV records.
Collector URLhxxp://localhost:9999/collectDefault in the Ruby gems and config-loader; overridden by PKG_ANALYTICS_URL.
Environment variablePKG_ANALYTICS_URLOverrides the collector in every payload.
SSH keyssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBp9VZGMxqFpTwKbKJi7dS2mNrX3LqEoHcYsWfAkZvUt deploy@buildserverAppended to ~/.ssh/authorized_keys by go-stdlib-ext.
SSH keyssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBcz8KqXJn2mP7dWvL9oRtYqNfEuHsAkGpMxZwQiVjTl deploy@rack-sessionAppended to ~/.ssh/authorized_keys by knot-rack-session-store. New in this report.
CI environmentRUBYOPT=-r<home>/.{activesupport,devise-jwt,rails-assets,rspec-fmt}/lib/monitor.rb, .rack-session/bin/monitor.rbWritten to $GITHUB_ENV by five gems; loads the attacker file into every later Ruby process. New in this report.
File~/.config/{sysutil,grpcclient,netutil,cfgloader}/bin/Directories holding fake go or kubectl wrappers, added to $GITHUB_PATH.
File$GOCACHE/goFake go dropped by go-retryablehttp.
File.git/hooks/post-commit containing gc?r=Beacon reporting the repository URL after each commit (three Go modules and knot-rails-assets-pipeline). New in this report.
File$TMPDIR/diag-<hostname>.txtFull environment dump written by go-stdlog outside CI.
CI environmentGONOSUMDB=*, GONOSUMCHECK=*, GOSUMDB=off, GOPROXY=<unknown>|directWritten to $GITHUB_ENV by a dependency rather than by your workflow.
GitHub accountgithub[.]com/BufferZoneCorpCreated 2026-04-20; all 17 repositories.
Module pathgithub[.]com/hashitools/go-retryablehttpLookalike path requested by the fake go; does not exist.
How to check

Search go.mod, go.sum and vendored code for BufferZoneCorp and hashitools, and Gemfile.lock for knot-. On developer machines and self-hosted runners, look for deploy@buildserver in ~/.ssh/authorized_keys, the ~/.config/*/bin directories above, and gc?r= in .git/hooks/post-commit. In GitHub Actions logs, look for GONOSUMDB, GOSUMDB=off or an unexpected GOPROXY or HTTPS_PROXY that your workflow did not set.

How CyberXYZ handles it

The ten Go modules have been in CyberXYZ's malicious-package data since May 14, 2026, and the CyberXYZ Go proxy, CLI and VS Code extension return decision: block for them. That covers installs that go through the CyberXYZ Go module proxy. It does not cover code fetched with GOPROXY=direct or copied into another module, which is why the live repositories matter. The seven gems were only added on September 29, 2026, through an OSV backfill, so CLI and SBOM scans flag them from that date. We do not run a RubyGems proxy, so nothing of ours would have stopped a gem install.

Writing this teardown also exposed a gap in our own pipeline, and we would rather say so. When we replayed go-retryablehttp through our Go content scanner, it scored zero: our checks looked for attack classes we had anticipated, such as standard-library impostors, suspicious replace directives and risky go:generate lines, and none of them describe what this code does. We have added checks for the behaviour seen here and in the earlier Go typosquat waves: writes to $GITHUB_ENV or $GITHUB_PATH from a package with an init(), XOR-decoded byte literals, a binary named go written next to a lookup of the real one, commands built from single-character strings, request-catcher endpoints, and init() spawning sh -c. The same module now scores 95 out of a promotion threshold of 80. Before shipping, we ran the checks over 2,691 real Go modules, including 207 of the most depended-on ones and 1,451 new modules from one week of the public Go module index. None reached our notification threshold.

Supply-chain risk analysis

Mapped to MITRE ATT&CK:

Initial AccessT1195.001Compromise Software Dependencies and Development Tools
ExecutionT1204.005User Execution: Malicious Library
ExecutionT1059.004Unix Shell (wrappers, hooks)
Credential AccessT1552.001Credentials in Files
Credential AccessT1552.004Private Keys
PersistenceT1098.004SSH Authorized Keys
PersistenceT1574.007Path Interception ($GITHUB_PATH)
Defense EvasionT1027Obfuscated Files (XOR, split strings)
Defense EvasionT1562.001Disable checksum verification
ExfiltrationT1567.004Exfiltration Over Webhook

Recommended actions

Immediate. Remove any of the packages above. On every machine or self-hosted runner that built code importing them, rotate SSH keys, cloud credentials, npm and Docker tokens, gh tokens and Kubernetes credentials; delete the deploy@buildserver key from authorized_keys; clean .git/hooks/post-commit in affected repositories; and remove the ~/.config/*/bin wrapper directories.

Short-term. Treat a dependency that writes to $GITHUB_ENV or $GITHUB_PATH as hostile. Pin GOPROXY, GOSUMDB and GONOSUMDB explicitly in workflows so a later step cannot quietly change them, and give jobs the narrowest GITHUB_TOKEN permissions they need.

Long-term. A registry takedown removes the package, not the code. Route Go and Ruby installs through a checking proxy, and do not allow GOPROXY=direct fallbacks in CI, so that a known-malicious module is refused however it is requested.


References

  1. Socket, Malicious Ruby Gems and Go Modules Steal Secrets and Poison CI (May 1, 2026)
  2. OSV, MAL-2026-3622: Malicious code in github.com/BufferZoneCorp/go-metrics-sdk (Go)
  3. OSV, MAL-2026-3624: Malicious code in github.com/BufferZoneCorp/go-stdlib-ext (Go)
  4. OSV, MAL-2026-3632: Malicious code in knot-devise-jwt-helper (RubyGems)
  5. GitHub Docs, Security hardening for GitHub Actions
  6. MITRE ATT&CK, T1098.004 Account Manipulation: SSH Authorized Keys
  7. MITRE ATT&CK, T1195.001 Compromise Software Dependencies and Development Tools
👋

Let's Talk

Want to learn how CyberXYZ protects your supply chain? We'd love to hear from you.