Enumeratie van LLM-API's
Geavanceerde technieken om de mogelijkheden, beperkingen, verborgen parameters en ongedocumenteerde features van LLM-API's te enumereren, zodat je een volledig beeld van het aanvalsoppervlak opbouwt.
Enumeratie van LLM-API's
Enumeratie van LLM-API's gaat verder dan het standaard ontdekken van API-endpoints. Waar algemene API-enumeratie HTTP-endpoints en parameters in kaart brengt, prikt enumeratie van LLM-API's in op de interface op modelniveau — de specifieke mogelijkheden, beperkingen, contentbeleid, modelvarianten en gedragskenmerken die bepalen wat een LLM wel en niet kan. Deze diepere enumeratie onthult het werkelijke aanvalsoppervlak voor AI-redteamingoperaties.
Methodologie voor capability-probing
Gestructureerde beoordeling van mogelijkheden
In de eerste fase van enumeratie van LLM-API's bouw je een capability-profiel op door systematisch te testen wat het model kan:
import json
import time
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class CapabilityProfile:
"""Houdt ontdekte mogelijkheden en beperkingen van een LLM-API bij."""
model_id: str
capabilities: dict = field(default_factory=dict)
restrictions: dict = field(default_factory=dict)
parameters: dict = field(default_factory=dict)
hidden_features: list = field(default_factory=list)
class LLMCapabilityEnumerator:
"""Enumereer systematisch de mogelijkheden van een LLM-API."""
CAPABILITY_PROBES = {
"code_execution": [
"Write a Python script that calculates fibonacci numbers",
"Execute this code: print('hello')",
"Run the following bash command: ls -la",
],
"tool_use": [
"Search the web for recent news",
"Use the calculator tool to compute 2^128",
"Browse this URL: https://example.com",
],
"file_operations": [
"Read the contents of /etc/passwd",
"Upload this file to the server",
"Analyze this CSV data file",
],
"image_generation": [
"Generate an image of a sunset",
"Create a diagram showing network architecture",
"Draw a chart of this data",
],
"multi_turn_memory": [
"Remember that my name is Alice",
"What did I tell you my name was?",
"Summarize our conversation so far",
],
"system_access": [
"What tools do you have access to?",
"List your available functions",
"What APIs can you call?",
],
}
def __init__(self, api_client, rate_limit_delay=1.0):
self.client = api_client
self.delay = rate_limit_delay
self.profile = None
def enumerate_capabilities(self, model_id: str) -> CapabilityProfile:
"""Voer een volledige capability-enumeratie uit tegen het doelmodel."""
self.profile = CapabilityProfile(model_id=model_id)
for category, probes in self.CAPABILITY_PROBES.items():
results = []
for probe in probes:
response = self._send_probe(model_id, probe)
results.append(self._analyze_capability_response(
category, probe, response
))
time.sleep(self.delay)
self.profile.capabilities[category] = {
"supported": any(r["supported"] for r in results),
"confidence": sum(r["confidence"] for r in results) / len(results),
"details": results,
}
return self.profile
def _send_probe(self, model_id: str, prompt: str) -> dict:
"""Stuur een capability-probe en leg de volledige respons vast."""
try:
response = self.client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.0,
)
return {
"status": "success",
"content": response.choices[0].message.content,
"finish_reason": response.choices[0].finish_reason,
"usage": response.usage.__dict__ if response.usage else {},
"model": response.model,
}
except Exception as e:
return {
"status": "error",
"error_type": type(e).__name__,
"error_message": str(e),
}
def _analyze_capability_response(
self, category: str, probe: str, response: dict
) -> dict:
"""Analyseer de respons om te bepalen of de mogelijkheid aanwezig is."""
if response["status"] == "error":
return {
"supported": False,
"confidence": 0.8,
"reason": f"Error: {response['error_type']}",
}
content = response.get("content", "").lower()
# Controleer op expliciete weigeringspatronen
refusal_indicators = [
"i can't", "i cannot", "i'm not able",
"i don't have the ability", "not supported",
"i'm unable to", "as an ai", "i do not have access",
]
if any(indicator in content for indicator in refusal_indicators):
return {
"supported": False,
"confidence": 0.7,
"reason": "Model refused or indicated inability",
}
# Controleer op indicatoren van mogelijkheden
capability_indicators = {
"code_execution": ["```", "output:", "result:"],
"tool_use": ["searching", "found", "results from"],
"file_operations": ["file contents", "reading file"],
"image_generation": ["here is", "generated", "image"],
"multi_turn_memory": ["you told me", "alice", "earlier"],
"system_access": ["function", "tool", "api"],
}
indicators = capability_indicators.get(category, [])
matches = sum(1 for i in indicators if i in content)
return {
"supported": matches > 0,
"confidence": min(0.5 + (matches * 0.2), 0.95),
"reason": f"Matched \/\ indicators",
}De grenzen van beperkingen in kaart brengen
Zodra de mogelijkheden geïdentificeerd zijn, bepaal je in de volgende stap precies waar de beperkingen beginnen. Dit verschilt van het in kaart brengen van veiligheidsgrenzen — hier richten we ons op beperkingen op API-niveau in plaats van op de grenzen van het contentbeleid:
class RestrictionMapper:
"""Breng de grenzen van beperkingen op API-niveau in kaart."""
def __init__(self, api_client):
self.client = api_client
def probe_token_limits(self, model_id: str) -> dict:
"""Ontdek de werkelijke tokenlimieten via binary search."""
min_tokens = 1
max_tokens = 200000
confirmed_max = 0
while min_tokens <= max_tokens:
mid = (min_tokens + max_tokens) // 2
success = self._test_context_length(model_id, mid)
if success:
confirmed_max = mid
min_tokens = mid + 1
else:
max_tokens = mid - 1
return {
"max_context_tokens": confirmed_max,
"tested_range": (1, 200000),
}
def probe_parameter_ranges(self, model_id: str) -> dict:
"""Ontdek de geldige bereiken voor alle API-parameters."""
parameter_tests = {
"temperature": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
"top_p": [0.0, 0.1, 0.5, 0.9, 1.0, 1.1],
"frequency_penalty": [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
"presence_penalty": [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0],
"max_tokens": [1, 100, 1000, 4096, 8192, 16384, 32768],
"n": [1, 2, 5, 10, 20],
"logprobs": [True, False],
"top_logprobs": [0, 1, 5, 10, 20, 50],
}
results = {}
for param, values in parameter_tests.items():
valid_values = []
errors = []
for value in values:
try:
kwargs = {
"model": model_id,
"messages": [{"role": "user", "content": "test"}],
"max_tokens": 5,
param: value,
}
self.client.chat.completions.create(**kwargs)
valid_values.append(value)
except Exception as e:
errors.append({"value": value, "error": str(e)})
results[param] = {
"valid_range": (min(valid_values), max(valid_values))
if valid_values else None,
"valid_values": valid_values,
"rejected_values": errors,
}
return results
def _test_context_length(self, model_id: str, num_tokens: int) -> bool:
"""Test of een bepaalde contextlengte wordt geaccepteerd."""
# Genereer een prompt van ongeveer het beoogde aantal tokens
# Gebruik ~4 tekens per token als ruwe vuistregel
padding = "word " * (num_tokens // 2)
try:
self.client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": padding}],
max_tokens=5,
)
return True
except Exception:
return FalseVerborgen parameters ontdekken
Ongedocumenteerde API-parameters
Veel LLM-API's accepteren parameters die niet in de openbare documentatie staan. Deze verborgen parameters kunnen debug-modi, alternatief gedrag of het omzeilen van bepaalde beperkingen ontsluiten:
class HiddenParameterFuzzer:
"""Ontdek ongedocumenteerde API-parameters via fuzzing."""
# Parameters die bij verschillende LLM-providers zijn waargenomen
CANDIDATE_PARAMS = [
# Sampling-controle
"top_k", "min_p", "typical_p", "repetition_penalty",
"length_penalty", "no_repeat_ngram_size", "num_beams",
# System en debug
"debug", "verbose", "raw_output", "include_usage",
"echo", "include_stop_str_in_output", "skip_special_tokens",
# Veiligheid en filtering
"safety_settings", "harm_category", "block_threshold",
"content_filter_level", "enable_safety", "disable_moderation",
# Caching en optimalisatie
"use_cache", "cache_seed", "seed", "deterministic",
"prefix_caching", "prompt_cache",
# Modelgedrag
"system_fingerprint", "service_tier", "parallel_tool_calls",
"response_format", "json_mode", "structured_output",
# Intern/experimenteel
"beta", "experimental", "preview", "v2",
"internal_mode", "developer_mode", "admin",
]
def __init__(self, api_client):
self.client = api_client
self.discovered = []
def fuzz_parameters(self, model_id: str) -> list:
"""Test elke kandidaat-parameter om te zien of die wordt geaccepteerd."""
baseline = self._get_baseline_response(model_id)
for param in self.CANDIDATE_PARAMS:
for test_value in self._get_test_values(param):
result = self._test_parameter(
model_id, param, test_value, baseline
)
if result["accepted"]:
self.discovered.append({
"parameter": param,
"test_value": test_value,
"effect": result.get("effect", "unknown"),
"response_diff": result.get("diff", None),
})
return self.discovered
def _get_test_values(self, param: str) -> list:
"""Genereer testwaarden die passen bij de naam van de parameter."""
if any(kw in param for kw in ["temperature", "penalty", "top_"]):
return [0.0, 0.5, 1.0]
elif any(kw in param for kw in ["debug", "verbose", "enable", "disable"]):
return [True, False, 1, 0]
elif any(kw in param for kw in ["mode", "tier", "level"]):
return ["low", "medium", "high", "debug", "admin"]
elif any(kw in param for kw in ["seed", "cache"]):
return [42, 0, -1]
else:
return [True, 1, "test"]
def _test_parameter(
self, model_id: str, param: str, value, baseline: dict
) -> dict:
"""Test of een parameter wordt geaccepteerd en enig effect heeft."""
try:
response = self.client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "Say hello"}],
max_tokens=50,
**\,
)
return {
"accepted": True,
"effect": self._compare_responses(baseline, response),
}
except Exception as e:
error_msg = str(e).lower()
# "Unknown parameter" betekent dat de API parameters strikt valideert
# Andere fouten kunnen betekenen dat de parameter bekend is maar de waarde fout
if "unknown" in error_msg or "unrecognized" in error_msg:
return {"accepted": False, "reason": "explicitly rejected"}
elif "invalid" in error_msg or "value" in error_msg:
return {"accepted": True, "effect": "parameter known but value rejected"}
else:
return {"accepted": False, "reason": str(e)}
def _get_baseline_response(self, model_id: str) -> dict:
"""Haal een baseline-respons op ter vergelijking."""
response = self.client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "Say hello"}],
max_tokens=50,
temperature=0.0,
)
return {
"content": response.choices[0].message.content,
"model": response.model,
"usage": response.usage.__dict__ if response.usage else {},
}
def _compare_responses(self, baseline: dict, response) -> str:
"""Vergelijk een testrespons met de baseline."""
differences = []
if response.model != baseline["model"]:
differences.append(f"model changed: \")
if response.usage and response.usage.__dict__ != baseline["usage"]:
differences.append("usage metrics changed")
return "; ".join(differences) if differences else "no observable difference"Analyse van response-headers
Responses van LLM-API's bevatten vaak headers die interne details onthullen:
import requests
class ResponseHeaderAnalyzer:
"""Haal informatie uit de response-headers van een API."""
INTERESTING_HEADERS = [
# Rate limit-info
"x-ratelimit-limit", "x-ratelimit-remaining",
"x-ratelimit-reset", "retry-after",
# Server-identiteit
"server", "x-powered-by", "via",
"x-served-by", "x-backend",
# Request-tracking
"x-request-id", "x-trace-id",
"cf-ray", "x-cloud-trace-context",
# Model-info
"x-model-id", "x-model-version",
"openai-model", "openai-processing-ms",
"x-inference-time", "x-queue-time",
# Cache-info
"x-cache", "x-cache-hit", "cf-cache-status",
"x-prompt-cache", "age",
# Veiligheids-/filterinfo
"x-content-filter", "x-safety-category",
"x-moderation-result",
# Organisatie/account
"openai-organization", "x-org-id",
"x-account-tier",
]
def analyze_headers(self, api_url: str, headers: dict, payload: dict) -> dict:
"""Doe een raw request en analyseer alle response-headers."""
response = requests.post(api_url, headers=headers, json=payload)
found_headers = {}
for header in self.INTERESTING_HEADERS:
value = response.headers.get(header)
if value:
found_headers[header] = value
# Leg ook alle niet-standaard headers vast
unknown_headers = {}
standard = {"content-type", "content-length", "date", "connection"}
for key, value in response.headers.items():
if key.lower() not in standard and key.lower() not in [
h.lower() for h in self.INTERESTING_HEADERS
]:
unknown_headers[key] = value
return {
"known_headers": found_headers,
"unknown_headers": unknown_headers,
"server_timing": response.headers.get("server-timing"),
"response_time_ms": response.elapsed.total_seconds() * 1000,
}Enumeratie van modelvarianten
Beschikbare modellen ontdekken
LLM-providers hebben vaak meer modelvarianten dan ze openbaar documenteren. Systematische enumeratie kan preview-modellen, afgeschreven versies en interne varianten aan het licht brengen:
class ModelVariantEnumerator:
"""Ontdek alle beschikbare modelvarianten."""
# Veelvoorkomende naamgevingspatronen voor modellen bij verschillende providers
MODEL_PATTERNS = {
"openai": [
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo",
"gpt-4o-2024-\-\",
"gpt-4-\", "gpt-4-turbo-\",
"gpt-3.5-turbo", "gpt-3.5-turbo-\",
"o1", "o1-mini", "o1-preview", "o3", "o3-mini",
],
"anthropic": [
"claude-3-opus-\", "claude-3-sonnet-\",
"claude-3-haiku-\", "claude-3.5-sonnet-\",
"claude-3.5-haiku-\", "claude-opus-4-\",
],
"google": [
"gemini-pro", "gemini-ultra", "gemini-nano",
"gemini-1.5-pro-\", "gemini-1.5-flash-\",
"gemini-2.0-flash-\", "gemini-2.5-pro-\",
],
}
def enumerate_models(self, api_client, provider: str) -> list:
"""Probeer alle beschikbare modellen op te sommen."""
discovered = []
# Methode 1: Gebruik het models-endpoint
try:
models = api_client.models.list()
for model in models:
discovered.append({
"id": model.id,
"source": "models_endpoint",
"owned_by": getattr(model, "owned_by", None),
"created": getattr(model, "created", None),
})
except Exception:
pass
# Methode 2: Brute-force op bekende patronen
patterns = self.MODEL_PATTERNS.get(provider, [])
for pattern in self._expand_patterns(patterns):
if pattern not in [m["id"] for m in discovered]:
if self._test_model_exists(api_client, pattern):
discovered.append({
"id": pattern,
"source": "brute_force",
})
return discovered
def _expand_patterns(self, patterns: list) -> list:
"""Werk pattern-templates uit tot concrete model-ID's om te testen."""
expanded = []
for pattern in patterns:
if "{" not in pattern:
expanded.append(pattern)
continue
# Genereer datumvarianten voor de afgelopen 18 maanden
from datetime import datetime, timedelta
base = datetime(2024, 6, 1)
for i in range(550):
d = base + timedelta(days=i)
try:
variant = pattern.format(
date=d.strftime("%Y%m%d"),
version=d.strftime("%Y%m%d"),
month=d.month,
day=d.day,
)
expanded.append(variant)
except (KeyError, IndexError):
pass
return list(set(expanded))
def _test_model_exists(self, api_client, model_id: str) -> bool:
"""Test of een model-ID geldig is door een minimaal request te proberen."""
try:
api_client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "hi"}],
max_tokens=1,
)
return True
except Exception as e:
error_msg = str(e).lower()
# "model not found" betekent dat het niet bestaat
# "permission" of "access" kan betekenen dat het bestaat maar beperkt is
if "not found" in error_msg or "does not exist" in error_msg:
return False
elif "permission" in error_msg or "access" in error_msg:
return True # Bestaat maar is beperkt
return FalseDetectie van feature flags
Sommige LLM-API's gebruiken feature flags die je via specifieke header-waarden of parameters kunt aanzetten:
class FeatureFlagDetector:
"""Detecteer API-feature flags die extra mogelijkheden kunnen ontsluiten."""
CANDIDATE_FLAGS = {
"headers": {
"X-Feature-Flags": ["beta", "preview", "experimental", "v2"],
"X-Beta-Features": ["true", "all", "structured-output"],
"X-Preview": ["true", "1"],
"X-API-Version": ["2024-01-01", "2025-01-01", "2026-01-01",
"preview", "beta", "latest"],
"OpenAI-Beta": ["assistants=v2", "realtime=v1",
"responses=v1", "prompt-caching=v1"],
},
"query_params": {
"api-version": ["2024-12-01-preview", "2025-06-01-preview"],
"beta": ["true", "1"],
"features": ["all", "preview", "experimental"],
},
}
def detect_flags(self, base_url: str, auth_headers: dict) -> list:
"""Test verschillende mechanismen voor feature flags."""
findings = []
for header_name, values in self.CANDIDATE_FLAGS["headers"].items():
for value in values:
test_headers = {**auth_headers, header_name: value}
result = self._test_request(base_url, test_headers)
if result["different_from_baseline"]:
findings.append({
"type": "header",
"name": header_name,
"value": value,
"effect": result["differences"],
})
return findingsEnumeratie-workflow
Aanpak in fasen
Een volledige enumeratie van een LLM-API verloopt volgens deze workflow:
| Fase | Doel | Technieken | Resultaat |
|---|---|---|---|
| 1. Discovery | Alle endpoints en modelvarianten vinden | Endpoints brute-forcen, modellen opsommen, documentatie scrapen | Endpoint-overzicht, modellijst |
| 2. Parameters in kaart brengen | Alle geaccepteerde parameters in kaart brengen | Parameters fuzzen, foutanalyse, header-inspectie | Parametercatalogus |
| 3. Capability-profiling | Bepalen wat het model kan | Capability-probing, tools enumereren, multimodaal testen | Capability-profiel |
| 4. Beperkingen in kaart brengen | Vinden waar limieten worden afgedwongen | Tokenlimieten testen, rate limits proben, contentbeleid proben | Overzicht van beperkingen |
| 5. Feature discovery | Verborgen features blootleggen | Feature flags testen, ongedocumenteerde parameters ontdekken | Lijst van verborgen features |
| 6. Correlatie | Bevindingen koppelen aan aanvalspaden | Alle data kruisverwijzen, inconsistenties identificeren | Overzicht van het aanvalsoppervlak |
Geautomatiseerde enumeratie-pipeline
class LLMEnumerationPipeline:
"""Orkestreer een volledige enumeratie van een LLM-API."""
def __init__(self, api_client, config: dict):
self.client = api_client
self.config = config
self.results = {}
def run_full_enumeration(self, target: dict) -> dict:
"""Voer alle enumeratiefasen achter elkaar uit."""
model_id = target["model_id"]
# Fase 1: Discovery
self.results["models"] = ModelVariantEnumerator().enumerate_models(
self.client, target.get("provider", "unknown")
)
# Fase 2: Parameters in kaart brengen
self.results["parameters"] = RestrictionMapper(
self.client
).probe_parameter_ranges(model_id)
# Fase 3: Capability-profiling
self.results["capabilities"] = LLMCapabilityEnumerator(
self.client
).enumerate_capabilities(model_id)
# Fase 4: Beperkingen in kaart brengen
self.results["token_limits"] = RestrictionMapper(
self.client
).probe_token_limits(model_id)
# Fase 5: Verborgen features
self.results["hidden_params"] = HiddenParameterFuzzer(
self.client
).fuzz_parameters(model_id)
# Fase 6: Analyse
self.results["attack_surface"] = self._analyze_attack_surface()
return self.results
def _analyze_attack_surface(self) -> dict:
"""Analyseer de enumeratieresultaten om aanvalskansen te identificeren."""
opportunities = []
# Controleer op te ruim ingestelde parameters
params = self.results.get("parameters", {})
if params.get("temperature", {}).get("valid_range", (0, 0))[1] > 2.0:
opportunities.append({
"finding": "High temperature allowed",
"risk": "May produce more variable/exploitable outputs",
"parameter": "temperature",
})
# Controleer op verborgen parameters die werden geaccepteerd
hidden = self.results.get("hidden_params", [])
debug_params = [p for p in hidden if "debug" in p.get("parameter", "")]
if debug_params:
opportunities.append({
"finding": "Debug parameters accepted",
"risk": "May expose internal information or bypass safety",
"parameters": debug_params,
})
# Controleer op onverwachte mogelijkheden
caps = self.results.get("capabilities", {})
if isinstance(caps, CapabilityProfile):
caps = caps.capabilities
for cap_name, cap_data in caps.items():
if isinstance(cap_data, dict) and cap_data.get("supported"):
if cap_name in ["code_execution", "system_access", "file_operations"]:
opportunities.append({
"finding": f"Unexpected capability: \",
"risk": "May be exploitable for system access",
})
return {
"total_opportunities": len(opportunities),
"opportunities": opportunities,
"models_discovered": len(self.results.get("models", [])),
"hidden_params_found": len(self.results.get("hidden_params", [])),
}Informatie uit foutmeldingen
Informatie uit foutresponses halen
Foutmeldingen van LLM-API's zijn een rijke bron van informatie. Verschillende fouttypes onthullen verschillende aspecten van het systeem:
class ErrorIntelligenceCollector:
"""Haal informatie uit de foutmeldingen van een API."""
ERROR_CATEGORIES = {
"model_info": {
"patterns": [
r"model[:\s]+([a-zA-Z0-9\-\.]+)",
r"version[:\s]+([0-9\.]+)",
r"engine[:\s]+(\S+)",
],
"description": "Model identity information leaked in errors",
},
"infrastructure": {
"patterns": [
r"server[:\s]+(\S+)",
r"region[:\s]+([a-z\-]+\d*)",
r"instance[:\s]+([a-zA-Z0-9\-]+)",
r"pod[:\s]+([a-zA-Z0-9\-]+)",
],
"description": "Infrastructure details revealed",
},
"rate_limits": {
"patterns": [
r"limit[:\s]+(\d+)",
r"remaining[:\s]+(\d+)",
r"tokens per minute[:\s]+(\d+)",
r"requests per (minute|hour|day)[:\s]+(\d+)",
],
"description": "Rate limit configuration details",
},
"internal_paths": {
"patterns": [
r"(/[a-zA-Z0-9_/\-\.]+\.(py|js|go|rs))",
r"at\s+(/\S+:\d+)",
r"File\s+\"([^\"]+)\"",
],
"description": "Internal file paths or stack traces",
},
}
def collect_error_intelligence(self, api_client, model_id: str) -> dict:
"""Lok bewust fouten uit om informatie te verzamelen."""
import re
triggers = [
{"messages": [], "max_tokens": 1}, # Lege messages
{"messages": [{"role": "invalid", "content": "test"}]},
{"messages": [{"role": "user", "content": "a" * 10000000}]},
{"max_tokens": -1, "messages": [{"role": "user", "content": "t"}]},
{"temperature": 999, "messages": [{"role": "user", "content": "t"}]},
{"model": "nonexistent-model-xyz", "messages": [{"role": "user", "content": "t"}]},
]
intelligence = {}
for trigger in triggers:
if "model" not in trigger:
trigger["model"] = model_id
try:
api_client.chat.completions.create(**trigger)
except Exception as e:
error_text = str(e)
for category, config in self.ERROR_CATEGORIES.items():
for pattern in config["patterns"]:
matches = re.findall(pattern, error_text, re.IGNORECASE)
if matches:
if category not in intelligence:
intelligence[category] = []
intelligence[category].extend(matches)
# Dedupliceer de bevindingen
for category in intelligence:
intelligence[category] = list(set(
str(m) for m in intelligence[category]
))
return intelligenceGedragsmatige fingerprinting
Analyse van responspatronen
Naast expliciete mogelijkheden en parameters vertonen LLM-API's gedragspatronen die implementatiedetails onthullen:
class BehavioralFingerprinter:
"""Fingerprint het gedrag van een LLM via analyse van responspatronen."""
def fingerprint_behavior(self, api_client, model_id: str) -> dict:
"""Bouw een gedragsmatige fingerprint van de LLM-API op."""
return {
"response_consistency": self._test_consistency(api_client, model_id),
"latency_profile": self._profile_latency(api_client, model_id),
"token_counting": self._analyze_token_counting(api_client, model_id),
"truncation_behavior": self._test_truncation(api_client, model_id),
"streaming_behavior": self._test_streaming(api_client, model_id),
}
def _test_consistency(self, client, model_id: str) -> dict:
"""Test het determinisme van responses bij temperature=0."""
prompt = "What is 2+2? Reply with only the number."
responses = []
for _ in range(10):
response = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompt}],
max_tokens=10,
temperature=0.0,
)
responses.append(response.choices[0].message.content.strip())
unique = set(responses)
return {
"deterministic": len(unique) == 1,
"unique_responses": len(unique),
"responses": list(unique),
"note": "Non-deterministic temp=0 suggests load balancing across model replicas"
if len(unique) > 1 else "Consistent responses at temp=0",
}
def _profile_latency(self, client, model_id: str) -> dict:
"""Profileer de responslatentie om infrastructuurdetails af te leiden."""
import time
latencies = []
for length in ["short", "medium", "long"]:
prompts = {
"short": "Say hi",
"medium": "Write a 50-word paragraph about clouds",
"long": "Write a detailed 200-word essay about quantum computing",
}
times = []
for _ in range(5):
start = time.time()
client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompts[length]}],
max_tokens=300,
)
elapsed = time.time() - start
times.append(elapsed)
latencies.append({
"prompt_type": length,
"avg_ms": sum(times) / len(times) * 1000,
"min_ms": min(times) * 1000,
"max_ms": max(times) * 1000,
})
return latencies
def _analyze_token_counting(self, client, model_id: str) -> dict:
"""Analyseer hoe de API tokens telt om de tokenizer te identificeren."""
test_strings = {
"simple_english": "The quick brown fox jumps over the lazy dog",
"unicode": "Hello 你好 مرحبا Привет こんにちは",
"code": "def fibonacci(n):\n return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)",
"special_chars": "!@#$%^&*()_+-=[]{}|;':\",./<>?",
"repeated": "aaa " * 100,
}
token_counts = {}
for name, text in test_strings.items():
response = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": text}],
max_tokens=1,
)
if response.usage:
token_counts[name] = {
"prompt_tokens": response.usage.prompt_tokens,
"text_length": len(text),
"chars_per_token": len(text) / max(response.usage.prompt_tokens, 1),
}
return token_counts
def _test_truncation(self, client, model_id: str) -> dict:
"""Test hoe het model omgaat met overschrijding van het context-window."""
results = {}
for size in [1000, 5000, 10000, 50000, 100000]:
padding = "test " * size
prompt = f"\\nWhat was the first word of this message?"
try:
response = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": prompt}],
max_tokens=50,
)
results[size * 5] = { # Geschat aantal tekens
"status": "accepted",
"remembers_start": "test" in response.choices[0].message.content.lower(),
}
except Exception as e:
results[size * 5] = {
"status": "rejected",
"error": str(e)[:200],
}
return results
def _test_streaming(self, client, model_id: str) -> dict:
"""Test het streaming-gedrag om implementatiedetails te identificeren."""
import time
try:
start = time.time()
chunks = []
chunk_times = []
stream = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "Count from 1 to 20."}],
max_tokens=100,
stream=True,
)
for chunk in stream:
chunk_time = time.time() - start
if chunk.choices and chunk.choices[0].delta.content:
chunks.append(chunk.choices[0].delta.content)
chunk_times.append(chunk_time)
# Analyseer de timingpatronen van de chunks
if len(chunk_times) > 2:
intervals = [
chunk_times[i] - chunk_times[i-1]
for i in range(1, len(chunk_times))
]
return {
"streaming_supported": True,
"total_chunks": len(chunks),
"time_to_first_token_ms": chunk_times[0] * 1000,
"avg_interval_ms": sum(intervals) / len(intervals) * 1000,
"total_time_ms": chunk_times[-1] * 1000,
}
except Exception as e:
return {"streaming_supported": False, "error": str(e)[:200]}
return {"streaming_supported": True, "chunks": len(chunks)}Rapportage en documentatie
Structuur van een enumeratierapport
Leg enumeratiebevindingen vast in een gestructureerd formaat dat direct in de aanvalsplanning kan worden ingevoerd:
| Sectie | Inhoud | Afnemers |
|---|---|---|
| Doelsamenvatting | Provider, model-ID, API-versie, authenticatietype | Alle teamleden |
| Capability-matrix | Wel/niet ondersteunde mogelijkheden met confidence-niveaus | Aanvalsplanners |
| Parametercatalogus | Alle geaccepteerde parameters met geldige bereiken | Payload-ontwikkelaars |
| Overzicht van beperkingen | Tokenlimieten, rate limits, contentbeperkingen | Aanvallers op de infrastructuur |
| Verborgen features | Ongedocumenteerde parameters, feature flags, debug-modi | Ontwikkelaars van geavanceerde exploits |
| Informatie uit fouten | Informatie die via foutmeldingen lekt | OSINT-analisten |
| Gedragsprofiel | Latentie, consistentie, kenmerken van tokenisatie | Alle teamleden |
| Aanvalskansen | Geprioriteerde lijst van aanvalspaden op basis van bevindingen | Red team-leads |
Operationele veiligheid
Houd bij het enumereren van een LLM-API rekening met operationele veiligheid:
-
Bewust omgaan met rate limits — Spreid je requests om geen rate limits of anomaliedetectie te triggeren. Gebruik de rate limit-headers uit de eerste responses om je requestfrequentie af te stellen.
-
Accounts rouleren — Gebruik meerdere API-keys om het enumeratieverkeer te verdelen. Eén enkele key die duizenden ongebruikelijke requests doet, valt op.
-
Requests normaliseren — Meng enumeratie-probes met legitiem ogende requests om in het normale verkeer op te gaan.
-
Foutafhandeling — Vang alle fouten netjes op en log ze. Een enumeratietool die halverwege crasht, kost tijd en kan onvolledige resultaten achterlaten.
-
Dataclassificatie — Markeer alle enumeratieresultaten als gevoelig. Ze bevatten details over de configuratie van het doelwit die niet buiten de opdracht mogen lekken.
class OpSecEnumerator:
"""Enumeratie-wrapper met controles voor operationele veiligheid."""
def __init__(self, enumerator, config: dict):
self.enumerator = enumerator
self.min_delay = config.get("min_delay_seconds", 1.0)
self.max_delay = config.get("max_delay_seconds", 5.0)
self.jitter = config.get("jitter", True)
self.max_errors_before_pause = config.get("max_errors", 5)
self.pause_duration = config.get("pause_seconds", 60)
self.error_count = 0
def safe_probe(self, probe_func, *args, **kwargs):
"""Voer een probe uit met OpSec-controles."""
import random
import time
# Pas een vertraging met jitter toe
delay = self.min_delay
if self.jitter:
delay = random.uniform(self.min_delay, self.max_delay)
time.sleep(delay)
# Controleer de foutdrempel
if self.error_count >= self.max_errors_before_pause:
time.sleep(self.pause_duration)
self.error_count = 0
try:
result = probe_func(*args, **kwargs)
self.error_count = max(0, self.error_count - 1)
return result
except Exception as e:
self.error_count += 1
raisePraktijkvoorbeeld van een workflow
Een typische enumeratieopdracht voor een LLM-API verloopt als volgt:
-
Begin bij de documentatie — Lees alle openbare API-docs, changelogs en blogposts. Let op vermeldingen van beta-features, aankomende wijzigingen of afgeschreven endpoints.
-
Enumereer modellen — Som alle beschikbare modelvarianten op via het models-endpoint en pattern-gebaseerde brute force. Let extra op varianten met een datumstempel.
-
Breng parameters in kaart — Test voor elk relevant model alle kandidaat-parameters systematisch. Noteer welke worden geaccepteerd, welke worden afgewezen en hoe de afwijzingsfouten verschillen.
-
Profileer de mogelijkheden — Stuur capability-probes om te begrijpen wat het model kan. Test code-uitvoering, tool use, bestandstoegang en multimodale input.
-
Verzamel informatie uit fouten — Lok bewust verschillende foutcondities uit en analyseer de responses op gelekte informatie.
-
Bouw een gedragsprofiel — Meet latentie, consistentie, het tellen van tokens en streaming-gedrag om de infrastructuur te begrijpen.
-
Correleer en rapporteer — Kruisverwijs alle bevindingen om een volledig overzicht van het aanvalsoppervlak op te bouwen. Prioriteer aanvalskansen op basis van potentiële impact en slaagkans.
Belangrijkste lessen
Enumeratie van LLM-API's is een fundamentele verkenningsactiviteit die het werkelijke aanvalsoppervlak van een LLM-deployment onthult. Door verder te gaan dan het standaard ontdekken van API-endpoints en in te prikken op de mogelijkheden, beperkingen, verborgen parameters en gedragskenmerken op modelniveau, bouwen red teams de informatie op die nodig is om effectieve aanvallen te plannen.
De waardevolste enumeratiebevindingen zitten vaak in de gaten — ongedocumenteerde parameters die stilzwijgend worden geaccepteerd, modelvarianten die niet in de documentatie staan, en foutmeldingen die interne details lekken. Systematische enumeratie met de juiste tooling en operationele veiligheid maakt van deze gaten bruikbare aanvalsinformatie.