
Proof-of-concept exploit for CVE-2026-44590, a command injection in Sherlock's GitHub Actions workflow enabling RCE and GITHUB_TOKEN exfiltration via pull_request_target.
Discovered & reported by: Astaruf
Full writeup: https://nstsec.com/en/posts/sherlock-rce-pull-request-target-cve-2026-44590/
Upstream advisory: sherlock-project/sherlock GHSA advisory
NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-44590
CVE Record: https://www.cve.org/CVERecord?id=CVE-2026-44590
This repository contains the proof-of-concept for CVE-2026-44590, a command injection in the validate_modified_targets.yml GitHub Actions workflow of sherlock-project/sherlock. Any GitHub user can open a pull request that triggers arbitrary command execution in the privileged CI context, exfiltrate the workflow's GITHUB_TOKEN, and auto-approve the malicious PR, all without any human interaction.
For the full technical writeup (root-cause analysis, exploitation walkthrough, impact discussion, and a chapter on what an attacker could do in real-world scenarios) see the blog post:
nstsec.com/en/posts/sherlock-rce-pull-request-target-cve-2026-44590
This README focuses exclusively on the PoC script: what it does, how to run it, and what to expect.
poc.pypoc.py is a single self-contained Python script (stdlib only) that automates the entire attack chain end-to-end:
sherlock-project/sherlock if neededmaster back to the pre-fix commit so the bug can be reproduced even after the upstream fixinteractsh-client)GITHUB_TOKEN from the OAST callback (in --mode exfil) and decodes it in clear textVULNERABILITY CONFIRMED or FIX VERIFIEDinteractsh-client)There is exactly one manual step required (clicking GitHub's "I understand my workflows" banner the first time per fork), because no public API exists to dismiss it. The script detects this case and pauses with a clear prompt.
python3 poc.py --fork-owner <your-github-username>
Forks the repo (if needed), syncs with upstream (patched master), opens a malicious PR, runs the attack chain, and reports FIX VERIFIED because the patched workflow blocks the payload before any shell command runs.
python3 poc.py --fork-owner <your-github-username> --vulnerable
Same as above, but first rolls the fork's master back to the pre-fix commit (271608fb). Expected verdict: VULNERABILITY CONFIRMED.
python3 poc.py --fork-owner <your-github-username> --vulnerable --mode exfil
The exfil payload dumps git config --list to the OAST and sleeps for 180 seconds to keep the workflow (and thus the GITHUB_TOKEN) alive. While the workflow is sleeping, the script extracts the token from the OAST log, decodes it, and immediately calls the GitHub API to approve the PR. The PR ends up approved by github-actions[bot].
gh (GitHub CLI), authenticated:
gh auth login
gitinteractsh-client (optional but recommended). When installed, the script auto-spawns it and verifies the callback in-script:
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
If you prefer to use your own OAST endpoint (Burp Collaborator, oast.fun via web UI, requestbin, etc.), pass it with --oast-url and the auto-verification step will be skipped.The script does not require a fork to exist beforehand, it creates one automatically.
--mode harmless (default)The payload is a single curl POST with a static confirmation string. No secrets are read, no API calls are made, the only side effect is the OAST callback. Use this to confirm the vulnerability exists without exposing any credential.
--mode exfilThe payload dumps git config --list (which contains the GITHUB_TOKEN base64-encoded under http.https://github.com/.extraheader) to the OAST, then sleeps for 180 seconds. The script then:
x-access-token:ghs_XXXXXXXX....x-access-token: prefix and uses the raw token to call POST /repos/<fork>/pulls/<n>/reviews with the standard approval payload ({"event":"APPROVE","body":"All checks passed. LGTM!"}).github-actions[bot], indistinguishable from legitimate CI automation.Once the approval is recorded, the script skips the rest of the workflow's 180-second sleep, since the attack chain is complete and waiting for the runner to time out adds nothing.
| Flag | Description |
|---|---|
--fork-owner <user> | Required. GitHub username that owns (or will own) the fork |
--fork-name <name> | Fork repository name (default: sherlock) |
--oast-url <url> | OAST endpoint receiving the callback. If omitted, the script auto-spawns interactsh-client and runs the in-script verdict |
--mode harmless|exfil | Payload type (default: harmless) |
--vulnerable | Force-reset the fork's master to the pre-fix commit (271608fb) before running. Implies --no-sync |
--no-sync | Skip syncing the fork with upstream (useful when testing a pinned commit) |
--base-branch <name> | PR target branch on the fork (default: master) |
--keep-branch | Do not delete the PoC branch after completion |
--no-poll | Skip workflow run polling and exit after PR creation |
By design, the PoC opens the PR from a branch on the fork to the master of the same fork. It does not target sherlock-project/sherlock directly. There are two reasons for this.
A pull request on a public repository is visible to anyone. The diff stays indexed even after the PR is closed, and the GitHub Actions logs are accessible via the web UI. Opening a PR with a working command injection payload on the upstream repository would effectively publish a functional exploit before maintainers had a chance to ship a fix. Anyone watching the repository could copy the payload, swap the OAST callback for a malicious endpoint, and use it to exfiltrate the real GITHUB_TOKEN.
The PoC uses a fork-to-fork PR to keep the exploit out of public view while still demonstrating the bug end-to-end.