Compare commits

...
Author SHA1 Message Date
moloch-- fa0e13a5cc Basic tui layout 2026-03-14 11:14:03 -07:00
moloch-- b355fe70d6 Add Windows makefile 2026-03-13 16:14:11 -07:00
moloch-- 10b1079286 Tweak agents.md 2026-03-13 15:29:45 -07:00
Joe 9c042e6977 Merge pull request #2217 from BishopFox/fix/cert-revoke
Improve operator cert revocation
2026-03-13 12:04:26 -07:00
Joe 62583cac8f Merge branch 'master' into fix/cert-revoke 2026-03-13 11:02:34 -07:00
moloch 95764945b8 Improve operator cert revocation 2026-03-13 10:59:00 -07:00
Joe 26abda1c15 Merge pull request #2216 from skoveit/fix/timeout-int64p-mismatch
fix: timeout flag ignored in aliases and extensions due to type mismatch
2026-03-13 07:38:21 -07:00
moloch 01dd3cac72 Go mod tidy 2026-03-13 07:02:06 -07:00
0xOral f8faa1f767 fix: normalize all command timeout flags to Int64P across client codebase 2026-03-13 02:49:01 +02:00
0xOral a9e48e84cd fix: extension timeout flag ignored due to IntP/Int64P mismatch 2026-03-13 02:30:55 +02:00
0xOral 9d6704d925 fix: alias -t flag always capped at 30s due to IntP/Int64P type mismatch (#2183) 2026-03-13 02:04:35 +02:00
Joe ce5157e2e9 Merge pull request #2213 from skoveit/fix/wininet-handle-leak
fix(wininet): Fix handle leak in WinINet driver
2026-03-11 08:29:42 -07:00
0xOral 7d19051df8 fix(wininet): close connection and request handles after each request 2026-03-11 01:32:27 +02:00
Joe a0d3bd579f Merge pull request #2211 from skoveit/feat/configurable-portfwd-keepalive
feat: make TCP keepalive period configurable for portfwd and rportfwd
2026-03-08 10:36:42 -07:00
0xOral 82c309632a default keepalive value 30s for both rportfwd and portfwd 2026-03-08 17:27:27 +02:00
0xOral 5c0b8ec460 disable keepalive with --keepalive -1 value 2026-03-08 17:25:06 +02:00
0xOral 7791a86eb4 configurable keepalive: portfwd --keepalive 2026-03-08 17:20:35 +02:00
Joe 7ac4db3fa0 Merge pull request #2207 from BishopFox/fix/beacon-getsystem
Fix for getsystem in beacon mode
2026-03-05 18:31:39 -07:00
moloch-- 512bc822ef Implement fix for getsystem in beacon mode 2026-03-05 08:22:42 -08:00
Joe f2bb9c3733 Merge pull request #2205 from BishopFox/feature/lazy-lg-msg
Lazy spool large messages to disk
2026-03-04 15:40:56 -07:00
moloch-- 0bd4f100fd Lazy spool large messages to disk 2026-03-04 14:21:07 -08:00
Joe 3230322fd6 Merge pull request #2201 from BishopFox/feature/exec-shellcode
Add support for wasm-donut options in windows `execute-shellcode`
2026-03-02 08:23:49 -07:00
56 changed files with 2981 additions and 800 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
/.vscode
/.vscode/*
/.devcontainer
/.idea
+1 -1
View File
@@ -4,7 +4,7 @@
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
## Coding Style & Naming Conventions
+333 -184
View File
@@ -1,184 +1,333 @@
#
# Makefile for Sliver
#
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# Targets
#
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
#
# Makefile for Sliver
#
ifneq ($(OS),Windows_NT)
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# Targets
#
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
#
# >>> WINDOWS <<<
#
else
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
SHELL := cmd.exe
.SHELLFLAGS := /C
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
COMMA := ,
ifeq ($(MAKECMDGOALS), linux)
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
define windows_exec
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
endef
define windows_go_build
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
endef
.PHONY: default
default: clean validate-go-version
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
.PHONY: pb
pb: validate-pb-compilers
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: validate-pb-compilers
validate-pb-compilers:
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
validate-go-version:
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
.PHONY: clean-all
clean-all: clean
-rmdir /S /Q server\assets\fs\darwin\amd64
-rmdir /S /Q server\assets\fs\darwin\arm64
-rmdir /S /Q server\assets\fs\windows\amd64
-rmdir /S /Q server\assets\fs\linux\amd64
-del /Q /F server\assets\fs\*.zip 2>NUL
-del /Q /F .downloaded_assets 2>NUL
.PHONY: clean
clean:
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
.downloaded_assets:
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
endif
+1
View File
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
## Sub-packages
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
- `alias/` – Implements the 'alias' command group for the Sliver client console.
- `armory/` – Implements the 'armory' command group for the Sliver client console.
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
+5
View File
@@ -0,0 +1,5 @@
# client/command/ai
Implements the `ai` implant command for the Sliver client console.
Current scope: a Bubble Tea TUI layout preview with placeholder sidebar, transcript, context, and composer panes. Chat execution and tool wiring are intentionally stubbed for a later change.
+189
View File
@@ -0,0 +1,189 @@
package ai
import (
"fmt"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
)
// AICmd launches the placeholder Sliver AI TUI.
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
model := newAIModel(buildAIContext(con))
width, height := 100, 30
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
if _, err := program.Run(); err != nil {
con.PrintErrorf("AI TUI error: %s\n", err)
}
}
type aiContext struct {
target aiTargetSummary
connection aiConnectionSummary
conversations []aiConversation
planned []string
status string
}
type aiTargetSummary struct {
Label string
Host string
OS string
Arch string
C2 string
Mode string
Details []string
}
type aiConnectionSummary struct {
Profile string
Server string
Operator string
State string
}
type aiConversation struct {
Title string
Subtitle string
Messages []aiMessage
}
type aiMessage struct {
Role string
Body string
}
func buildAIContext(con *console.SliverClient) aiContext {
ctx := aiContext{
target: aiTargetSummary{
Label: "No active target",
Host: "Select a session or beacon with `use`",
Mode: "offline preview",
C2: "n/a",
OS: "unknown",
Arch: "unknown",
},
connection: aiConnectionSummary{
Profile: "<disconnected>",
Server: "<unknown>",
Operator: "<unknown>",
State: "idle",
},
planned: []string{
"Streaming chat transcript for operator prompts and assistant replies.",
"Target-aware actions that can call Sliver commands and summarize output.",
"Conversation history, pinned context, and tool traces in the side panels.",
},
status: "Layout preview only. Prompt submission is captured locally until the AI backend is implemented.",
}
if con != nil {
if details, state, ok := con.CurrentConnection(); ok {
ctx.connection.State = strings.ToLower(state.String())
if details != nil && details.Config != nil {
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
}
}
session, beacon := con.ActiveTarget.Get()
switch {
case session != nil:
ctx.target = aiTargetSummary{
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
Host: fallback(session.Hostname, "<unknown host>"),
OS: fallback(session.OS, "unknown"),
Arch: fallback(session.Arch, "unknown"),
C2: fallback(session.ActiveC2, "unknown"),
Mode: "interactive session",
Details: []string{
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
fmt.Sprintf("PID: %d", session.PID),
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
},
}
case beacon != nil:
ctx.target = aiTargetSummary{
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
Host: fallback(beacon.Hostname, "<unknown host>"),
OS: fallback(beacon.OS, "unknown"),
Arch: fallback(beacon.Arch, "unknown"),
C2: fallback(beacon.ActiveC2, "unknown"),
Mode: "asynchronous beacon",
Details: []string{
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
},
}
}
}
ctx.conversations = []aiConversation{
{
Title: "Current Target",
Subtitle: fmt.Sprintf("%s on %s (%s/%s)", ctx.target.Label, ctx.target.Host, ctx.target.OS, ctx.target.Arch),
Messages: []aiMessage{
{Role: "system", Body: "Bubble Tea layout preview for the future Sliver AI workspace."},
{Role: "assistant", Body: fmt.Sprintf("Active target context is loaded as %s with %s over %s.", ctx.target.Mode, ctx.target.Label, ctx.target.C2)},
{Role: "user", Body: "Summarize the target, operator context, and likely next steps."},
{Role: "assistant", Body: "Placeholder only: chat execution, command orchestration, and result streaming will be added in a later pass."},
},
},
{
Title: "Recon Ideas",
Subtitle: "Placeholder thread for future guided workflows",
Messages: []aiMessage{
{Role: "assistant", Body: "This thread will eventually hold scoped recon plans, command suggestions, and concise result summaries."},
{Role: "assistant", Body: "For now it exists to exercise the sidebar, transcript pane, and context inspector layout."},
},
},
{
Title: "Tooling Backlog",
Subtitle: "Stubbed product notes for the upcoming AI integration",
Messages: []aiMessage{
{Role: "system", Body: "Planned modules: conversation persistence, tool traces, target-aware prompts, and execution approvals."},
{Role: "assistant", Body: fmt.Sprintf("Current server profile: %s (%s).", ctx.connection.Profile, ctx.connection.State)},
},
},
{
Title: "Operator Scratchpad",
Subtitle: "Local-only placeholder messages",
Messages: []aiMessage{
{Role: "assistant", Body: "Submitting a prompt here will append a local placeholder exchange so we can exercise the composer and transcript layout before backend work starts."},
},
},
}
return ctx
}
func fallback(value, def string) string {
value = strings.TrimSpace(value)
if value == "" {
return def
}
return value
}
func formatUnix(ts int64) string {
if ts <= 0 {
return "<unknown>"
}
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
}
+31
View File
@@ -0,0 +1,31 @@
package ai
import (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
// Commands returns the ai command.
func Commands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
}
// ServerCommands returns the ai command for the top-level client REPL.
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
}
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
return &cobra.Command{
Use: consts.AIStr,
Short: "Open the Sliver AI TUI layout preview",
Long: help.GetHelpFor([]string{consts.AIStr}),
Args: cobra.NoArgs,
GroupID: groupID,
Run: func(cmd *cobra.Command, args []string) {
AICmd(cmd, con, args)
},
}
}
+768
View File
@@ -0,0 +1,768 @@
package ai
import (
"strings"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
clienttheme "github.com/bishopfox/sliver/client/theme"
"golang.org/x/term"
)
const (
aiMinWidth = 72
aiMinHeight = 18
aiPaneHorizontalChrome = 4
aiPaneVerticalChrome = 2
)
type aiFocus int
const (
aiFocusSidebar aiFocus = iota
aiFocusTranscript
aiFocusDetails
aiFocusComposer
)
type aiStyles struct {
badge lipgloss.Style
chip lipgloss.Style
chipMuted lipgloss.Style
subtleText lipgloss.Style
footerHint lipgloss.Style
pane lipgloss.Style
paneFocused lipgloss.Style
paneTitle lipgloss.Style
item lipgloss.Style
itemSelected lipgloss.Style
itemFocused lipgloss.Style
roleSystem lipgloss.Style
roleAssistant lipgloss.Style
roleUser lipgloss.Style
heading lipgloss.Style
cursor lipgloss.Style
inputText lipgloss.Style
inputPlaceholder lipgloss.Style
warning lipgloss.Style
}
type aiModel struct {
width int
height int
focus aiFocus
ctx aiContext
selectedConversation int
input []rune
cursor int
status string
styles aiStyles
}
func newAIModel(ctx aiContext) *aiModel {
return &aiModel{
focus: aiFocusSidebar,
ctx: ctx,
status: ctx.status,
styles: newAIStyles(),
}
}
func newAIStyles() aiStyles {
return aiStyles{
badge: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.DefaultMod(900)).
Background(clienttheme.Primary()).
Padding(0, 1),
chip: lipgloss.NewStyle().
Foreground(clienttheme.PrimaryMod(700)).
Background(clienttheme.PrimaryMod(50)).
Padding(0, 1),
chipMuted: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(700)).
Background(clienttheme.DefaultMod(50)).
Padding(0, 1),
subtleText: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(500)),
footerHint: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(700)),
pane: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(clienttheme.DefaultMod(300)).
Padding(0, 1),
paneFocused: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(clienttheme.Primary()).
Padding(0, 1),
paneTitle: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.DefaultMod(900)),
item: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(800)),
itemSelected: lipgloss.NewStyle().
Foreground(clienttheme.Primary()).
Bold(true),
itemFocused: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.DefaultMod(900)).
Background(clienttheme.PrimaryMod(800)),
roleSystem: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.Secondary()),
roleAssistant: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.Primary()),
roleUser: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.Warning()),
heading: lipgloss.NewStyle().
Bold(true).
Foreground(clienttheme.DefaultMod(900)),
cursor: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(900)).
Background(clienttheme.Primary()),
inputText: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(900)),
inputPlaceholder: lipgloss.NewStyle().
Foreground(clienttheme.DefaultMod(400)),
warning: lipgloss.NewStyle().
Foreground(clienttheme.Warning()).
Bold(true),
}
}
func (m *aiModel) Init() tea.Cmd {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
m.height = h
} else {
m.width = 100
m.height = 30
}
}
return nil
}
func (m *aiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case tea.KeyPressMsg:
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
return m, tea.Quit
}
if m.focus == aiFocusComposer {
return m.handleComposerKey(key)
}
return m.handleGlobalKey(key)
}
return m, nil
}
func (m *aiModel) handleGlobalKey(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyTab:
m.focus = (m.focus + 1) % 4
m.status = "Focus moved to " + m.focus.String() + "."
return m, nil
case tea.KeyEsc:
return m, tea.Quit
case tea.KeyUp:
if m.focus == aiFocusSidebar {
m.moveSelection(-1)
}
return m, nil
case tea.KeyDown:
if m.focus == aiFocusSidebar {
m.moveSelection(1)
}
return m, nil
case tea.KeyEnter:
if m.focus == aiFocusSidebar {
m.status = "Selected placeholder thread: " + m.currentConversation().Title + "."
}
return m, nil
}
switch key.Text {
case "q":
return m, tea.Quit
case "j":
if m.focus == aiFocusSidebar {
m.moveSelection(1)
}
case "k":
if m.focus == aiFocusSidebar {
m.moveSelection(-1)
}
}
return m, nil
}
func (m *aiModel) handleComposerKey(key tea.Key) (tea.Model, tea.Cmd) {
if key.Code == tea.KeyTab {
m.focus = aiFocusSidebar
m.status = "Focus moved to " + m.focus.String() + "."
return m, nil
}
if key.Code == tea.KeyEsc {
m.focus = aiFocusTranscript
m.status = "Composer blurred. Press q or Esc again outside the composer to exit."
return m, nil
}
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'u' {
m.input = nil
m.cursor = 0
m.status = "Prompt cleared."
return m, nil
}
switch key.Code {
case tea.KeyEnter:
m.capturePlaceholderPrompt()
case tea.KeyBackspace:
if m.cursor > 0 {
m.input = append(m.input[:m.cursor-1], m.input[m.cursor:]...)
m.cursor--
}
case tea.KeyDelete:
if m.cursor < len(m.input) {
m.input = append(m.input[:m.cursor], m.input[m.cursor+1:]...)
}
case tea.KeyLeft:
if m.cursor > 0 {
m.cursor--
}
case tea.KeyRight:
if m.cursor < len(m.input) {
m.cursor++
}
case tea.KeyHome:
m.cursor = 0
case tea.KeyEnd:
m.cursor = len(m.input)
default:
if key.Text != "" {
insert := []rune(key.Text)
m.input = append(m.input[:m.cursor], append(insert, m.input[m.cursor:]...)...)
m.cursor += len(insert)
}
}
return m, nil
}
func (m *aiModel) View() tea.View {
if m.width < aiMinWidth || m.height < aiMinHeight {
return aiView(m.renderTooSmall())
}
headerHeight := 1
if m.width >= 90 {
headerHeight = 2
}
composerHeight := 4
if m.width >= 96 {
composerHeight = 5
}
footerHeight := 1
bodyHeight := maxInt(1, m.height-headerHeight-composerHeight-footerHeight)
header := m.renderHeader(headerHeight)
body := m.renderBody(bodyHeight)
composer := m.renderComposer(composerHeight)
footer := m.renderFooter()
return aiView(lipgloss.JoinVertical(lipgloss.Left, header, body, composer, footer))
}
func (m *aiModel) renderTooSmall() string {
lines := []string{
m.styles.badge.Render("SLIVER AI"),
"",
m.styles.warning.Render("Terminal too small for the AI layout preview."),
m.styles.subtleText.Render("Resize to at least 72x18 to see the full sidebar/chat/context shell."),
}
return strings.Join(lines, "\n")
}
func (m *aiModel) renderHeader(height int) string {
pieces := []string{
m.styles.badge.Render("SLIVER AI"),
m.styles.chip.Render("layout preview"),
m.styles.chipMuted.Render(m.ctx.target.Label),
m.styles.chipMuted.Render(m.ctx.connection.State),
m.styles.chipMuted.Render(m.layoutName()),
}
row := fitStyledPieces(m.width, pieces)
row = lipgloss.NewStyle().Width(m.width).Render(row)
if height == 1 {
return row
}
subtitle := "Bubble Tea shell for future target-aware chat, tool traces, and operator context."
return lipgloss.JoinVertical(
lipgloss.Left,
row,
m.styles.subtleText.Width(m.width).Render(truncateText(subtitle, m.width)),
)
}
func (m *aiModel) renderBody(height int) string {
switch {
case m.width >= 110:
sidebarWidth := clampInt(m.width/5, 24, 28)
detailsWidth := clampInt(m.width/4, 28, 32)
transcriptWidth := maxInt(34, m.width-sidebarWidth-detailsWidth)
return lipgloss.JoinHorizontal(
lipgloss.Top,
m.renderSidebar(sidebarWidth, height),
m.renderTranscript(transcriptWidth, height),
m.renderDetails(detailsWidth, height),
)
case m.width >= 78:
sidebarWidth := clampInt(m.width/4, 24, 28)
mainWidth := maxInt(34, m.width-sidebarWidth)
detailsHeight := clampInt(height/3, 5, 8)
if height-detailsHeight < 7 {
detailsHeight = maxInt(4, height-7)
}
chatHeight := maxInt(6, height-detailsHeight)
mainColumn := lipgloss.JoinVertical(
lipgloss.Left,
m.renderTranscript(mainWidth, chatHeight),
m.renderDetails(mainWidth, detailsHeight),
)
return lipgloss.JoinHorizontal(
lipgloss.Top,
m.renderSidebar(sidebarWidth, height),
mainColumn,
)
default:
sidebarHeight := clampInt(height/4, 5, 7)
detailsHeight := clampInt(height/4, 5, 7)
transcriptHeight := maxInt(6, height-sidebarHeight-detailsHeight)
return lipgloss.JoinVertical(
lipgloss.Left,
m.renderSidebar(m.width, sidebarHeight),
m.renderTranscript(m.width, transcriptHeight),
m.renderDetails(m.width, detailsHeight),
)
}
}
func (m *aiModel) renderSidebar(width, height int) string {
innerWidth := innerPaneWidth(width)
innerHeight := innerPaneHeight(height)
lines := []string{
m.styles.paneTitle.Render("Conversations"),
}
bodyHeight := maxInt(1, innerHeight-1)
for i, conversation := range m.ctx.conversations {
line := truncateText(conversation.Title, maxInt(1, innerWidth-2))
if i == m.selectedConversation {
line = "> " + line
style := m.styles.itemSelected
if m.focus == aiFocusSidebar {
style = m.styles.itemFocused
}
lines = append(lines, style.Width(innerWidth).Render(line))
} else {
lines = append(lines, m.styles.item.Width(innerWidth).Render(" "+line))
}
if len(lines)-1 >= bodyHeight {
return m.renderPane(width, height, aiFocusSidebar, lines)
}
}
quickPrompts := []string{
"Quick prompts:",
"- summarize target",
"- plan next step",
"- explain latest result",
}
if remaining := bodyHeight - (len(lines) - 1); remaining > 2 {
lines = append(lines, "")
for _, prompt := range quickPrompts {
if len(lines)-1 >= bodyHeight {
break
}
lines = append(lines, m.styles.subtleText.Width(innerWidth).Render(truncateText(prompt, innerWidth)))
}
}
return m.renderPane(width, height, aiFocusSidebar, lines)
}
func (m *aiModel) renderTranscript(width, height int) string {
innerWidth := innerPaneWidth(width)
innerHeight := innerPaneHeight(height)
lines := []string{
m.styles.paneTitle.Render("Conversation"),
}
conversation := m.currentConversation()
header := []string{
m.styles.heading.Width(innerWidth).Render(truncateText(conversation.Title, innerWidth)),
m.styles.subtleText.Width(innerWidth).Render(truncateText(conversation.Subtitle, innerWidth)),
}
lines = append(lines, header...)
bodyHeight := maxInt(1, innerHeight-len(lines))
messageLines := make([]string, 0, bodyHeight)
for _, message := range conversation.Messages {
messageLines = append(messageLines, m.renderMessageLines(message, innerWidth)...)
}
messageLines = tailLines(messageLines, bodyHeight)
lines = append(lines, messageLines...)
return m.renderPane(width, height, aiFocusTranscript, lines)
}
func (m *aiModel) renderDetails(width, height int) string {
innerWidth := innerPaneWidth(width)
innerHeight := innerPaneHeight(height)
lines := []string{
m.styles.paneTitle.Render("Context"),
m.styles.heading.Render("Target"),
}
targetLines := []string{
m.ctx.target.Label,
m.ctx.target.Host,
m.ctx.target.OS + "/" + m.ctx.target.Arch + " via " + m.ctx.target.C2,
}
targetLines = append(targetLines, m.ctx.target.Details...)
for _, line := range targetLines {
lines = append(lines, m.styles.item.Width(innerWidth).Render(truncateText(line, innerWidth)))
}
lines = append(lines, m.styles.heading.Render("Connection"))
connectionLines := []string{
m.ctx.connection.Profile,
m.ctx.connection.Server,
"operator: " + m.ctx.connection.Operator,
"state: " + m.ctx.connection.State,
}
for _, line := range connectionLines {
lines = append(lines, m.styles.subtleText.Width(innerWidth).Render(truncateText(line, innerWidth)))
}
lines = append(lines, m.styles.heading.Render("Planned"))
for _, line := range m.ctx.planned {
wrapped := wrapText("- "+line, innerWidth)
for _, wrappedLine := range wrapped {
lines = append(lines, m.styles.subtleText.Width(innerWidth).Render(truncateText(wrappedLine, innerWidth)))
}
}
return m.renderPane(width, height, aiFocusDetails, headLines(lines, innerHeight))
}
func (m *aiModel) renderComposer(height int) string {
innerWidth := innerPaneWidth(m.width)
innerHeight := innerPaneHeight(height)
lines := []string{
m.styles.paneTitle.Render("Composer"),
m.renderInputLine(innerWidth),
}
status := truncateText(m.status, innerWidth)
lines = append(lines, m.styles.subtleText.Width(innerWidth).Render(status))
if innerHeight-len(lines) > 0 {
help := "tab focus enter placeholder-send ctrl+u clear esc blur q quit"
lines = append(lines, m.styles.subtleText.Width(innerWidth).Render(truncateText(help, innerWidth)))
}
return m.renderPane(m.width, height, aiFocusComposer, headLines(lines, innerHeight))
}
func (m *aiModel) renderFooter() string {
pieces := []string{
m.styles.chipMuted.Render("focus: " + m.focus.String()),
m.styles.footerHint.Render("tab: next pane"),
m.styles.footerHint.Render("j/k: move"),
m.styles.footerHint.Render("enter: select/send"),
m.styles.footerHint.Render("esc: back"),
m.styles.footerHint.Render("q: quit"),
}
return lipgloss.NewStyle().Width(m.width).Render(fitStyledPieces(m.width, pieces))
}
func (m *aiModel) renderPane(width, height int, focus aiFocus, lines []string) string {
body := strings.Join(headLines(lines, innerPaneHeight(height)), "\n")
style := m.styles.pane
if m.focus == focus {
style = m.styles.paneFocused
}
return style.Width(width).Height(height).Render(body)
}
func (m *aiModel) renderMessageLines(message aiMessage, width int) []string {
label, style := m.messageLabel(message.Role)
bodyWidth := maxInt(10, width-5)
wrapped := wrapText(message.Body, bodyWidth)
lines := make([]string, 0, len(wrapped)+1)
for i, line := range wrapped {
prefix := " "
if i == 0 {
prefix = style.Render(label)
}
lines = append(lines, prefix+" "+line)
}
lines = append(lines, "")
return lines
}
func (m *aiModel) messageLabel(role string) (string, lipgloss.Style) {
switch role {
case "system":
return "SYS ", m.styles.roleSystem
case "user":
return "YOU ", m.styles.roleUser
default:
return "AI ", m.styles.roleAssistant
}
}
func (m *aiModel) renderInputLine(width int) string {
prefix := m.styles.roleUser.Render("YOU")
available := maxInt(1, width-4)
content := m.renderInputContent(available)
return prefix + " " + content
}
func (m *aiModel) renderInputContent(width int) string {
if len(m.input) == 0 {
placeholder := truncateText("Ask Sliver AI about the active target...", maxInt(1, width))
if m.focus == aiFocusComposer {
if width == 1 {
return m.styles.cursor.Render(" ")
}
return m.styles.cursor.Render(" ") + m.styles.inputPlaceholder.Render(truncateText(placeholder, width-1))
}
return m.styles.inputPlaceholder.Render(placeholder)
}
visible, cursor := inputWindow(m.input, m.cursor, width)
var b strings.Builder
for i, r := range visible {
ch := string(r)
if i == cursor && m.focus == aiFocusComposer {
b.WriteString(m.styles.cursor.Render(ch))
continue
}
b.WriteString(m.styles.inputText.Render(ch))
}
if cursor == len(visible) && m.focus == aiFocusComposer && lipgloss.Width(b.String()) < width {
b.WriteString(m.styles.cursor.Render(" "))
}
return b.String()
}
func (m *aiModel) moveSelection(delta int) {
if len(m.ctx.conversations) == 0 {
return
}
m.selectedConversation = clampInt(m.selectedConversation+delta, 0, len(m.ctx.conversations)-1)
m.status = "Viewing placeholder thread: " + m.currentConversation().Title + "."
}
func (m *aiModel) capturePlaceholderPrompt() {
prompt := strings.TrimSpace(string(m.input))
if prompt == "" {
m.status = "Type a prompt first. Submission is still placeholder-only for now."
return
}
conversation := m.currentConversation()
conversation.Messages = append(conversation.Messages,
aiMessage{Role: "user", Body: prompt},
aiMessage{Role: "assistant", Body: "Placeholder response: this prompt was captured locally so we can exercise the transcript layout before wiring the real AI backend."},
)
m.input = nil
m.cursor = 0
m.status = "Captured placeholder prompt in " + conversation.Title + "."
}
func (m *aiModel) currentConversation() *aiConversation {
if len(m.ctx.conversations) == 0 {
m.ctx.conversations = []aiConversation{{Title: "Untitled", Subtitle: "", Messages: nil}}
m.selectedConversation = 0
}
if m.selectedConversation < 0 || m.selectedConversation >= len(m.ctx.conversations) {
m.selectedConversation = 0
}
return &m.ctx.conversations[m.selectedConversation]
}
func (m aiFocus) String() string {
switch m {
case aiFocusSidebar:
return "sidebar"
case aiFocusTranscript:
return "conversation"
case aiFocusDetails:
return "context"
case aiFocusComposer:
return "composer"
default:
return "unknown"
}
}
func (m *aiModel) layoutName() string {
switch {
case m.width >= 110:
return "three-pane"
case m.width >= 78:
return "split"
default:
return "stacked"
}
}
func aiView(content string) tea.View {
view := tea.NewView(content)
view.AltScreen = true
return view
}
func inputWindow(input []rune, cursor, width int) ([]rune, int) {
if width <= 0 {
return nil, 0
}
if cursor < 0 {
cursor = 0
}
if cursor > len(input) {
cursor = len(input)
}
start := 0
if len(input) > width {
start = cursor - width + 1
if start < 0 {
start = 0
}
if start > len(input)-width {
start = len(input) - width
}
}
end := minInt(len(input), start+width)
return input[start:end], cursor - start
}
func fitStyledPieces(width int, pieces []string) string {
if width <= 0 {
return ""
}
var kept []string
for _, piece := range pieces {
candidate := strings.Join(append(kept, piece), " ")
if lipgloss.Width(candidate) > width {
break
}
kept = append(kept, piece)
}
return strings.Join(kept, " ")
}
func wrapText(text string, width int) []string {
if width <= 1 {
return []string{truncateText(text, width)}
}
paragraphs := strings.Split(text, "\n")
lines := make([]string, 0, len(paragraphs))
for _, paragraph := range paragraphs {
words := strings.Fields(paragraph)
if len(words) == 0 {
lines = append(lines, "")
continue
}
current := words[0]
for _, word := range words[1:] {
if utf8.RuneCountInString(current)+1+utf8.RuneCountInString(word) <= width {
current += " " + word
continue
}
lines = append(lines, current)
current = word
}
lines = append(lines, current)
}
return lines
}
func truncateText(text string, width int) string {
if width <= 0 {
return ""
}
if utf8.RuneCountInString(text) <= width {
return text
}
if width <= 3 {
return string([]rune(text)[:width])
}
runes := []rune(text)
return string(runes[:width-3]) + "..."
}
func headLines(lines []string, count int) []string {
if count <= 0 {
return nil
}
if len(lines) <= count {
return lines
}
return lines[:count]
}
func tailLines(lines []string, count int) []string {
if count <= 0 {
return nil
}
if len(lines) <= count {
return lines
}
return lines[len(lines)-count:]
}
func innerPaneWidth(width int) int {
return maxInt(1, width-aiPaneHorizontalChrome)
}
func innerPaneHeight(height int) int {
return maxInt(1, height-aiPaneVerticalChrome)
}
func clampInt(value, low, high int) int {
if value < low {
return low
}
if value > high {
return high
}
return value
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
+1 -1
View File
@@ -250,7 +250,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
+1 -1
View File
@@ -377,7 +377,7 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
// Flags
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
extensionCmd.Flags().AddFlagSet(f)
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
+1 -1
View File
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
// Session flags and completions.
+9
View File
@@ -56,6 +56,7 @@ var (
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.AIStr: aiHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.HexEditStr: hexEditHelp,
@@ -167,6 +168,14 @@ _=/usr/bin/env
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
[[.Bold]]About:[[.Normal]] Get information about a Sliver by name, or for the active Sliver.`
aiHelp = `[[.Bold]]Command:[[.Normal]] ai
[[.Bold]]About:[[.Normal]] Launch the Sliver AI Bubble Tea layout preview. This currently provides a full-screen TUI shell with placeholder conversations, context panes, and a composer, but does not yet execute real chat or tool actions.
[[.Bold]]Example:[[.Normal]]
use my-session
ai
`
useHelp = `[[.Bold]]Command:[[.Normal]] use [sliver name/session]
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
+2 -2
View File
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.NetworkHelpGroup,
}
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
f.Int32P("kill", "k", -1, "kill a background job")
f.BoolP("kill-all", "K", false, "kill all jobs")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = JobsIDCompleter(con)
+1 -1
View File
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
f.StringP("filter", "f", "", "filter based on loot type")
+1 -1
View File
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
pivotsCmd.AddCommand(pivotDetailsCmd)
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
})
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
(*comp)["id"] = PivotIDCompleter(con)
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, addCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote target host:port (e.g., 10.0.0.1:445)")
f.StringP("bind", "b", "127.0.0.1:8080", "bind port forward to interface")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(addCmd, func(comp *carapace.ActionMap) {
(*comp)["bind"] = completers.ClientInterfacesCompleter()
+11 -1
View File
@@ -68,13 +68,23 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
bindAddr = fmt.Sprintf("127.0.0.1:%s", bindAddr)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
var keepAlivePeriod time.Duration
if keepAlive > 0 {
keepAlivePeriod = time.Duration(keepAlive) * time.Second
} else if keepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
tcpProxy := &tcpproxy.Proxy{}
channelProxy := &core.ChannelProxy{
Rpc: con.Rpc,
Session: session,
RemoteAddr: remoteAddr,
BindAddr: bindAddr,
KeepAlivePeriod: 60 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
+1 -1
View File
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GetSystemCmd(cmd, con, args)
},
GroupID: consts.PrivilegesHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
}
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
+8
View File
@@ -37,6 +37,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if session == nil && beacon == nil {
return
}
if session == nil {
con.PrintErrorf("Command is only supported for sessions.\n")
return
}
targetOS := getOS(session, beacon)
if targetOS != "windows" {
con.PrintErrorf("Command only supported on Windows.\n")
@@ -45,6 +49,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
process, _ := cmd.Flags().GetString("process")
config := con.GetActiveSessionConfig()
if config == nil {
con.PrintErrorf("Failed to derive active session config.\n")
return
}
/* If the HTTP C2 Config name is not defined, then put in the default value
This will have no effect on implants that do not use HTTP C2
+1 -1
View File
@@ -52,7 +52,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("loot-name", "N", "", "name to assign when adding the memory dump to the loot store (optional)")
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
+2 -2
View File
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
timeout, _ := cmd.Flags().GetInt32("timeout")
timeout, _ := cmd.Flags().GetInt64("timeout")
if timeout < 1 {
con.PrintErrorf("Invalid timeout argument\n")
@@ -68,7 +68,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
dump, err := con.Rpc.ProcessDump(context.Background(), &sliverpb.ProcessDumpReq{
Request: con.ActiveTarget.Request(cmd),
Pid: int32(pid),
Timeout: timeout,
Timeout: int32(timeout),
})
ctrl <- true
<-ctrl
+1 -1
View File
@@ -22,7 +22,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("registry", true, registryCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
registryReadCmd := &cobra.Command{
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, rportfwdAddCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote address <ip>:<port> connection is forwarded to")
f.StringP("bind", "b", "", "bind address <ip>:<port> for implants to listen on")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(rportfwdAddCmd, func(comp *carapace.ActionMap) {
(*comp)["remote"] = completers.ClientInterfacesCompleter()
+3
View File
@@ -53,10 +53,13 @@ func StartRportFwdListenerCmd(cmd *cobra.Command, con *console.SliverClient, arg
if portNumberOnlyRegexp.MatchString(forwardAddress) {
forwardAddress = fmt.Sprintf("127.0.0.1:%s", forwardAddress)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
rportfwdListener, err := con.Rpc.StartRportFwdListener(context.Background(), &sliverpb.RportFwdStartListenerReq{
Request: con.ActiveTarget.Request(cmd),
BindAddress: bindAddress,
ForwardAddress: forwardAddress,
KeepAlive: keepAlive,
})
if err != nil {
con.PrintWarnf("%s\n", err)
+2
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/command/ai"
"github.com/bishopfox/sliver/client/command/aka"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/armory"
@@ -98,6 +99,7 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
// Core
bind(consts.GenericHelpGroup,
exit.Command,
ai.ServerCommands,
serverctx.Commands,
licenses.Commands,
settings.Commands,
+1 -1
View File
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("sessions", false, sessionsCmd, func(f *pflag.FlagSet) {
f.StringP("interact", "i", "", "interact with a session")
+3 -1
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/ai"
"github.com/bishopfox/sliver/client/command/aka"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/backdoor"
@@ -32,6 +33,7 @@ import (
"github.com/bishopfox/sliver/client/command/exec"
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/command/hexedit"
"github.com/bishopfox/sliver/client/command/info"
"github.com/bishopfox/sliver/client/command/kill"
"github.com/bishopfox/sliver/client/command/network"
@@ -49,7 +51,6 @@ import (
"github.com/bishopfox/sliver/client/command/tasks"
"github.com/bishopfox/sliver/client/command/wasm"
"github.com/bishopfox/sliver/client/command/wireguard"
"github.com/bishopfox/sliver/client/command/hexedit"
client "github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/reeflective/console"
@@ -78,6 +79,7 @@ func SliverCommands(con *client.SliverClient) console.Commands {
// [ Core ]
bind(consts.SliverCoreHelpGroup,
ai.Commands,
reconfig.Commands,
// sessions.Commands,
sessions.SliverCommands,
+1 -1
View File
@@ -23,7 +23,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.BeaconCmdsFilter),
}
flags.Bind("tasks", true, tasksCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.BoolP("overflow", "O", false, "overflow terminal width (display truncated rows)")
f.IntP("skip-pages", "S", 0, "skip the first n page(s)")
f.StringP("filter", "f", "", "filter based on task type (case-insensitive prefix matching)")
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.Bool("force", false, "skip version comparison and download the latest release")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
+1 -1
View File
@@ -60,7 +60,7 @@ const (
func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
VerboseVersionsCmd(cmd, con, args)
timeoutF, _ := cmd.Flags().GetInt("timeout")
timeoutF, _ := cmd.Flags().GetInt64("timeout")
timeout := time.Duration(timeoutF) * time.Second
insecure, _ := cmd.Flags().GetBool("insecure")
+1
View File
@@ -220,6 +220,7 @@ const (
MkdirStr = "mkdir"
CdStr = "cd"
PwdStr = "pwd"
AIStr = "ai"
CatStr = "cat"
EditStr = "edit"
HexEditStr = "hex-edit"
+1
View File
@@ -216,6 +216,7 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
Port: p.Port(),
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
})
if err != nil {
return nil, err
+17 -3
View File
@@ -20,10 +20,15 @@ set -u
set -o pipefail
SKIP_GENERATE=0
UNIT_ONLY=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--unit-only" ]; then
UNIT_ONLY=1
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -219,8 +224,13 @@ done
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
if [ "$UNIT_ONLY" -eq 1 ]; then
echo
echo "Skipping ./server/c2 e2e tests (--unit-only)"
else
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
fi
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
@@ -235,5 +245,9 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo
echo "Skipping ./server/generate tests (--skip-generate)"
if [ "$UNIT_ONLY" -eq 1 ]; then
echo "Skipping ./server/generate tests (--unit-only)"
else
echo "Skipping ./server/generate tests (--skip-generate)"
fi
fi
+1 -2
View File
@@ -211,7 +211,6 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
@@ -231,7 +230,7 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/colorprofile v0.4.2
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
-4
View File
@@ -4,8 +4,6 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
@@ -194,8 +192,6 @@ github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRb
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
+10 -1
View File
@@ -97,11 +97,20 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
}
}
tcpProxy := &tcpproxy.Proxy{}
var keepAlivePeriod time.Duration
if req.KeepAlive > 0 {
keepAlivePeriod = time.Duration(req.KeepAlive) * time.Second
} else if req.KeepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
channelProxy := &rportfwd.ChannelProxy{
Conn: connection,
RemoteAddr: req.ForwardAddress,
BindAddr: req.BindAddress,
KeepAlivePeriod: 1000 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(req.BindAddress, channelProxy)
@@ -81,9 +81,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if portfwdReq.KeepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if portfwdReq.KeepAlive > 0 {
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
+10 -3
View File
@@ -140,9 +140,16 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if p.KeepAlivePeriod < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if p.KeepAlivePeriod > 0 {
conn.SetKeepAlivePeriod(p.KeepAlivePeriod)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
// {{if .Config.Debug}}
@@ -58,6 +58,7 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
var buf []byte
var err error
var reqHandle uintptr
var connHandle uintptr
var resp *Response
var rawBody []byte
@@ -81,9 +82,17 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
Body: rawBody,
}
if reqHandle, err = buildRequest(c.handle, req); err != nil {
if connHandle, reqHandle, err = buildRequest(c.handle, req); err != nil {
if reqHandle != 0 {
InternetCloseHandle(reqHandle)
}
if connHandle != 0 {
InternetCloseHandle(connHandle)
}
return nil, err
}
defer InternetCloseHandle(reqHandle)
defer InternetCloseHandle(connHandle)
if c.Timeout > 0 {
buf = make([]byte, 4)
@@ -232,3 +241,11 @@ func (jar *Jar) SetCookies(u *url.URL, cookies []*Cookie) {
func (jar *Jar) Cookies(u *url.URL) []*Cookie {
return jar.cookies
}
// Close will close the client handle
func (c *Client) Close() {
if c.handle != 0 {
InternetCloseHandle(c.handle)
c.handle = 0
}
}
@@ -17,7 +17,7 @@ func convertFail(str string, err error) error {
)
}
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, uintptr, error) {
var connHndl uintptr
var err error
var flags uintptr
@@ -29,7 +29,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
// Parse URL
if uri, err = url.Parse(r.URL); err != nil {
return 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
return 0, 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
}
passwd, _ = uri.User.Password()
@@ -37,7 +37,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
if uri.Port() != "" {
if port, err = strconv.ParseInt(uri.Port(), 10, 64); err != nil {
err = fmt.Errorf("port %s invalid: %w", uri.Port(), err)
return 0, err
return 0, 0, err
}
}
@@ -58,7 +58,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to create connection: %w", err)
return 0, 0, fmt.Errorf("failed to create connection: %w", err)
}
// Send query string too
@@ -82,10 +82,10 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to open request: %w", err)
return connHndl, 0, fmt.Errorf("failed to open request: %w", err)
}
return reqHndl, nil
return connHndl, reqHndl, nil
}
var cookies []*Cookie
@@ -213,6 +213,20 @@ func HTTPSendRequestW(
return nil
}
// InternetCloseHandle is from wininet.h
func InternetCloseHandle(hndl uintptr) error {
var err error
var proc string = "InternetCloseHandle"
var success uintptr
success, _, err = wininet.NewProc(proc).Call(hndl)
if success == 0 {
return fmt.Errorf("%s: %w", proc, err)
}
return nil
}
// InternetConnectW is from wininet.h
func InternetConnectW(
sessionHndl uintptr,
@@ -437,7 +451,7 @@ func InternetErrorDlg(
)
// we expect 12032 if user hits okay, otherwise we return the response code as an error
if uint32(success) != ERROR_INTERNET_FORCE_RETRY {
return success, fmt.Errorf("%s: %s", proc, success)
return success, fmt.Errorf("%s: %v", proc, success)
}
*lppvData = buf
File diff suppressed because it is too large. Load diff
+2
View File
@@ -883,6 +883,7 @@ message PortfwdReq {
int32 Protocol = 2;
string Host = 3;
int32 KeepAlive = 4;
uint64 TunnelID = 8 [jstype = JS_STRING]; // Bind to this tunnel
commonpb.Request Request = 9;
}
@@ -1185,6 +1186,7 @@ message RportFwdStartListenerReq {
uint32 BindPort = 2;
uint32 ForwardPort = 3;
string ForwardAddress = 4;
int32 KeepAlive = 5;
commonpb.Request Request = 9;
}
+69
View File
@@ -0,0 +1,69 @@
package c2
import (
"io"
"os"
)
const (
// socketEnvelopeDiskSpoolThreshold is the point where envelope payloads are
// streamed to disk first to avoid large attacker-triggered allocations.
socketEnvelopeDiskSpoolThreshold = 64 * 1024 * 1024
// socketEnvelopeReadChunkSize bounds peak in-memory usage while reading large
// envelopes from the network.
socketEnvelopeReadChunkSize = 1 * 1024 * 1024
)
func readSocketEnvelopeData(reader io.Reader, dataLength int, inMemoryLimit int) ([]byte, error) {
if dataLength <= inMemoryLimit {
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(reader, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
tmpFile, err := os.CreateTemp("", "sliver-envelope-*")
if err != nil {
return nil, err
}
tmpPath := tmpFile.Name()
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
}()
chunkSize := socketEnvelopeReadChunkSize
if dataLength < chunkSize {
chunkSize = dataLength
}
chunkBuf := make([]byte, chunkSize)
remaining := dataLength
for remaining > 0 {
nextRead := chunkSize
if remaining < nextRead {
nextRead = remaining
}
if _, err := io.ReadFull(reader, chunkBuf[:nextRead]); err != nil {
return nil, err
}
if _, err := tmpFile.Write(chunkBuf[:nextRead]); err != nil {
return nil, err
}
remaining -= nextRead
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
return nil, err
}
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(tmpFile, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
+135
View File
@@ -0,0 +1,135 @@
package c2
import (
"bytes"
"encoding/binary"
"errors"
"io"
"math"
"net"
"runtime"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/minisign"
)
type readSizeRecorder struct {
reader io.Reader
maxRead int
}
func (r *readSizeRecorder) Read(p []byte) (int, error) {
if len(p) > r.maxRead {
r.maxRead = len(p)
}
return r.reader.Read(p)
}
func envelopeFrameWithDeclaredLength(dataLength uint32, payload []byte) []byte {
frame := make([]byte, minisign.RawSigSize+4+len(payload))
binary.LittleEndian.PutUint16(frame[:2], minisign.EdDSA)
binary.LittleEndian.PutUint32(frame[minisign.RawSigSize:minisign.RawSigSize+4], dataLength)
copy(frame[minisign.RawSigSize+4:], payload)
return frame
}
func measureEnvelopeReadAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error), frame []byte) (uint64, error) {
t.Helper()
srv, cli := net.Pipe()
defer srv.Close()
defer cli.Close()
errCh := make(chan error, 1)
go func() {
_, err := readFunc(srv)
errCh <- err
}()
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
go func() {
_, _ = cli.Write(frame)
_ = cli.Close()
}()
err := <-errCh
runtime.GC()
var after runtime.MemStats
runtime.ReadMemStats(&after)
if after.TotalAlloc < before.TotalAlloc {
return 0, err
}
return after.TotalAlloc - before.TotalAlloc, err
}
func assertLargeTruncatedFrameHasBoundedAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error)) {
t.Helper()
declaredLength := uint32(socketEnvelopeDiskSpoolThreshold + (8 * 1024 * 1024))
frame := envelopeFrameWithDeclaredLength(declaredLength, []byte{0x01})
minAllocDelta := uint64(math.MaxUint64)
for i := 0; i < 3; i++ {
allocDelta, err := measureEnvelopeReadAlloc(t, readFunc, frame)
if err == nil {
t.Fatalf("expected read failure for truncated frame")
}
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("expected EOF-like error for truncated frame, got %v", err)
}
if allocDelta < minAllocDelta {
minAllocDelta = allocDelta
}
}
const maxAllowedAlloc = 24 * 1024 * 1024
if minAllocDelta > maxAllowedAlloc {
t.Fatalf("expected bounded allocation <= %d bytes, got %d", maxAllowedAlloc, minAllocDelta)
}
}
func TestReadSocketEnvelopeDataKeepsSmallPayloadsInMemory(t *testing.T) {
payload := bytes.Repeat([]byte{0x7a}, 4096)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), 8192)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead != len(payload) {
t.Fatalf("expected in-memory read size %d, got %d", len(payload), recorder.maxRead)
}
}
func TestReadSocketEnvelopeDataSpoolsLargePayloadsInChunks(t *testing.T) {
payload := bytes.Repeat([]byte{0x42}, (2*socketEnvelopeReadChunkSize)+123)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), socketEnvelopeReadChunkSize/2)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead > socketEnvelopeReadChunkSize {
t.Fatalf("expected max read chunk <= %d, got %d", socketEnvelopeReadChunkSize, recorder.maxRead)
}
}
func TestMTLSSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketReadEnvelope)
}
func TestWGSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketWGReadEnvelope)
}
+2 -4
View File
@@ -358,10 +358,8 @@ func socketReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[pivot] invalid data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
mtlsLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+2 -5
View File
@@ -452,11 +452,8 @@ func socketWGReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[wireguard] zero data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
wgLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+58
View File
@@ -21,6 +21,7 @@ package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/bishopfox/sliver/server/db"
@@ -35,6 +36,17 @@ const (
serverNamespace = "server" // Operator servers
)
var (
// ErrOperatorClientCertificateNotFound indicates that the presented operator
// client certificate is no longer trusted because it is not present in the
// certificate store.
ErrOperatorClientCertificateNotFound = errors.New("operator client certificate not found in database")
// ErrInvalidOperatorClientCertificate indicates that the presented
// certificate is not shaped like an operator client leaf certificate.
ErrInvalidOperatorClientCertificate = errors.New("invalid operator client certificate")
)
// OperatorClientGenerateCertificate - Generate a certificate signed with a given CA
func OperatorClientGenerateCertificate(operator string) ([]byte, []byte, error) {
cert, key := GenerateECCCertificate(OperatorCA, operator, false, true, true)
@@ -52,6 +64,52 @@ func OperatorClientRemoveCertificate(operator string) error {
return RemoveCertificate(OperatorCA, ECCKey, fmt.Sprintf("%s.%s", clientNamespace, operator))
}
// ValidateOperatorClientCertificate ensures that the presented operator client
// certificate is still present in the database. A valid chain alone is not
// enough; the exact leaf certificate must still exist in storage.
func ValidateOperatorClientCertificate(peerCertificates []*x509.Certificate) error {
if len(peerCertificates) == 0 || peerCertificates[0] == nil {
return ErrInvalidOperatorClientCertificate
}
leaf := peerCertificates[0]
if leaf.IsCA || leaf.Subject.CommonName == "" || !hasExtKeyUsage(leaf, x509.ExtKeyUsageClientAuth) {
return ErrInvalidOperatorClientCertificate
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: leaf.Raw,
})
if len(pemBytes) == 0 {
return ErrInvalidOperatorClientCertificate
}
record := &models.Certificate{}
result := db.Session().Select("id").Where(&models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, leaf.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(pemBytes),
}).First(record)
if result.Error == nil {
return nil
}
if errors.Is(result.Error, db.ErrRecordNotFound) {
return ErrOperatorClientCertificateNotFound
}
return result.Error
}
func hasExtKeyUsage(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == usage {
return true
}
}
return false
}
// OperatorServerGetCertificate - Helper function to fetch a server cert
func OperatorServerGetCertificate(hostname string) ([]byte, []byte, error) {
return GetECCCertificate(OperatorCA, fmt.Sprintf("%s.%s", serverNamespace, hostname))
+117
View File
@@ -0,0 +1,117 @@
package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"testing"
"time"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
)
func TestValidateOperatorClientCertificateAdversarial(t *testing.T) {
SetupCAs()
t.Run("accepts stored operator client leaf", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "stored")
certPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator client certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, certPEM)})
if err != nil {
t.Fatalf("expected stored operator client certificate to be accepted, got %v", err)
}
})
t.Run("rejects valid stored certificate hidden behind an unstored first certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "hidden")
storedCertPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{
mustParseCertificate(t, rogueCertPEM),
mustParseCertificate(t, storedCertPEM),
})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected first unstored certificate to be rejected, got %v", err)
}
})
t.Run("rejects same-common-name different-leaf certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "same-cn")
_, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, rogueCertPEM)})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected rotated or forged same-CN leaf to be rejected, got %v", err)
}
})
t.Run("rejects stored operator server certificate", func(t *testing.T) {
hostName := uniqueOperatorCertificateName(t, "server")
serverCertPEM, _, err := OperatorServerGenerateCertificate(hostName)
if err != nil {
t.Fatalf("generate operator server certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, serverCertPEM)})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected stored operator server certificate to be rejected, got %v", err)
}
})
t.Run("rejects operator CA certificate even if inserted into the certificates table", func(t *testing.T) {
caCertPEM, caKeyPEM, err := GetCertificateAuthorityPEM(OperatorCA)
if err != nil {
t.Fatalf("get operator CA: %v", err)
}
caCert := mustParseCertificate(t, caCertPEM)
record := &models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, caCert.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(caCertPEM),
PrivateKeyPEM: string(caKeyPEM),
}
if err := db.Session().Create(record).Error; err != nil {
t.Fatalf("insert CA cert into certificates table: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{caCert})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected CA certificate to be rejected, got %v", err)
}
})
}
func mustParseCertificate(t *testing.T, certPEM []byte) *x509.Certificate {
t.Helper()
block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse x509 certificate: %v", err)
}
return cert
}
func uniqueOperatorCertificateName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+29 -11
View File
@@ -218,18 +218,9 @@ func kickOperatorCmd(cmd *cobra.Command, args []string) {
}
fmt.Printf(Info+"Removing auth token(s) for %s, please wait ... \n", operator)
err = db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
err = kickOperator(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove operator %s: %v\n", operator, err)
return
}
transport.ClearTokenCache()
fmt.Printf(Info+"Removing client certificate(s) for %s, please wait ... \n", operator)
err = certs.OperatorClientRemoveCertificate(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove the operator certificate: %v \n", err)
fmt.Printf(Warn+"Failed to kick operator %s: %v\n", operator, err)
return
}
fmt.Printf(Info+"Operator %s has been kicked out.\n", operator)
@@ -242,6 +233,33 @@ func shouldPromptKickOperator(cmd *cobra.Command, args []string) bool {
return cmd.Flags().NFlag() == 0
}
func removeOperator(operator string) error {
err := db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
if err != nil {
return err
}
transport.ClearTokenCache()
return nil
}
func revokeOperatorClientCertificate(operator string) error {
return certs.OperatorClientRemoveCertificate(operator)
}
func closeOperatorStreams(operator string) {
transport.CloseOperatorStreams(operator)
}
func kickOperator(operator string) error {
if err := removeOperator(operator); err != nil {
return err
}
defer closeOperatorStreams(operator)
return revokeOperatorClientCertificate(operator)
}
func operatorNames() ([]string, error) {
operators, err := db.OperatorAll()
if err != nil {
+211
View File
@@ -0,0 +1,211 @@
package console
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"testing"
"time"
clientassets "github.com/bishopfox/sliver/client/assets"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
servertransport "github.com/bishopfox/sliver/server/transport"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
func TestKickOperatorClosesActiveEventStreams(t *testing.T) {
certs.SetupCAs()
listener := bufconn.Listen(2 * 1024 * 1024)
grpcServer, err := servertransport.StartMtlsClientServer(listener)
if err != nil {
t.Fatalf("start mTLS client server: %v", err)
}
defer grpcServer.Stop()
defer listener.Close()
operatorName := uniqueKickOperatorName(t)
t.Cleanup(func() {
_ = removeOperator(operatorName)
_ = revokeOperatorClientCertificate(operatorName)
closeOperatorStreams(operatorName)
})
config := mustNewOperatorAssetsConfig(t, operatorName)
rpcClient, conn, err := mustMTLSBufconnClient(t, listener, config)
if err != nil {
t.Fatalf("connect operator client: %v", err)
}
defer conn.Close()
stream, err := rpcClient.Events(context.Background(), &commonpb.Empty{})
if err != nil {
t.Fatalf("start events stream: %v", err)
}
waitForCondition(t, 3*time.Second, func() bool {
return operatorActive(operatorName)
}, "operator to appear in the active client registry")
recvErr := make(chan error, 1)
go func() {
_, err := stream.Recv()
recvErr <- err
}()
if err := kickOperator(operatorName); err != nil {
t.Fatalf("kick operator: %v", err)
}
select {
case err := <-recvErr:
if err == nil {
t.Fatal("expected kicked operator event stream to close")
}
if errors.Is(err, io.EOF) {
break
}
code := status.Code(err)
if code != codes.Canceled && code != codes.Unavailable && code != codes.Unknown {
t.Fatalf("expected stream cancellation after kick, got %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for kicked operator event stream to close")
}
waitForCondition(t, 3*time.Second, func() bool {
return !operatorActive(operatorName)
}, "operator to leave the active client registry")
_, err = rpcClient.GetVersion(context.Background(), &commonpb.Empty{})
if err == nil {
t.Fatal("expected kicked operator to be denied on subsequent RPCs")
}
code := status.Code(err)
if code != codes.Unauthenticated && code != codes.Unavailable {
t.Fatalf("expected unauthenticated or unavailable after kick, got %v", err)
}
exists, err := operatorExists(operatorName)
if err != nil {
t.Fatalf("lookup operator after kick: %v", err)
}
if exists {
t.Fatal("expected operator record to be removed after kick")
}
_, _, err = certs.OperatorClientGetCertificate(operatorName)
if !errors.Is(err, certs.ErrCertDoesNotExist) {
t.Fatalf("expected operator certificate to be removed after kick, got %v", err)
}
}
func mustNewOperatorAssetsConfig(t *testing.T, operatorName string) *clientassets.ClientConfig {
t.Helper()
configJSON, err := NewOperatorConfig(operatorName, "bufnet", 31337, []string{"all"})
if err != nil {
t.Fatalf("generate operator config: %v", err)
}
config := &clientassets.ClientConfig{}
if err := json.Unmarshal(configJSON, config); err != nil {
t.Fatalf("parse operator config: %v", err)
}
return config
}
func mustMTLSBufconnClient(t *testing.T, listener *bufconn.Listener, config *clientassets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
t.Helper()
dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(dialCtx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithTransportCredentials(credentials.NewTLS(mustClientTLSConfig(t, config))),
grpc.WithPerRPCCredentials(staticTokenAuth(config.Token)),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(clienttransport.ClientMaxReceiveMessageSize)),
grpc.WithBlock(),
)
if err != nil {
return nil, nil, err
}
return rpcpb.NewSliverRPCClient(conn), conn, nil
}
func mustClientTLSConfig(t *testing.T, config *clientassets.ClientConfig) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair([]byte(config.Certificate), []byte(config.PrivateKey))
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(config.CACertificate))
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return clienttransport.RootOnlyVerifyCertificate(config.CACertificate, rawCerts)
},
}
}
func operatorActive(name string) bool {
for _, active := range core.Clients.ActiveOperators() {
if active == name {
return true
}
}
return false
}
func waitForCondition(t *testing.T, timeout time.Duration, predicate func() bool, description string) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if predicate() {
return
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("timed out waiting for %s", description)
}
func uniqueKickOperatorName(t *testing.T) string {
t.Helper()
return fmt.Sprintf("kick-operator-%d", time.Now().UnixNano())
}
type staticTokenAuth string
func (t staticTokenAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{
"Authorization": "Bearer " + string(t),
}, nil
}
func (staticTokenAuth) RequireTransportSecurity() bool {
return true
}
+2
View File
@@ -74,6 +74,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(tokenAuthFunc),
permissionsStreamServerInterceptor(),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
@@ -90,6 +91,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
),
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(serverAuthFunc),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
+28 -5
View File
@@ -21,6 +21,7 @@ package transport
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"runtime/debug"
@@ -50,15 +51,34 @@ var (
// StartMtlsClientListener - Start a mutual TLS listener
func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listener, error) {
mtlsLog.Infof("Starting gRPC/mtls listener on %s:%d", host, port)
tlsConfig := getOperatorServerTLSConfig("multiplayer")
creds := credentials.NewTLS(tlsConfig)
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
mtlsLog.Error(err)
return nil, nil, err
}
grpcServer, err := StartMtlsClientServer(ln)
if err != nil {
ln.Close()
return nil, nil, err
}
return grpcServer, ln, nil
}
// StartMtlsClientServer serves the authenticated multiplayer gRPC server on an
// existing listener. This is primarily useful for tests that need the full mTLS
// + auth stack without opening a real TCP socket.
func StartMtlsClientServer(ln net.Listener) (*grpc.Server, error) {
if ln == nil {
return nil, errors.New("listener is required")
}
tlsConfig := getOperatorServerTLSConfig("multiplayer")
if tlsConfig == nil {
return nil, errors.New("failed to create operator TLS config")
}
creds := credentials.NewTLS(tlsConfig)
options := []grpc.ServerOption{
grpc.Creds(creds),
grpc.MaxRecvMsgSize(ServerMaxMessageSize),
@@ -81,7 +101,7 @@ func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listen
panicked = false
}
}()
return grpcServer, ln, nil
return grpcServer, nil
}
// getOperatorServerTLSConfig - Generate the TLS configuration, we do now allow the end user
@@ -115,6 +135,9 @@ func getOperatorServerTLSConfig(host string) *tls.Config {
ClientCAs: caCertPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
VerifyConnection: func(state tls.ConnectionState) error {
return certs.ValidateOperatorClientCertificate(state.PeerCertificates)
},
}
return tlsConfig
+153
View File
@@ -0,0 +1,153 @@
package transport
import (
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/server/certs"
)
func TestOperatorClientCertificateValidation(t *testing.T) {
certs.SetupCAs()
t.Run("accepts stored operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "stored")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err != nil {
t.Fatalf("expected stored operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects deleted operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "deleted")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected deleted operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected database rejection error, got %v", err)
}
})
t.Run("rejects rotated-out certificate but accepts current one", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "rotated")
oldCertPEM, oldKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate original operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove original operator certificate: %v", err)
}
newCertPEM, newKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate replacement operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, oldCertPEM, oldKeyPEM))
if err == nil {
t.Fatal("expected rotated-out operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected rotated certificate to fail database validation, got %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, newCertPEM, newKeyPEM))
if err != nil {
t.Fatalf("expected replacement operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects certificate from another authority", func(t *testing.T) {
certPEM, keyPEM := certs.GenerateECCCertificate(certs.HTTPSCA, uniqueOperatorName(t, "wrong-ca"), false, true, false)
err := performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected certificate from another authority to be rejected")
}
if strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected trust-chain rejection before database lookup, got %v", err)
}
})
}
func newClientTLSConfig(t *testing.T, certPEM []byte, keyPEM []byte) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
}
func performMutualTLSHandshake(serverConfig *tls.Config, clientConfig *tls.Config) error {
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
deadline := time.Now().Add(2 * time.Second)
_ = serverConn.SetDeadline(deadline)
_ = clientConn.SetDeadline(deadline)
serverTLS := tls.Server(serverConn, serverConfig)
clientTLS := tls.Client(clientConn, clientConfig)
defer serverTLS.Close()
defer clientTLS.Close()
type handshakeResult struct {
side string
err error
}
results := make(chan handshakeResult, 2)
go func() {
results <- handshakeResult{side: "server", err: serverTLS.Handshake()}
}()
go func() {
results <- handshakeResult{side: "client", err: clientTLS.Handshake()}
}()
var errs []string
for i := 0; i < 2; i++ {
result := <-results
if result.err == nil {
continue
}
if errors.Is(result.err, net.ErrClosed) {
continue
}
errs = append(errs, fmt.Sprintf("%s: %v", result.side, result.err))
}
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "; "))
}
func uniqueOperatorName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+130
View File
@@ -0,0 +1,130 @@
package transport
import (
"context"
"strings"
"sync"
"sync/atomic"
"github.com/bishopfox/sliver/server/db/models"
"google.golang.org/grpc"
)
type trackedServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (s *trackedServerStream) Context() context.Context {
return s.ctx
}
type operatorStreamRegistry struct {
mu sync.Mutex
nextID atomic.Uint64
operators map[string]map[uint64]context.CancelFunc
}
var activeOperatorStreams = &operatorStreamRegistry{
operators: map[string]map[uint64]context.CancelFunc{},
}
func (r *operatorStreamRegistry) register(operator string, cancel context.CancelFunc) uint64 {
if cancel == nil {
return 0
}
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
streamID := r.nextID.Add(1)
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
streams = map[uint64]context.CancelFunc{}
r.operators[operator] = streams
}
streams[streamID] = cancel
return streamID
}
func (r *operatorStreamRegistry) unregister(operator string, streamID uint64) {
if streamID == 0 {
return
}
operator = strings.TrimSpace(operator)
if operator == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
return
}
delete(streams, streamID)
if len(streams) == 0 {
delete(r.operators, operator)
}
}
func (r *operatorStreamRegistry) close(operator string) int {
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
r.mu.Lock()
streams := r.operators[operator]
if len(streams) == 0 {
r.mu.Unlock()
return 0
}
cancels := make([]context.CancelFunc, 0, len(streams))
for streamID, cancel := range streams {
cancels = append(cancels, cancel)
delete(streams, streamID)
}
delete(r.operators, operator)
r.mu.Unlock()
for _, cancel := range cancels {
cancel()
}
return len(cancels)
}
// CloseOperatorStreams cancels all tracked operator-owned gRPC streams.
func CloseOperatorStreams(operator string) int {
return activeOperatorStreams.close(operator)
}
func trackOperatorStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
operator, ok := ss.Context().Value(Operator).(*models.Operator)
if !ok || operator == nil || strings.TrimSpace(operator.Name) == "" {
return handler(srv, ss)
}
ctx, cancel := context.WithCancel(ss.Context())
streamID := activeOperatorStreams.register(operator.Name, cancel)
defer func() {
activeOperatorStreams.unregister(operator.Name, streamID)
cancel()
}()
return handler(srv, &trackedServerStream{
ServerStream: ss,
ctx: ctx,
})
}
}