Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/kambo/tools/post_exploit.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ async def post_privesc_windows(target: str) -> dict:
"always_install_elevated": "reg query HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer /v AlwaysInstallElevated 2>nul & reg query HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer /v AlwaysInstallElevated 2>nul",
}

metrics = get_metrics()
metrics.record_run("post_privesc_windows")

return {
"target": target,
"commands_to_execute": commands,
Expand Down
28 changes: 28 additions & 0 deletions src/kambo/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,15 @@ def validate_idor(

# Different content for different IDs = real IDOR
if len(unique_hashes) > 1 and accessible_count > 0:
# Capture the first accessible response body as raw evidence
first_accessible = next(
(resp for _, resp in test_responses if resp.status == 200), None
)
raw_evidence = first_accessible.body[:500] if first_accessible else ""
chain = chain.add(
signal=f"{accessible_count} resources accessible with {len(unique_hashes)} unique responses",
source="idor_analysis",
raw_data=raw_evidence,
weight=0.6,
)

Expand Down Expand Up @@ -1434,6 +1440,17 @@ def validate_prototype_pollution(
if re.search(r"(invalid json|json parse|unexpected token|malformed)", output, re.IGNORECASE):
return chain.add_fp_check("Server rejected input as malformed JSON — not polluted")

# FP check: injected value echoed only inside error context (e.g. 400 validation message)
if injected_value and injected_value in output:
if re.search(r"(error|invalid|rejected|blocked|validation)", output[:500], re.IGNORECASE):
# Check if payload only appears inside error fields — not a real pollution
value_pos = output.find(injected_value)
surrounding = output[max(0, value_pos - 100): value_pos + 100]
if re.search(r"(error|message|detail|description)\s*[\"']?\s*:", surrounding, re.IGNORECASE):
return chain.add_fp_check(
f"Injected value appears only inside error/validation message — server echoed payload, not polluted"
)

# Primary signal: injected value appears in unexpected response field
if injected_value and injected_value in output:
# Check it's not just reflected in an error
Expand Down Expand Up @@ -1524,6 +1541,17 @@ def validate_deserialization(
if re.search(pattern, output, re.IGNORECASE):
return chain.add_fp_check(f"Deserialization explicitly rejected: {pattern}")

# FP check: generic HTTP 5xx error without deserialization-specific indicators
if re.search(r"(HTTP/\d\.?\d?\s+5\d{2}|internal server error|500 error)", output, re.IGNORECASE):
deser_specific = any(
re.search(pattern, output, re.IGNORECASE)
for pattern, _, _ in _DESER_CONFIRMED_SIGNALS
)
if not deser_specific and not expected_output:
return chain.add_fp_check(
"Generic HTTP 5xx error without deserialization-specific indicators — likely input validation rejection"
)

# Primary signal: expected command output (RCE confirmation)
if expected_output and expected_output in output:
chain = chain.add(
Expand Down
124 changes: 124 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Tests for KamboConfig — defaults, env-var overrides, singleton."""

from __future__ import annotations

import os
from pathlib import Path

import pytest

from kambo.config import KamboConfig, get_config


class TestKamboConfigDefaults:
def test_default_container_name(self) -> None:
cfg = KamboConfig()
assert cfg.container_name == "kambo-kali"

def test_default_docker_timeout(self) -> None:
cfg = KamboConfig()
assert cfg.docker_timeout == 300

def test_default_request_timeout(self) -> None:
cfg = KamboConfig()
assert cfg.docker_request_timeout == 30

def test_default_output_dir_is_path(self) -> None:
cfg = KamboConfig()
assert isinstance(cfg.output_dir, Path)

def test_default_wordlists_dir_is_path(self) -> None:
cfg = KamboConfig()
assert isinstance(cfg.wordlists_dir, Path)

def test_default_db_path_is_path(self) -> None:
cfg = KamboConfig()
assert isinstance(cfg.db_path, Path)

def test_default_context_is_pentest(self) -> None:
cfg = KamboConfig()
assert cfg.default_context == "pentest"

def test_default_max_concurrent_tools(self) -> None:
cfg = KamboConfig()
assert cfg.max_concurrent_tools == 5

def test_default_network_mode(self) -> None:
cfg = KamboConfig()
assert cfg.network_mode == "host"

def test_default_recon_rate_limit(self) -> None:
cfg = KamboConfig()
assert cfg.recon_rate_limit == 10

def test_default_exploit_rate_limit(self) -> None:
cfg = KamboConfig()
assert cfg.exploit_rate_limit == 5


class TestKamboConfigEnvOverrides:
def test_container_name_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_CONTAINER_NAME", "my-kali")
cfg = KamboConfig()
assert cfg.container_name == "my-kali"

def test_docker_timeout_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_DOCKER_TIMEOUT", "600")
cfg = KamboConfig()
assert cfg.docker_timeout == 600

def test_output_dir_override(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("KAMBO_OUTPUT_DIR", str(tmp_path))
cfg = KamboConfig()
assert cfg.output_dir == tmp_path

def test_default_context_pentest(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_DEFAULT_CONTEXT", "pentest")
cfg = KamboConfig()
assert cfg.default_context == "pentest"

def test_default_context_bug_bounty(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_DEFAULT_CONTEXT", "bug_bounty")
cfg = KamboConfig()
assert cfg.default_context == "bug_bounty"

def test_default_context_ctf(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_DEFAULT_CONTEXT", "ctf")
cfg = KamboConfig()
assert cfg.default_context == "ctf"

def test_max_concurrent_tools_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_MAX_CONCURRENT_TOOLS", "10")
cfg = KamboConfig()
assert cfg.max_concurrent_tools == 10

def test_network_mode_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_NETWORK_MODE", "bridge")
cfg = KamboConfig()
assert cfg.network_mode == "bridge"

def test_recon_rate_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMBO_RECON_RATE_LIMIT", "20")
cfg = KamboConfig()
assert cfg.recon_rate_limit == 20


class TestGetConfig:
def test_returns_kambo_config(self) -> None:
cfg = get_config()
assert isinstance(cfg, KamboConfig)

def test_independent_calls_return_same_defaults(self) -> None:
cfg1 = get_config()
cfg2 = get_config()
assert cfg1.container_name == cfg2.container_name
assert cfg1.docker_timeout == cfg2.docker_timeout

def test_config_is_immutable_pydantic(self) -> None:
"""Config fields should be type-safe Pydantic fields."""
cfg = KamboConfig()
# Should be able to read all fields without error
_ = cfg.container_name
_ = cfg.docker_timeout
_ = cfg.output_dir
_ = cfg.default_context
Loading