Skip to content

errantpacket/Oblique-Relay

Repository files navigation

Oblique Relay

Edge redirector built on Cloudflare Workers for authorized penetration testing infrastructure.

Sits between implants and your teamserver. Inbound requests are validated against a configurable profile. Traffic that matches gets proxied to the backend. Everything else receives a decoy response, either a 302 redirect to a legitimate site or a static placeholder page. The profile lives in KV and can be updated at runtime without redeployment.

Architecture

Architecture

Request Flow

Request Flow

Quick Start

git clone https://github.com/errantpacket/Oblique-Relay.git && cd Oblique-Relay
npm install

# local dev (uses .dev.vars for secrets)
cp .dev.vars.example .dev.vars
npm run dev

# run tests
npm test

Deployment

wrangler login

wrangler secret put BACKEND_URL      # https://your-teamserver.internal
wrangler secret put DECOY_URL        # https://en.wikipedia.org
wrangler secret put PROFILE_SECRET   # shared token for operator endpoints

npm run deploy

Profile Configuration

The profile controls which requests get proxied and which get decoyed. It is stored in the PROFILE KV namespace at key profile:active and can be updated at runtime through the operator API. When no KV profile exists, a hardcoded default is used.

Profile Schema

{
  "paths": ["/api/v1/status", "/api/v1/data"],
  "methods": ["GET", "POST"],
  "headers": {"X-Request-ID": "^[a-f0-9-]{36}$"},
  "ua_pattern": "Mozilla/5\\.0",
  "geo_allow": ["US", "CA"],
  "geo_deny": ["CN", "RU"],
  "time_window": {"start": "08:00", "end": "22:00", "tz": "America/New_York"},
  "jitter_ms": 100,
  "backends": {"/content": "https://alt-teamserver.example.com"}
}

Validation Pipeline

Validation Pipeline

Every check must pass. The first failure triggers a decoy response.

PROFILE_SECRET is not part of this pipeline. It only gates operator endpoints (/__health, /__profile, etc.). Implant traffic is validated by the profile alone, because C2 frameworks typically do not include custom auth headers in their HTTP callbacks by default.

Field Type Description
paths string[] URI prefixes that pass validation
methods string[] Allowed HTTP methods
headers {name: regex} Required request headers with regex patterns
ua_pattern string|null Regex the User-Agent must match (null to skip)
geo_allow string[] Allowed country codes from CF-IPCountry (empty allows all)
geo_deny string[] Blocked country codes (empty blocks none)
time_window object|null Active hours as {start, end, tz} in HH:MM (null for always)
jitter_ms number Max random delay before responding, in ms (0 to disable)
backends {prefix: url} Route by path prefix (unmatched paths use BACKEND_URL)

Operator API

All operator endpoints require an X-Auth-Token header matching PROFILE_SECRET.

Health Check

curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__health
# {"status":"ok","ts":1710000000000,"profile":"kv"}

View / Update Profile

# view current profile
curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__profile

# replace profile
curl -X PUT -H "X-Auth-Token: <secret>" -H "Content-Type: application/json" \
  -d '{"paths":["/beacon"],"methods":["GET","POST"],"geo_allow":["US"]}' \
  https://oblique-relay.example.com/__profile

Import C2 Profile

Import a native C2 framework profile. The parser extracts paths, methods, headers, and user-agent into the relay's normalized format.

# dry run (preview without saving)
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @malleable.profile \
  "https://oblique-relay.example.com/__profile/import?dry_run=true"

# Cobalt Strike (default format)
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @malleable.profile \
  "https://oblique-relay.example.com/__profile/import"

# Sliver
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @sliver-http.json \
  "https://oblique-relay.example.com/__profile/import?format=sliver"

# Mythic
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @mythic-profile.json \
  "https://oblique-relay.example.com/__profile/import?format=mythic"

# Havoc
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @havoc.yaotl \
  "https://oblique-relay.example.com/__profile/import?format=havoc"

# PoshC2
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @poshc2-config.txt \
  "https://oblique-relay.example.com/__profile/import?format=poshc2"

# Oblique Server (private repo)
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @oblique-profile.json \
  "https://oblique-relay.example.com/__profile/import?format=oblique"

Supported parsers:

Format Config type What it extracts
cobalt-strike Malleable C2 .profile URIs, methods, client headers, useragent
sliver HTTP C2 JSON (native export) Path segments, methods, implant headers, user_agent
mythic JSON profile get/post URIs, AgentHeaders, user_agent
havoc Yaotl/HCL-like config Uris, UserAgent, Headers array
poshc2 Python config UrlsX, UserAgent, DomainFrontHeader, PayloadCommsHost
oblique JSON (native adapter export) paths, methods, headers, user_agent

Fields not present in the C2 config (geo, time window, jitter, backends) stay at their defaults. Configure them separately with PUT /__profile.

Both PUT and import validate against the profile schema. Invalid profiles are rejected with descriptive errors.

Metrics Dashboard

curl -H "X-Auth-Token: <secret>" \
  "https://oblique-relay.example.com/__metrics" -o metrics.html

# filter by time range and entry limit
curl -H "X-Auth-Token: <secret>" \
  "https://oblique-relay.example.com/__metrics?hours=72&limit=1000"

Shows valid/decoy counts, valid ratio, requests per hour, top source IPs, paths, countries, and user-agents. Aggregated from the LOG KV namespace.

Session Tracking

Per-implant session tracking using Durable Objects with SQLite storage. Sessions are keyed by X-Session-ID header, falling back to CF-Connecting-IP.

# list active sessions
curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__sessions

# view a specific session
curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__sessions/<session-id>

Each session tracks:

Field Description
request_count Total requests from this session
first_seen Timestamp of first request
last_seen Timestamp of most recent request
paths Last 100 paths accessed with timestamps
flagged Auto-flagged if request count exceeds 1000
flag_reason Reason for flagging

Sessions auto-expire after 24 hours of inactivity via DO alarms. A KV-based index (session: prefix in LOG) enables listing without enumerating all Durable Objects.

Logging

Every request is logged to the LOG KV namespace with a 7-day TTL. Keys are prefixed valid: or decoy: for filtering.

npm run logs         # valid traffic
npm run logs:decoy   # rejected traffic
npm run tail         # real-time stream

Testing

Unit Tests

146 tests across 12 files covering validation, proxy, operator endpoints, logging, sessions, and all parsers.

npm test             # run full suite
npm run test:watch   # watch mode

E2E Tests

The E2E suites validate the full chain against real C2 servers with no mocks.

Sliver E2E (27 tests): log-based and PTY-driven verification:

  1. Start Sliver server with HTTP listener and implant generation
  2. Establish a cloudflared tunnel to the Sliver backend
  3. Import the real Sliver C2 profile through the relay's parser
  4. Verify negative-path filtering (invalid paths get decoyed)
  5. Compile and run a beacon implant through the relay
  6. Confirm beacon registration in Sliver's server logs
  7. Execute C2 tasks (whoami, file download) through the relay
  8. Verify bidirectional traffic (task results returned through relay)
# requires Docker, Sliver v1.7.3 image, and a deployed relay
./test/e2e/validate-sliver.sh <relay-url> <auth-token>

Mythic E2E (33 tests): API-driven verification with 9 containers:

  1. Start Mythic stack (postgres, rabbitmq, server, Hasura GraphQL, nginx, HTTP C2 profile, Poseidon)
  2. Authenticate via REST API, poll until C2 profile and agent type register via postgres
  3. Start HTTP C2 profile listener via webhook API
  4. Establish a cloudflared tunnel to the HTTP C2 profile listener
  5. Extract C2 profile parameters from postgres, transform to parser format, and import into relay
  6. Verify negative-path filtering (invalid paths get decoyed, valid Mythic paths proxied)
  7. Generate Poseidon payload via webhook API with relay as callback host (~30s cross-compilation)
  8. Run agent on victim container, confirm callback registration and AES key exchange via postgres
  9. Execute C2 tasks (shell whoami, file download) via webhook API, verify completion and relay traffic
# requires Docker, Mythic GHCR images (latest), and a deployed relay
./test/e2e/validate-mythic.sh <relay-url> <auth-token>

Operator Dashboard

A local HTML dashboard for managing the relay from your browser. No server, no build step. Open the file and connect.

open tools/dashboard.html

Five tabs: Health, Profile (view/edit/import), Metrics (charts and breakdown tables), Logs (filterable raw entries), and Sessions (expandable detail with path history). All tabs support JSON export.

See docs/dashboard.md for full documentation.

Key Files

File Role
src/worker.js Entry point, routing, operator endpoints
src/validation.js Request validation pipeline (method, path, headers, UA, geo, time)
src/proxy.js Backend proxying and decoy responses
src/profile.js Profile loading and management (KV-backed)
src/logging.js Request logging (best-effort, non-blocking)
src/metrics.js HTML metrics dashboard
src/sessions.js Durable Object session tracking
src/util.js Response helpers, timing-safe comparison, escaping
src/parsers/ C2 profile parsers (Cobalt Strike, Sliver, Mythic, Havoc, PoshC2, Oblique)
tools/dashboard.html Local operator dashboard (single HTML file, no deps)
test/e2e/ E2E validation suites (Sliver: 27 tests, Mythic: 33 tests)
docs/dashboard.md Dashboard documentation
docs/adding-a-parser.md Guide for adding new C2 parser support

Environment Bindings

Binding Type Purpose
BACKEND_URL Secret Default backend/teamserver URL
DECOY_URL Secret Redirect target for invalid traffic
PROFILE_SECRET Secret Operator auth token (gates operator endpoints only)
LOG KV Namespace Request logging with 7-day TTL
PROFILE KV Namespace Runtime profile configuration
SESSIONS Durable Object Per-implant session tracking

Commands

npm run dev          # local dev server on :8787
npm run deploy       # deploy to Cloudflare
npm run tail         # real-time request logs
npm run logs         # list valid-traffic KV entries
npm run logs:decoy   # list decoy-traffic KV entries
npm run lint         # ESLint with security rules
npm run lint:fix     # auto-fix lint issues
npm test             # 146 tests across 12 files
npm run test:watch   # tests in watch mode

Security

Auth Model

There are two separate trust boundaries:

Operator endpoints (/__health, /__profile, /__metrics, /__sessions) are protected by PROFILE_SECRET. Requests must include an X-Auth-Token header with the correct token. Comparison is timing-safe (HMAC-SHA256).

Implant traffic is validated by the profile pipeline only: method, path, headers, user-agent, geo, and time window. There is no shared secret on this path. C2 frameworks typically do not include custom auth headers in their HTTP callbacks by default, so a blanket token check here would reject real implant traffic out of the box. Instead, the profile itself is the security boundary. An attacker would need to know the exact C2 configuration (path prefixes, required headers, UA pattern) to craft requests that pass validation. Even then, the backend C2 server has its own encryption and key exchange, so proxied garbage gets ignored.

Operators who want tighter control can add required headers to the profile. For example, setting "headers": {"X-Custom": "^secret-value$"} forces implant traffic to include that header. This is framework-dependent and must be configured in both the C2 profile and the relay profile.

General

  • Secrets never in source. Use wrangler secret put or the CF dashboard.
  • All C2 parser output escaped with escapeRegex to prevent regex injection
  • Parsers only extract request headers (what the implant sends), never response headers
  • Backend errors fall through to the decoy silently
  • server and x-powered-by headers stripped from proxied responses
  • HTML output escaped with escapeHTML to prevent XSS in the metrics dashboard
  • Client IP forwarded via X-Forwarded-For
  • CF-specific headers (CF-Connecting-IP, CF-IPCountry, CF-Ray) stripped before proxying
  • Decoy responses never expose backend URLs or secrets
  • ESLint with eslint-plugin-security enforced in CI

Adding a New C2 Parser

See docs/adding-a-parser.md for the full guide.

  1. Create src/parsers/<name>.js exporting { name, parse(text) } that returns a normalized profile
  2. Register it in src/parsers/index.js
  3. Add tests in test/parsers/<name>.test.js covering parsing, edge cases, and rejection
  4. Escape all extracted strings with escapeRegex()
  5. Only extract request headers, never response headers

License

MIT. Developed for authorized penetration testing and research. Do what you want. You are responsible for how you use it.

About

Cloudflare Workers edge redirector for authorized red team infrastructure. Validates implant traffic against C2 profiles, proxies to backend, decoys everything else.

Topics

Resources

License

Stars

7 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors