Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 86 additions & 7 deletions src/network-services-pentesting/pentesting-web/git.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,103 @@ ls .git/hooks
* **TruffleHog v3+**: entropy+regex with automatic Git history traversal. `trufflehog git file://$PWD --only-verified --json > secrets.json`
* **Gitleaks** (v8+): fast regex ruleset, can scan unpacked tree or full history. `gitleaks detect -v --source . --report-format json --report-path gitleaks.json`

### Server-side Git integration RCE via `hooksPath` override
### Server-side Git integrations as an attack surface

Modern web apps that integrate Git repos sometimes **rewrite `.git/config` using user-controlled identifiers**. If those identifiers are concatenated into `hooksPath`, you can redirect Git hooks to an attacker-controlled directory and execute arbitrary code when the server runs native Git (e.g., `git commit`). Key steps:
If the application **clones attacker-controlled repositories** and later runs `git` operations from a web/API action (`status`, `diff`, `checkout`, `pull`, `merge`, `commit`, dependency sync, package install, branch switch...), treat Git itself as part of the **server-side attack surface**.

* **Path traversal in `hooksPath`**: if a repo name/dependency name is copied into `hooksPath`, inject `../../..` to escape the intended hooks directory and point to a writable location. This is effectively a [path traversal](../../pentesting-web/file-inclusion/README.md) in Git config.
* **Force the target directory to exist**: when the application performs server-side clones, abuse clone destination controls (e.g., a `ref`/branch/path parameter) to make it clone into `../../git_hooks` or a similar traversal path so intermediate folders are created for you.
### Git config / hook abuse when the backend uses the native Git CLI

If you can **write or append** to `.git/config`, several directives can turn a low-impact file write into **server-side command execution** during later Git actions:

| Directive | Common trigger | Abuse |
| --- | --- | --- |
| `core.fsmonitor` | `git status`, `git diff` | Executes an external helper on common working-tree operations |
| `core.sshCommand` / `core.gitProxy` | `git fetch`, `git push`, `git clone` | Replaces the transport command |
| `credential.helper` | Authenticated Git operations | Runs a shell helper for credential lookup/storage |
| `filter.<name>.clean` / `filter.<name>.smudge` | `git add`, `git checkout`, `git clone` | Executes external clean/smudge filters |
| `diff.external` | `git diff` | Executes an external diff tool |
| `core.hooksPath` | commit / checkout / merge / push | Redirects hook execution to an attacker-controlled directory |

* **Append-only writes can still work**: Git accepts duplicate INI sections and the **last value may win**, so appending a second `[core]` section can be enough.
* **Direct hook write** also works if you can plant files inside `.git/hooks/` such as `post-checkout`, `post-merge`, or `pre-commit`.
* This is mainly a **native Git CLI** primitive. **JGit/libgit2/go-git** usually do **not** execute the same hooks/config helpers, although they can still be exploitable through path and symlink handling.

### `hooksPath` overwrite via path traversal

Modern web apps sometimes **regenerate `.git/config` from user-controlled repo names, dependency names, refs, or workspace identifiers**. If one value lands inside `core.hooksPath` and another one controls **where the generated config is written**, you can chain both into RCE:

* **Path traversal in `hooksPath`**: if a repo/dependency name is copied into `hooksPath`, inject `../../..` to escape the intended hooks directory and point to a writable location. This is effectively a [path traversal](../../pentesting-web/file-inclusion/README.md) in Git config.
* **Overwrite another repo's `.git/config`**: if `ref` / branch / destination path controls where generated Git metadata is written, traverse into another workspace and replace its config.
* **Force intermediate directories to exist**: abuse clone destination controls so the backend creates paths such as `../../git_hooks` for you.
* **Ship executable hooks**: set the executable bit inside Git metadata so every clone writes the hook with mode `100755`:
```bash
git update-index --chmod=+x pre-commit
```
Add your payload (reverse shell, file dropper, etc.) to `pre-commit`/`post-commit` in that repo.
* **Find a native Git code path**: libraries like **JGit** ignore hooks. Hunt for deployment flows/flags that fall back to system Git (e.g., forcing deploy-with-attached-repo parameters) so hooks will actually run.
* **Race the config rewrite**: if the app sanitizes `.git/config` right before running Git, spam the endpoint that writes your malicious `hooksPath` while triggering the Git action to win a [race condition](../../pentesting-web/race-condition.md) and get your hook executed.
* **Find a native Git code path**: libraries like **JGit** ignore hooks. Hunt for features that fall back to system Git so hooks will actually run.
* **Race the config rewrite**: if the app restores `.git/config` right before invoking Git, keep overwriting it while triggering the Git action to win a [race condition](../../pentesting-web/race-condition.md).

### Buried bare repositories & repo-detection abuse

Git discovers repositories by walking upward and looking for `HEAD`, `config`, `objects/`, and `refs/`. Therefore, an attacker can **bury a bare repository inside a normal repository subdirectory** and wait until the service runs Git from there.

1. Create a nested bare repo.
2. Change `bare = true` to `bare = false`.
3. Add `core.worktree` and an execution directive such as `core.fsmonitor`.
4. If the service later `cd`s into that folder and runs `git status` / `git diff`, Git loads the buried config and executes your helper.

Also check whether deleting/corrupting `.git/HEAD` could make Git stop recognizing the intended repository and **fall through** to a planted bare repo in the same or parent directory.

### Git argument injection and unsafe shell construction

When untrusted **filenames, branch names, refs, or paths** are passed to Git without `--`, values starting with `-` / `--` may be parsed as **options** instead of positional arguments.

```bash
# Vulnerable
git checkout $branch
git rm $path

# Safer
git checkout -- "$branch"
git rm -- "$path"
```

Interesting primitives to test:

* `--pathspec-from-file=<file>` with `git rm` to make Git read pathspecs from an arbitrary file and leak content via errors.
* Any workflow that joins argv into a **shell string** instead of using a real argument array.
* Cases where the backend safely escapes Git arguments but still embeds an **unescaped working directory** in a shell command like `cd #{working_directory} && git ...`; if you can control the directory name, shell syntax such as `$(id)` may execute before Git starts.

### Symlink-based repository escape

Git stores symlinks as blob targets. If checkout materializes **real symlinks** and the web UI/API **follows symlinks**, a repository-scoped file read/write primitive can become **filesystem escape**:

```text
repo/
link -> ../../../etc/
```

Then `repo/link/passwd` may resolve to `/etc/passwd`, and writes through that path may escape the repository boundary too.

Extra pivot points:

* **Blacklist bypass via repo-internal symlinks**: if the app blocks direct `.git/` reads with a prefix check, look for alternative paths such as `node_modules/pkg/.git/config` that resolve back to the real repo root through symlinks.
* **JGit `core.symlinks` abuse**: even if JGit ignores classic config-to-RCE directives, writing `symlinks = true` in `.git/config` can make the next pull/checkout materialize attacker-controlled symlinks, exposing broader filesystem paths or even other tenants' repositories if storage is shared.

### Quick checklist when you find a Git-backed file primitive

* **File read**: inspect `.git/config` for credentials, remote URLs, internal paths, repo layout, and deployment metadata.
* **File write / append**: target `.git/config`, `.git/hooks/*`, or alternate repo markers (`HEAD`, `config`, `objects/`, `refs/`) to escalate into command execution or repo confusion.
* **File delete**: test whether removing `.git/HEAD` changes which repository Git discovers.
* **Path traversal / arbitrary folder creation**: look for generated clone destinations, dependency sync paths, worktrees, hook paths, and package install directories.
* **Package installation features**: local npm dependencies (`"pkg": "./"`, `file:`) may create symlinks under `node_modules/` that help bypass path blacklists and reach `.git/config` indirectly.

## References

- [holly-hacker/git-dumper – parallel fast /.git dumper](https://github.com/holly-hacker/git-dumper)
- [Ebryx/GitDump](https://github.com/Ebryx/GitDump)
- [The gift that keeps giving: Exploiting Git Integrations in Cloud Services](https://nopnop.pro/2026/06/17/exploiting-git-integrations-in-cloud-services/)
- [Argument Injection Vectors (SonarSource)](https://sonarsource.github.io/argument-injection-vectors/)
- [Git buried bare repos and fsmonitor abuses (justinsteven)](https://github.com/justinsteven/advisories/blob/main/2022_git_buried_bare_repos_and_fsmonitor_various_abuses.md)
- [LookOut: RCE and internal access on Looker (Tenable)](https://www.tenable.com/blog/google-looker-vulnerabilities-rce-internal-access-lookout)

{{#include ../../banners/hacktricks-training.md}}