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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
# k3s / OnPrem deploy local config (see deployments/README.md)
deployments/k3s/config.env
deployments/config.env
deployments/k3s/*.local.yaml
deployments/k8s/**/*.local.yaml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Open `https://<host>:8443` (or `http://` locally; `/` redirects to **Engagements
$ ./Scythe Http --method GET --timeout 5s --url http://127.0.0.1:8080 --headers 'Content-Type:application/json,X-Client-Id:550e8400-e29b-41d4-a716-446655440000,X-API-Secret:mysecurekey1' --directories '/heartbeat/550e8400-e29b-41d4-a716-446655440000,/heartbeat'
```

With a pivot (parent beacon), the example adds `--proxy <host:port>` (from the form, or `BEACON_PIVOT_PROXY`).
A pivot is an upstream beacon built with **SOCKS5 listener** (UI default port 9050) and a child whose **Pivot proxy** is that listener’s `host:port`. Parent ClientId alone does not add `-proxy`. See [Initial proxy](docs/operator-guide-beacons.md#initial-proxy-pivot).

* If there is no authenticated user, then no access.

Expand Down
1 change: 1 addition & 0 deletions SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Embedded Scythe requires **`TERM_HARVEST=9`** in the environment before launch.

- Tie recommendations to observed facts (e.g. domain user → credential access techniques, local admin → privilege escalation).
- For lateral movement, check **Topology** for existing beacons and pivot (`ParentClientId`, pivot proxy).
- Initial proxy: upstream beacon has **SOCKS5 listener** checked (UI default port 9050), no parent, and empty Proxy. It listens on all interfaces with no SOCKS auth. Each child sets **Parent beacon ClientId** to that UUID and **Pivot proxy** to `<upstream-ip>:<port>` reachable from the child. The child’s C2 base URL stays the public listener; `-proxy` only changes the dial. Parent alone does not add `-proxy`. These flags are compiled into Scythe.embedded, so a profile rebuild does not turn a listener on after the fact.
- Prefer living-off-the-land and engagement-appropriate tooling; note OPSEC (logging, EDR, DLP).

### Reporting
Expand Down
6 changes: 5 additions & 1 deletion deployments/k3s/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ patch_k3s_ingress_host() {
local file="$1"
local host="$2"
local issuer="$3"
local tls_secret
tls_secret="$(ingress_tls_secret_name "$host")"
sed -e "s|beacons\\.example\\.com|${host}|g" \
-e "s|beacons-example-com-tls|${tls_secret}|g" \
-e "s|cert-manager.io/cluster-issuer: .*|cert-manager.io/cluster-issuer: ${issuer}|" \
"$file"
}
Expand Down Expand Up @@ -263,7 +266,8 @@ apply_k3s_ingress() {
teardown_k3s_ingress() {
local k3s_root="$1"
local ns="${REAPERC2_NAMESPACE:-reaperc2-ns}"
local tls_secret="beacons-example-com-tls"
local tls_secret
tls_secret="$(ingress_tls_secret_name "${INGRESS_HOST:-beacons.example.com}")"
local ing_name="reaperc2-ingress"

echo "Removing ingress, middlewares, and staging/prod TLS resources..."
Expand Down
5 changes: 5 additions & 0 deletions deployments/k3s/scripts/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ load_config() {
CERT_MANAGER_ISSUER="${CERT_MANAGER_ISSUER:-letsencrypt-prod}"
}

ingress_tls_secret_name() {
local host="${1:-${INGRESS_HOST:-beacons.example.com}}"
echo "${host//./-}-tls"
}

reaperc2_image_ref() {
echo "${REAPERC2_IMAGE}:${REAPERC2_IMAGE_TAG}"
}
Expand Down
55 changes: 51 additions & 4 deletions docs/operator-guide-beacons.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Use URL hash `#profiles` (or **Refresh profile list**) to jump to saved profiles
Optional friendly name shown on **Topology**, **Commands** beacon picker, and exports. Does not affect authentication.

**Parent beacon ClientId**
Optional UUID of an **upstream** beacon for a **pivot chain**. When set, Scythe examples include `--proxy` (see pivot proxy). Topology draws parent → child edges toward C2.
Optional UUID of an **upstream** beacon for a **pivot chain**. Topology draws a parent → child edge toward C2. A parent does **not** by itself add `-proxy`. The child’s dial path comes from **Pivot proxy** or Scythe Http **Proxy** (see **Initial proxy** below).

**Pivot proxy `host:port`**
Used in Scythe `--proxy` when a parent is set (unless overridden in Scythe Http **Proxy**). Server default: environment `BEACON_PIVOT_PROXY`.
Address of the upstream beacon’s SOCKS5 listener, as the **child** can reach it (for example `10.0.1.20:9050`). Copied into Scythe `-proxy` only when a parent is set and Scythe Http **Proxy** is empty. If both are empty, the server uses environment `BEACON_PIVOT_PROXY` when that variable is set. The k3s deployment does not set it; put the address on the form for each hop.

**Beacon C2 base URL**
Public origin implants should call (e.g. `https://c2.example.com` or `10.0.0.5:8080`). Saved on the profile for embedded rebuilds. Server default: `BEACON_PUBLIC_BASE_URL`.
Expand All @@ -49,8 +49,8 @@ These options build the example **`Scythe Http …`** command and the embedded c
| Request body | `-body` (optional JSON string) |
| Extra directories | Appended after required `/heartbeat/<ClientId>,/heartbeat` |
| Extra headers | Merged after required `Content-Type`, `X-Client-Id`, `X-API-Secret` |
| Proxy | `-proxy`; pivot proxy fills in when parent is set and this is empty |
| SOCKS5 listener | `-socks5-listen` / `-socks5-port` |
| Proxy | `-proxy`. If this is set, it wins over **Pivot proxy**. Leave it empty on a child so **Pivot proxy** is applied. |
| SOCKS5 listener | `-socks5-listen` / `-socks5-port`. The checkbox must be on; the port field alone does nothing. UI default port is **9050**. |
| Skip TLS verify | `-skip-tls-verify` |
| GOOS / GOARCH | Target for **Scythe.embedded** (`linux`/`windows`/`darwin`, `amd64`/`arm64`) |

Expand Down Expand Up @@ -100,6 +100,51 @@ Typical Scythe HTTP flow (simplified):

Queue work from **Commands**; results appear in output history and audit logs.

## Initial proxy (pivot)

A pivot is two beacons. The **upstream** beacon phones home directly and also listens as a SOCKS5 proxy. Each **downstream** beacon dials the public C2 URL through that proxy.

`-proxy` and `-socks5-listen` are compiled into **Scythe.embedded**. **Saved profiles → Scythe.embedded** rebuilds the options saved on that profile. To turn a listener on, or to point a child at a different address, generate a new beacon and download again.

### 1. Upstream beacon (the proxy)

On **Generate beacon**:

| Field | Value |
|-------|--------|
| Parent beacon ClientId | Empty |
| Pivot proxy | Empty |
| Beacon C2 base URL | Public beacon origin (ingress or `:8080`), not the admin UI |
| Scythe Http → SOCKS5 listener | **Checked** |
| SOCKS5 listen port | Port the child will connect to (form default **9050**) |
| Scythe Http → Proxy | Empty |

Download **Scythe.embedded** and start it with `TERM_HARVEST=9`. On startup it phones home as usual and listens on **all interfaces** at `:<port>` (`net.Listen("tcp", ":9050")`). The listener accepts SOCKS5 with **no authentication**.

Confirm the beacon is green on **Topology** before you build children. From a host that should pivot through it, the address you will type later is that host’s route to the upstream machine plus the listen port (`10.0.1.20:9050`). That is not the public C2 hostname, and it is not `127.0.0.1` unless the child runs on the same machine.

### 2. Downstream beacon (through the proxy)

The upstream client must already exist in this engagement.

| Field | Value |
|-------|--------|
| Parent beacon ClientId | Upstream beacon’s ClientId |
| Pivot proxy | `<upstream-ip>:<socks-port>` reachable **from the child** |
| Beacon C2 base URL | Same public origin the upstream uses |
| Scythe Http → Proxy | Empty (a value here replaces Pivot proxy) |
| SOCKS5 listener | Off, unless this host should also proxy a further hop |

Generate and download a new embedded binary. The child still requests `https://<c2>/heartbeat/<its-client-id>`. `-proxy` only changes the TCP dial: Scythe opens a SOCKS5 connection to the pivot address, then asks that proxy to connect to the C2 host.

Topology draws the edge from **Parent beacon ClientId** alone. If Pivot proxy is missing, or nothing is listening at that address, the graph can still show the link while the child dials C2 directly (no `-proxy`) or fails to connect (bad `-proxy`).

### Further hops

Repeat step 2. Each new beacon’s parent is the beacon **one hop closer to C2**, and its pivot proxy is that parent’s SOCKS5 address. Check **SOCKS5 listener** on any beacon that should accept the next hop.

`BEACON_PIVOT_PROXY` on the ReaperC2 process is only a fallback when a new beacon has a parent and both proxy fields are empty. One server-wide address is usually wrong once you have more than one hop, so set **Pivot proxy** on the form.

## Beacon troubleshooting

| Issue | Check |
Expand All @@ -109,6 +154,8 @@ Queue work from **Commands**; results appear in output history and audit logs.
| Embedded build fails | Go installed on server; Scythe sources at `third_party/Scythe` or `REAPERC2_ROOT` |
| No beacons on Commands page | Generate under **Beacons** for the **active** engagement |
| Topology all gray | Beacon never checked in, or interval much shorter than actual sleep |
| Child does not use the pivot | Parent ClientId was set but **Pivot proxy** and Scythe Http **Proxy** were both empty, and `BEACON_PIVOT_PROXY` is unset. Generate again with **Pivot proxy** set to the upstream `host:port`. |
| Child cannot connect through the pivot | Upstream was built without **SOCKS5 listener** checked, the port does not match, or the child cannot route to that `host:port`. The listener binds all interfaces and has no SOCKS username. |
| **`tls: failed to verify certificate: x509: certificate signed by unknown authority`** (Scythe `Http` logs `[-] Error: request failed: Get "https://…/heartbeat"`) | The beacon host does **not** trust the **issuer** of the certificate it received. Common causes: (1) **Split DNS / internal VIP** — the hostname resolves to an **internal** load balancer that presents a **different** cert (corporate CA, self-signed, or old staging) while the public Internet sees Let’s Encrypt. Compare `dig` / `curl -v` from the **beacon machine** vs your laptop. (2) **TLS interception** (corporate proxy) — trust the proxy root or use lab-only `-skip-tls-verify`. (3) **Stale or custom-built Scythe** — rebuild **Scythe.embedded** (or CLI Scythe) with a **current Go** (repo uses Go 1.24); very old runtimes or minimal containers **without `ca-certificates`** can fail verification. (4) **Let’s Encrypt staging** still in front of some paths — staging chains use CAs that are not in default trust stores. |

### TLS: quick checks (run on the **same machine** that runs the failing Scythe)
Expand Down
2 changes: 2 additions & 0 deletions docs/operator-guide-topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

Interactive **graph** of C2 → beacons (and parent → child when `ParentClientId` is set). Data from `GET /api/topology`.

The edge is the parent UUID stored at generation. Traffic follows that path only when the upstream beacon was built with a **SOCKS5 listener** and the child was built with **Pivot proxy** set to that listener. See [Initial proxy](/documentation/operator-guide-beacons) on the Beacons page.

| Color | Meaning |
|-------|---------|
| **Blue** | C2 server node |
Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Skill text: repository root **`SKILLS.md`** (also used by Cursor agents).
- Set **Beacon C2 base URL** in the UI (or `BEACON_PUBLIC_BASE_URL`) to the **public** origin beacons use (port **8080** / ingress to beacon API — not `localhost:8443` admin).
- **Scythe.embedded** requires **Go** on the server host at runtime; sources resolve via `REAPERC2_ROOT/third_party/Scythe` or submodule path next to the binary.
- Embedded binaries need **`TERM_HARVEST=9`** in the environment before launch (see **Beacons** page in the UI or Operator guide).
- The first pivot is an upstream beacon with **SOCKS5 listener** checked (no parent). Children set **Parent beacon ClientId** and **Pivot proxy** to that listener’s `host:port`. Steps: [Initial proxy](/documentation/operator-guide-beacons) in the operator guide.

## Security-related environment variables

Expand Down
1 change: 1 addition & 0 deletions pkg/adminpanel/audit_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var preferredAuditDetailKeys = []string{
"new_username",
"new_role",
"target_username",
"clear_totp",
"operators",
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/adminpanel/ghostwriter_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ func ghostwriterUserContext(action string) string {
return "Admin | user enabled"
case dbconnections.AuditActionUserRoleUpdated:
return "Admin | user role updated"
case dbconnections.AuditActionUserPasswordReset:
return "Admin | user password reset"
case dbconnections.AuditActionEngagementOperatorsUpdated:
return "Admin | engagement operators updated"
case dbconnections.AuditActionAIChat:
Expand Down
Loading
Loading