Compare commits

...
43 Commits
Author SHA1 Message Date
moloch-- d95da61556 Improve go linker flag handling 2026-09-04 06:57:44 -07:00
moloch-- 5d5661947c Update Garble v1.27.1 checksums 2026-09-03 17:13:50 -07:00
moloch-- aad8ca5d4e Merge branch 'master' into obf-cfo
# Conflicts:
#	server/db/models/beacon.go
#	server/db/models/implant.go
#	server/gogo/go.go
#	server/handlers/beacons.go
2026-09-03 16:53:58 -07:00
moloch-- 84bb2397c7 Merge pull request #2357 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/browserslist-4.28.8 2026-09-03 16:26:41 -07:00
moloch-- 503d040885 Merge pull request #2355 from BishopFox/dependabot/go_modules/github.com/alecthomas/chroma/v2-2.27.0 2026-09-03 16:26:37 -07:00
moloch-- 801452eeff Merge pull request #2341 from BishopFox/dependabot/go_modules/google.golang.org/protobuf-1.36.12 2026-09-03 16:26:03 -07:00
moloch-- f6c7d73014 Merge pull request #2343 from BishopFox/dependabot/go_modules/golang.org/x/crypto-0.55.0 2026-09-03 16:25:12 -07:00
moloch-- 228757f656 Merge pull request #2358 from BishopFox/dependabot/go_modules/google.golang.org/grpc-1.83.1 2026-09-03 16:24:09 -07:00
dependabot[bot] 738010e990 build(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.1 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.1)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.83.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-03 22:59:35 +00:00
dependabot[bot] 9dcf9b2d25 build(deps): bump golang.org/x/crypto from 0.53.0 to 0.55.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.53.0 to 0.55.0.
- [Commits](https://github.com/golang/crypto/compare/v0.53.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-03 22:59:31 +00:00
dependabot[bot] 32ab020dbd build(deps): bump google.golang.org/protobuf from 1.36.11 to 1.36.12
Bumps google.golang.org/protobuf from 1.36.11 to 1.36.12.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-03 22:59:17 +00:00
Joe 6fcaf970b1 Merge pull request #2346 from BishopFox/feat/go-1.27-server-client-build
Go v1.27
2026-09-03 15:56:40 -07:00
moloch-- 47c83bbaa0 Adjust width of session/beacons cmd output depending on terminal width 2026-09-03 15:56:05 -07:00
moloch-- 7d76febbc9 test: synchronize reverse port forward cleanup 2026-09-03 14:04:02 -07:00
moloch-- f4c182427f refactor: adopt Go 1.27 implant APIs 2026-09-03 13:41:11 -07:00
moloch-- 836e33ade7 Merge remote-tracking branch 'origin/master' into feat/go-1.27-server-client-build
# Conflicts:
#	test/e2e/Dockerfile.linux-386
#	test/reflektor/Dockerfile.linux-386
2026-09-03 11:03:51 -07:00
Joe 0aa7e5bf96 Merge pull request #2360 from BishopFox/feat/remove-386-e2e
ci: remove 386 targets from E2E matrices
2026-09-03 08:58:13 -07:00
moloch-- b46df4b05e ci: remove 386 targets from E2E matrices 2026-09-03 07:22:30 -07:00
Joe cbc9200649 Merge pull request #2359 from BishopFox/feat/portfwd-socks5-e2e
feat: harden portfwd and SOCKS5 tunnels
2026-09-02 22:09:53 -07:00
moloch-- a0bf009c06 refactor tunnel lifecycle bookkeeping 2026-09-02 19:00:28 -07:00
moloch-- 46f516fa3a feat: harden portfwd and SOCKS5 tunnels
Add negotiated bounded flow control and exact-generation tunnel lifecycle handling.

Preserve legacy implant compatibility, serialize HTTP tunnel responses, and add manual local/Proxmox E2E plus fuzz coverage for real traffic and RDP.
2026-09-02 15:09:29 -07:00
moloch-- 1f82c38e11 Fix Go 1.27 Linux shellcode generation 2026-09-02 11:51:54 -07:00
moloch-- 6ac84ffc11 Merge remote-tracking branch 'origin/master' into feat/go-1.27-server-client-build
# Conflicts:
#	client/command/shell/ioloop.go
#	server/handlers/pivot.go
#	server/rpc/rpc-extensions_test.go
2026-09-02 10:20:06 -07:00
moloch-- 2638b5f384 Update Go and Garble to 1.27.1 2026-09-02 09:54:14 -07:00
dependabot[bot] cf19741b9d chore(deps-dev): bump browserslist in /docs/sliver-docs
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.2 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-01 23:23:11 +00:00
Joe b2c0979b63 Merge pull request #2356 from BishopFox/feat/rportfwd-authorization
Rework reverse port forwarding lifecycle and E2E coverage
2026-09-01 16:21:51 -07:00
moloch-- ea5e1412b2 fix: pass cross-platform lint checks 2026-09-01 13:19:51 -07:00
moloch-- bd948b8ab1 test: preserve pinned OPFOR corpus bytes 2026-09-01 11:01:04 -07:00
moloch-- 937a74d597 test: make e2e process cleanup idempotent 2026-09-01 09:20:24 -07:00
moloch-- e747187dda ci: fix reverse port forward workflow context 2026-09-01 09:05:15 -07:00
moloch-- ec992dea76 fix: complete reverse port forward hardening 2026-09-01 09:03:30 -07:00
moloch-- 544c872881 test: integrate reverse port forwarding into e2e 2026-08-31 12:40:48 -07:00
moloch-- 4701b895b7 fix: harden reverse port forward authorization 2026-08-31 12:22:36 -07:00
Joe 18c56ec3c5 Merge pull request #2353 from BishopFox/fix/shell-win
Improvements to windows/shell handling
2026-08-31 09:57:48 -07:00
dependabot[bot] a8c6d5e0a3 chore(deps): bump github.com/alecthomas/chroma/v2 from 2.23.1 to 2.27.0
Bumps [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) from 2.23.1 to 2.27.0.
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.23.1...v2.27.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-31 03:03:42 +00:00
moloch-- f447a170c6 Close UUID test database on Windows 2026-08-26 14:00:49 -07:00
moloch-- d565592773 Fix Go 1.27 CI validation 2026-08-26 13:37:58 -07:00
moloch-- c21da4dceb Modernize host code for Go 1.27 2026-08-26 13:13:50 -07:00
moloch-- b08e2ff5c0 Merge branch 'master' into feat/go-1.27-server-client-build 2026-08-26 12:32:17 -07:00
Ronan Kervella 55b0f2bce3 Merge branch 'master' into obf-cfo 2026-08-25 10:14:11 -07:00
moloch-- c481a4f5db Tweaks for go v1.27 2026-08-24 17:31:01 -07:00
moloch-- 73a567550d Fix control-flow CI validation
Handle newly introduced lint findings, make Windows default builds produce executable names, and verify pinned Garble extraction before the native control-flow probe.
2026-08-18 13:13:35 -07:00
moloch-- 9be2cd8b34 Add versioned Garble control-flow obfuscation
Carry the balanced-v1 policy through clients, profiles, builders, and persisted build snapshots. Verify pinned Garble assets, fail closed on unsupported runtimes, and cover local/external build lifecycle and native control-flow behavior.
2026-08-18 09:39:51 -07:00
667 changed files with 83307 additions and 13025 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
# Preserve pinned upstream source and attribution files byte-for-byte.
client/command/opfor/testdata/corpus/** -text
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
+2 -2
View File
@@ -60,10 +60,10 @@ jobs:
with:
fetch-depth: 0
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
steps:
# Setup go environment
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Checkout repository
uses: actions/checkout@v6
+200 -105
View File
@@ -2,11 +2,29 @@ name: Comprehensive e2e Tests
on:
workflow_dispatch:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: true
default: comprehensive
type: choice
options:
- comprehensive
- rportfwd
workflow_call:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: false
default: comprehensive
type: string
permissions:
contents: read
env:
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
concurrency:
group: comprehensive-e2e-${{ github.ref }}
cancel-in-progress: false
@@ -21,8 +39,16 @@ jobs:
uses: actions/github-script@v9
env:
REQUESTING_ACTOR: ${{ github.triggering_actor }}
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
with:
script: |
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
return;
}
core.info(`Validated E2E test scope ${testScope}`);
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
if (isReleaseTag) {
core.info(`Authorized release test invocation for ${context.ref}`);
@@ -40,6 +66,38 @@ jobs:
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
}
rportfwd-security-regression:
name: Reverse-port-forward malicious protocol regression
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Test malicious destination poisoning and cross-session isolation
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: >-
go test -buildvcs=false -mod=vendor
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
./server/c2
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
-count=1 -v
darwin:
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
needs: authorize
@@ -96,21 +154,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os darwin \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os darwin
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -178,21 +246,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os linux \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os linux
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -204,71 +282,6 @@ jobs:
overwrite: true
retention-days: 14
linux-386:
name: comprehensive-e2e-linux-386
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 360
env:
CONTAINER_NAME: sliver-comprehensive-e2e-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-comprehensive-e2e-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'linux/386\n' > e2e-results/target-linux-386.txt
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 E2E image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/e2e/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
- name: Run comprehensive E2E suite
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
--volume "${PWD}/e2e-results:/results"
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-linux-386
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
windows:
name: comprehensive-e2e-windows-${{ matrix.goarch }}
needs: authorize
@@ -278,9 +291,6 @@ jobs:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-2022
goarch: amd64
server_arch: amd64
@@ -328,21 +338,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e.exe ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e.exe \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server.exe \
-server-arch ${{ matrix.server_arch }} \
-target-os windows \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e.exe
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server.exe
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os windows
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -354,15 +374,88 @@ jobs:
overwrite: true
retention-days: 14
aggregate-rportfwd:
name: Aggregate reverse-port-forward E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download target coverage
id: download
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: comprehensive-e2e-target-*
path: rportfwd-coverage-input
- name: Prepare aggregate artifact
if: ${{ always() }}
run: |
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
- name: Aggregate coverage and enforce all 162 required cells
id: aggregate
if: ${{ always() }}
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input rportfwd-coverage-input
-output rportfwd-coverage-summary
- name: Publish reverse-port-forward coverage table
if: ${{ always() }}
run: |
report="rportfwd-coverage-summary/rportfwd-coverage.md"
if [[ -s "${report}" ]]; then
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
else
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
fi
- name: Upload aggregate reverse-port-forward coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-rportfwd-coverage-summary
path: rportfwd-coverage-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
aggregate-coverage:
name: Aggregate comprehensive E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- linux-386
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
outputs:
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
runs-on: ubuntu-24.04
@@ -441,16 +534,18 @@ jobs:
overwrite: true
retention-days: 30
- name: Fail on failed or not-run coverage
if: ${{ steps.aggregate.outcome != 'success' }}
- name: Fail on failed prerequisites or failed or not-run coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
reflektor:
name: Existing Reflektor test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/reflektor-tests.yml
shellcode:
name: Shellcode E2E test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/shellcode-tests.yml
+3 -3
View File
@@ -43,14 +43,14 @@ jobs:
with:
fetch-depth: 0
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
version: v2.13.1
# Gate only findings introduced since the event base revision.
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
-50
View File
@@ -111,53 +111,6 @@ jobs:
-target-os linux
-target-arch ${{ matrix.goarch }}
linux-386:
name: reflektor-linux-386-docker
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
CONTAINER_NAME: sliver-reflektor-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-reflektor-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 test image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/reflektor/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
docker buildx prune --all --force
- name: Load Sliver with Reflektor
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
windows:
name: reflektor-windows-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
@@ -169,9 +122,6 @@ jobs:
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: amd64
-7
View File
@@ -40,13 +40,6 @@ jobs:
server_arch: arm64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: windows-2022
target_os: windows
target_arch: "386"
server_os: windows
server_arch: amd64
server_executable: ./sliver-server.exe
test_executable: ./sliver-shellcode-e2e.exe
- runner: windows-2022
target_os: windows
target_arch: amd64
+10 -8
View File
@@ -6,6 +6,8 @@ on:
paths:
- "go.mod"
- "go.sum"
- ".gitattributes"
- "client/command/opfor/testdata/corpus/**"
- ".github/workflows/unit-tests.yml"
- "Makefile"
- "**/*.go"
@@ -25,10 +27,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: OS Packages
run: |
@@ -71,10 +73,10 @@ jobs:
runs-on: windows-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Install make
run: choco install make -y --no-progress
@@ -115,10 +117,10 @@ jobs:
runs-on: macos-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Check Out Code
uses: actions/checkout@v6
@@ -153,10 +155,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.26.6
- name: Go 1.27.1
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.27.1"
- name: Check Out Code
uses: actions/checkout@v6
+23 -15
View File
@@ -26,7 +26,7 @@ ifneq (,$(findstring cgo_sqlite,$(TAGS)))
endif
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 26
MIN_SUPPORTED_GO_MINOR_VERSION = 27
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
@@ -35,6 +35,7 @@ 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
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ASSET_SOURCES = $(filter-out %_test.go,$(wildcard util/assets/*.go util/cmd/assets/*.go))
.PHONY: lint
lint:
@@ -170,8 +171,8 @@ pb:
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
.PHONY: debug
debug: clean .downloaded_assets validate-go-version
$(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
@@ -199,9 +200,9 @@ clean-all: 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
.downloaded_assets: $(ASSET_SOURCES)
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
#
@@ -221,6 +222,13 @@ LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLI
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
COMMA := ,
DEFAULT_EXECUTABLE_EXT := .exe
ifneq ($(strip $(GOOS)),)
ifneq ($(strip $(GOOS)),windows)
DEFAULT_EXECUTABLE_EXT :=
endif
endif
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)"
@@ -235,10 +243,10 @@ $(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build
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))
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)$(DEFAULT_EXECUTABLE_EXT))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX)$(DEFAULT_EXECUTABLE_EXT))
.PHONY: client
client: clean .downloaded_assets validate-go-version
@@ -299,8 +307,8 @@ pb: validate-pb-compilers
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
.PHONY: debug
debug: clean .downloaded_assets validate-go-version
$(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))
@@ -324,8 +332,8 @@ clean-all: 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
.downloaded_assets: $(ASSET_SOURCES)
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
endif
+13
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"strings"
"testing"
"testing/synctest"
"time"
"github.com/bishopfox/sliver/client/transport"
@@ -52,6 +53,10 @@ func TestFormatConnectSpinnerMessage(t *testing.T) {
}
func TestConnectWithSpinnerOutput(t *testing.T) {
synctest.Test(t, testConnectWithSpinnerOutput)
}
func testConnectWithSpinnerOutput(t *testing.T) {
var out bytes.Buffer
spinnerDelay := 2 * (100 * time.Millisecond)
@@ -85,6 +90,10 @@ func TestConnectWithSpinnerOutput(t *testing.T) {
}
func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
synctest.Test(t, testConnectWithSpinnerFastSuccessStillShowsMultipleFrames)
}
func testConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
@@ -108,6 +117,10 @@ func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
}
func TestConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
synctest.Test(t, testConnectWithSpinnerFastSuccessShowsEachStatus)
}
func testConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
+5 -20
View File
@@ -487,10 +487,7 @@ func (m *aiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if conversationContainsUserPrompt(m.currentConversation, m.pendingPrompt) {
m.pendingPrompt = ""
}
m.selectedConversation = conversationIndexByID(m.conversations, selectedConversationID(msg.conversation, msg.selectedID))
if m.selectedConversation < 0 {
m.selectedConversation = 0
}
m.selectedConversation = max(conversationIndexByID(m.conversations, selectedConversationID(msg.conversation, msg.selectedID)), 0)
m.invalidateTranscriptCache()
if strings.TrimSpace(msg.status) != "" {
m.status = msg.status
@@ -3273,10 +3270,7 @@ func (m *aiModel) removeConversation(conversationID string) {
}
m.conversations = filtered
next := conversationIndexByID(m.conversations, m.selectedConversationID())
if next < 0 {
next = 0
}
next := max(conversationIndexByID(m.conversations, m.selectedConversationID()), 0)
m.selectedConversation = next
}
@@ -3683,10 +3677,7 @@ func (m *aiModel) applyConversationEvent(event *clientpb.AIConversationEvent) {
}
if m.currentConversation != nil {
m.selectedConversation = conversationIndexByID(m.conversations, m.currentConversation.GetID())
if m.selectedConversation < 0 {
m.selectedConversation = 0
}
m.selectedConversation = max(conversationIndexByID(m.conversations, m.currentConversation.GetID()), 0)
}
m.invalidateTranscriptCache()
}
@@ -5579,10 +5570,7 @@ func truncateToolCallContentLines(content string, maxLines int) (string, int) {
}
truncated := strings.Join(lines[:maxLines], "\n")
hiddenBytes := len(content) - len(truncated)
if hiddenBytes < 0 {
hiddenBytes = 0
}
hiddenBytes := max(len(content)-len(truncated), 0)
return truncated, hiddenBytes
}
@@ -5828,10 +5816,7 @@ func inputWindow(input []rune, cursor, width int) ([]rune, int) {
start := 0
if len(input) > width {
start = cursor - width + 1
if start < 0 {
start = 0
}
start = max(cursor-width+1, 0)
if start > len(input)-width {
start = len(input) - width
}
+2 -2
View File
@@ -22,6 +22,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/url"
"os"
"path/filepath"
@@ -37,7 +38,6 @@ import (
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/minisign"
)
@@ -348,7 +348,7 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
func getPackageIDFromUser(name string, options map[string]string) string {
selectedPackageKey := ""
optionKeys := util.Keys(options)
optionKeys := slices.Collect(maps.Keys(options))
slices.Sort(optionKeys)
// Add a cancel option
optionKeys = append(optionKeys, doNotInstallOption)
+4 -3
View File
@@ -21,7 +21,9 @@ package armory
import (
"errors"
"fmt"
"maps"
"os"
"slices"
"sort"
"github.com/jedib0t/go-pretty/v6/table"
@@ -33,7 +35,6 @@ import (
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/util"
)
type VersionInformation struct {
@@ -244,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
result := []UpdateIdentifier{}
aliasNames := util.Keys(aliasUpdates)
extensionNames := util.Keys(extensionUpdates)
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
for _, name := range aliasNames {
result = append(result, UpdateIdentifier{
Type: AliasPackage,
+143 -87
View File
@@ -30,6 +30,7 @@ import (
"github.com/bishopfox/sliver/client/command/kill"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
@@ -121,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
if err != nil {
width = 999
}
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
wideTermWidth := con.Settings.SmallTermWidth < width
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
// preventing a second boundary from changing the table width while it is fitted.
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
windowsBeaconInList := false
for _, beacon := range beacons {
if beacon.OS == "windows" {
windowsBeaconInList = true
break
}
}
if wideTermWidth {
if windowsBeaconInList {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Integrity",
"Operating System",
"Locale",
"Last Check-in",
"Next Check-in",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Operating System",
"Locale",
"Last Check-in",
"Next Check-in",
})
}
} else {
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
})
}
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
if layout.Compact {
tw.AppendHeader(table.Row{
"ID",
"Name",
@@ -175,22 +156,84 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
"Last Check-in",
"Next Check-in",
})
} else {
header := table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
}
if includeIntegrity {
header = append(header, "Integrity")
}
header = append(header, "Operating System")
if layout.IncludeLocale {
header = append(header, "Locale")
}
header = append(header, "Last Check-in", "Next Check-in")
tw.AppendHeader(header)
}
activeBeaconID := ""
if con.ActiveTarget != nil {
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
activeBeaconID = activeBeacon.ID
}
}
for _, beacon := range beacons {
integrity := beacon.Integrity
if integrity == "" {
integrity = "-"
}
username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")
lastCheckin := time.Unix(beacon.LastCheckin, 0)
nextCheckin := time.Unix(beacon.NextCheckin, 0)
canonicalEntries := []string{
strings.Split(beacon.ID, "-")[0],
beacon.Name,
fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount),
beacon.Transport,
beacon.RemoteAddress,
beacon.Hostname,
username,
fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID),
integrity,
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
beacon.Locale,
formatBeaconDateDelta(lastCheckin, now, true, false),
formatBeaconDateDelta(nextCheckin, now, true, false),
}
if !beaconMatchesFilter(canonicalEntries, filter, filterRegex) {
continue
}
style := console.StyleNormal
activeBeacon := con.ActiveTarget.GetBeacon()
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
if activeBeaconID == beacon.ID {
style = console.StyleGreen
}
if beacon.Integrity == "" {
beacon.Integrity = "-"
}
// We need a slice of strings so we can apply filters
var rowEntries []string
if wideTermWidth {
if layout.Compact {
rowEntries = []string{
style.Render(strings.Split(beacon.ID, "-")[0]),
style.Render(beacon.Name),
style.Render(beacon.Transport),
style.Render(beacon.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
formatBeaconDateDelta(lastCheckin, now, false, false),
formatBeaconDateDelta(nextCheckin, now, false, true),
}
} else {
processName := beacon.Filename
if layout.ProcessBasename {
processName = targettable.ProcessName(processName)
}
rowEntries = []string{
style.Render(strings.Split(beacon.ID, "-")[0]),
style.Render(beacon.Name),
@@ -198,56 +241,69 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
style.Render(beacon.Transport),
style.Render(beacon.RemoteAddress),
style.Render(beacon.Hostname),
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
style.Render(username),
style.Render(fmt.Sprintf("%s (%d)", processName, beacon.PID)),
}
if windowsBeaconInList {
rowEntries = append(rowEntries, style.Render(beacon.Integrity))
if includeIntegrity {
rowEntries = append(rowEntries, style.Render(integrity))
}
rowEntries = append(rowEntries, []string{
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
style.Render(beacon.Locale),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
}...)
} else {
rowEntries = []string{
style.Render(strings.Split(beacon.ID, "-")[0]),
style.Render(beacon.Name),
style.Render(beacon.Transport),
style.Render(beacon.Hostname),
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)))
if layout.IncludeLocale {
rowEntries = append(rowEntries, style.Render(beacon.Locale))
}
rowEntries = append(rowEntries,
formatBeaconDateDelta(lastCheckin, now, true, false),
formatBeaconDateDelta(nextCheckin, now, true, true),
)
}
// Build the row struct
row := table.Row{}
for _, entry := range rowEntries {
row = append(row, entry)
}
// Apply filters if any
if filter == "" && filterRegex == nil {
tw.AppendRow(row)
} else {
for _, rowEntry := range rowEntries {
if filter != "" {
if strings.Contains(rowEntry, filter) {
tw.AppendRow(row)
break
}
}
if filterRegex != nil {
if filterRegex.MatchString(rowEntry) {
tw.AppendRow(row)
break
}
}
}
}
tw.AppendRow(row)
}
return tw
}
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
if filter == "" && filterRegex == nil {
return true
}
for _, entry := range entries {
if filter != "" && strings.Contains(entry, filter) {
return true
}
if filterRegex != nil && filterRegex.MatchString(entry) {
return true
}
}
return false
}
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
date := timestamp.Format(time.UnixDate)
interval := ""
if timestamp.Before(now) {
delta := now.Sub(timestamp).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (%s ago)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldRed.Render(interval)
}
} else {
delta := timestamp.Sub(now).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (in %s)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldGreen.Render(interval)
}
}
return interval
}
+257
View File
@@ -0,0 +1,257 @@
package beacons
import (
"regexp"
"strings"
"testing"
"time"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
beacon := testWindowsBeacon()
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("full process path missing from table:\n%s", rendered)
}
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
tests := []struct {
name string
beacon *clientpb.Beacon
hasIntegrity bool
wantProcess string
}{
{
name: "windows path",
beacon: testWindowsBeacon(),
hasIntegrity: true,
wantProcess: "winterfell.exe (2020)",
},
{
name: "posix path",
beacon: testLinuxBeacon(),
hasIntegrity: false,
wantProcess: "winterfell-agent (2020)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
beacons := []*clientpb.Beacon{tt.beacon}
fullLayout := targettable.Layout{
IncludeIntegrity: tt.hasIntegrity,
IncludeLocale: true,
}
basenameLayout := fullLayout
basenameLayout.ProcessBasename = true
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
if basenameWidth >= fullWidth {
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
if !strings.Contains(rendered, tt.wantProcess) {
t.Fatalf("process basename missing from table:\n%s", rendered)
}
if strings.Contains(rendered, tt.beacon.Filename) {
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, basenameWidth)
})
}
}
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
withAll := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
withoutIntegrity := withAll
withoutIntegrity.IncludeIntegrity = false
withoutLocale := withoutIntegrity
withoutLocale.IncludeLocale = false
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
}
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
}
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
beacon := testLinuxBeacon()
beacons := []*clientpb.Beacon{beacon}
layout := targettable.Layout{IncludeLocale: true}
width := beaconCandidateWidth(t, beacons, layout, false)
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
assertHeaderPresence(t, rendered, "Integrity", false)
assertHeaderPresence(t, rendered, "Locale", true)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
basenameLayout := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
withoutLocale := basenameLayout
withoutLocale.IncludeIntegrity = false
withoutLocale.IncludeLocale = false
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
t.Run("full process path survives basename projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
}
if strings.Contains(rendered, `AppData\Local\Temp`) {
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
}
})
t.Run("hidden integrity survives column projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
}
assertHeaderPresence(t, rendered, "Integrity", false)
})
}
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
beacon := testWindowsBeacon()
beacon.Integrity = ""
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if beacon.Integrity != "" {
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
}
}
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
if compactWidth >= withoutOptionalWidth {
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
assertHeaderPresence(t, rendered, omitted, false)
}
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
}
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, compactWidth)
}
func testWindowsBeacon() *clientpb.Beacon {
return &clientpb.Beacon{
ID: "0c9f6b27-1111-2222-3333-444444444444",
Name: "winterfell-session",
TasksCount: 3,
TasksCountCompleted: 2,
Transport: "mtls",
RemoteAddress: "10.13.37.11:49760",
Hostname: "winterfell",
Username: `winterfell\vagrant`,
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
PID: 2020,
Integrity: "High",
OS: "windows",
Arch: "amd64",
Locale: "en-US",
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
}
}
func testLinuxBeacon() *clientpb.Beacon {
beacon := testWindowsBeacon()
beacon.OS = "linux"
beacon.Hostname = "winterfell-linux"
beacon.Username = "vagrant"
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
beacon.Integrity = ""
return beacon
}
func testBeaconConsole() *console.SliverClient {
return &console.SliverClient{
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
}
}
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
t.Helper()
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
return text.LongestLineLen(tw.Render())
}
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
t.Helper()
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
return text.StripEscape(tw.Render())
}
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
t.Helper()
if got := text.LongestLineLen(rendered); got > width {
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
}
}
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
t.Helper()
headerLine := strings.SplitN(rendered, "\n", 2)[0]
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
}
}
+2 -2
View File
@@ -145,8 +145,8 @@ func filterCompletionValues(values []string, prefix string) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
candidate = value[:tab]
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
+2 -1
View File
@@ -49,7 +49,7 @@ func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverClie
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Name", "Operator", "Templates", "Platform", "Compiler Targets",
"Name", "Operator", "Templates", "Platform", "Capabilities", "Compiler Targets",
})
for _, builder := range externalBuilders {
@@ -63,6 +63,7 @@ func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverClie
builder.OperatorName,
strings.Join(builder.Templates, ", "),
fmt.Sprintf("%s/%s", builder.GOOS, builder.GOARCH),
strings.Join(builder.Capabilities, ", "),
strings.Join(targets, "\n"),
}
tw.AppendRow(table.Row(row))
+1 -1
View File
@@ -28,11 +28,11 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/settings"
+2 -2
View File
@@ -84,8 +84,8 @@ func filterCompletionValues(values []string, prefix string) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
candidate = value[:tab]
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
+9 -1
View File
@@ -228,6 +228,14 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
return nil, errors.Join(
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
terminateStartedCursedProcess(con, session.ID, chromeExec.GetPid()),
)
}
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
curse := &core.CursedProcess{
@@ -241,7 +249,7 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
}
core.CursedProcesses.Store(bindPort, curse)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
+1 -1
View File
@@ -80,7 +80,7 @@ func selectDebugTarget(targets []overlord.ChromeDebugTarget, con *console.Sliver
if selected == "" {
return nil
}
selectedID := strings.Split(selected, " ")[0]
selectedID, _, _ := strings.Cut(selected, " ")
selectedTarget := id2target[selectedID]
return &selectedTarget
}
+10 -1
View File
@@ -20,6 +20,7 @@ package cursed
import (
"context"
"errors"
"fmt"
"log"
"path"
@@ -190,6 +191,14 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
return nil, errors.Join(
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
terminateStartedCursedProcess(con, session.ID, electronExec.GetPid()),
)
}
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
curse := &core.CursedProcess{
@@ -202,7 +211,7 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
}
core.CursedProcesses.Store(bindPort, curse)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
+27
View File
@@ -20,20 +20,47 @@ package cursed
import (
"bytes"
"context"
"errors"
"fmt"
"strconv"
"strings"
"text/tabwriter"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
const cursedStartCleanupTimeout = 30 * time.Second
func terminateStartedCursedProcess(con *console.SliverClient, sessionID string, pid uint32) error {
const maxTerminatePID = uint32(1<<31 - 1)
if con == nil || con.Rpc == nil || sessionID == "" || pid == 0 || pid > maxTerminatePID {
return errors.New("cannot terminate incompletely tracked cursed process")
}
ctx, cancel := context.WithTimeout(context.Background(), cursedStartCleanupTimeout)
defer cancel()
response, err := con.Rpc.Terminate(ctx, &sliverpb.TerminateReq{
Request: &commonpb.Request{SessionID: sessionID},
Pid: int32(pid),
})
if err != nil {
return fmt.Errorf("terminate started cursed process %d: %w", pid, err)
}
if response.GetResponse().GetErr() != "" {
return fmt.Errorf("terminate started cursed process %d: %s", pid, response.GetResponse().GetErr())
}
return nil
}
// CursedChromeCmd - Execute a .NET assembly in-memory.
func CursedCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// Collect existing curses from core
+5 -21
View File
@@ -396,10 +396,7 @@ func (m *editorModel) renderLine(index int) string {
return ""
}
prefixWidth := m.lineNumberWidth()
contentWidth := totalWidth - prefixWidth
if contentWidth < 0 {
contentWidth = 0
}
contentWidth := max(totalWidth-prefixWidth, 0)
prefix := m.linePrefix(index, prefixWidth)
if index >= len(m.lines) {
@@ -508,10 +505,7 @@ func (m *editorModel) lineNumberWidth() int {
if !m.showLineNumbers {
return 0
}
total := len(m.lines)
if total < 1 {
total = 1
}
total := max(len(m.lines), 1)
return len(fmt.Sprintf("%d", total)) + 1
}
@@ -559,10 +553,7 @@ func (m *editorModel) ensureCursorVisible() {
m.top = 0
}
width := m.textWidth() - m.lineNumberWidth()
if width < 1 {
width = 1
}
width := max(m.textWidth()-m.lineNumberWidth(), 1)
if m.col < m.left {
m.left = m.col
} else if m.col >= m.left+width {
@@ -1125,20 +1116,13 @@ func lastIndexOfRunes(haystack, needle []rune, end int) int {
return -1
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func baseName(path string) string {
if path == "" {
return ""
}
clean := strings.ReplaceAll(path, "\\", "/")
if idx := strings.LastIndex(clean, "/"); idx != -1 {
return clean[idx+1:]
if _, after, ok := strings.CutLast(clean, "/"); ok {
return after
}
return path
}
+5 -3
View File
@@ -234,10 +234,12 @@ func parseEnvPairs(envPairs []string) (map[string]string, error) {
func determineCommandName(command string) string {
commandName := strings.ReplaceAll(command, "\\", "/")
commandName = commandName[strings.LastIndex(commandName, "/")+1:]
if _, after, ok := strings.CutLast(commandName, "/"); ok {
commandName = after
}
if strings.Contains(commandName, ".") {
commandName = commandName[:strings.LastIndex(commandName, ".")]
if before, _, ok := strings.CutLast(commandName, "."); ok {
commandName = before
}
return commandName
+28
View File
@@ -59,6 +59,19 @@ func MigrateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
config = con.GetActiveBeaconConfig()
implantName = beacon.Name
}
if config == nil {
con.PrintErrorf("Failed to derive active implant config.\n")
return
}
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("Failed to retrieve originating implant build %q: %v\n", implantName, err)
return
}
if !applyMigrateObfuscationPolicy(config, implantName, builds) {
con.PrintWarnf("Originating implant build %q was not found; continuing with reconstructed legacy obfuscation settings.\n", implantName)
}
targetArch := config.GetGOARCH()
if session != nil && session.Arch != "" {
@@ -134,6 +147,21 @@ func MigrateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
}
func applyMigrateObfuscationPolicy(config *clientpb.ImplantConfig, implantName string, builds *clientpb.ImplantBuilds) bool {
if config == nil || builds == nil {
return false
}
originatingConfig, ok := builds.GetConfigs()[implantName]
if !ok || originatingConfig == nil {
return false
}
config.Debug = originatingConfig.Debug
config.ObfuscateSymbols = originatingConfig.ObfuscateSymbols
config.ControlFlow = originatingConfig.ControlFlow
return true
}
func normalizeShellcodeEncoderName(name string) string {
normalized := strings.ToLower(strings.TrimSpace(name))
normalized = strings.ReplaceAll(normalized, "-", "_")
@@ -0,0 +1,53 @@
package exec
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestApplyMigrateObfuscationPolicy(t *testing.T) {
config := &clientpb.ImplantConfig{
Debug: true,
ObfuscateSymbols: false,
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED,
}
builds := &clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{
"source-build": {
Debug: false,
ObfuscateSymbols: true,
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
},
}}
if !applyMigrateObfuscationPolicy(config, "source-build", builds) {
t.Fatal("applyMigrateObfuscationPolicy() = false, want true")
}
if config.Debug {
t.Fatal("Debug = true, want originating build value false")
}
if !config.ObfuscateSymbols {
t.Fatal("ObfuscateSymbols = false, want originating build value true")
}
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
t.Fatalf("ControlFlow = %s, want %s", config.ControlFlow, clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1)
}
}
func TestApplyMigrateObfuscationPolicyMissingBuildPreservesLegacyConfig(t *testing.T) {
config := &clientpb.ImplantConfig{
Debug: true,
ObfuscateSymbols: false,
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED,
}
if applyMigrateObfuscationPolicy(
config,
"missing-build",
&clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{}},
) {
t.Fatal("applyMigrateObfuscationPolicy() = true for missing build, want false")
}
if !config.Debug || config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
t.Fatalf("missing build changed legacy config: %+v", config)
}
}
+5 -1
View File
@@ -90,7 +90,11 @@ func PsExecCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.PrintErrorf("No profile found for name %s\n", profile)
return
}
serviceBinary, _ = generate.GetSliverBinary(implantProfile, con)
serviceBinary, err = generate.GetSliverBinary(implantProfile, con)
if err != nil {
con.PrintErrorf("Failed to generate service binary: %s\n", err)
return
}
} else {
// use a custom exe instead of generating a new Sliver
fileBytes, err := os.ReadFile(customExe)
+2 -3
View File
@@ -21,6 +21,7 @@ package extensions
import (
"encoding/json"
"fmt"
"maps"
"os"
"path/filepath"
"strings"
@@ -129,9 +130,7 @@ func getInstalledManifests() map[string]*ExtensionManifest {
// Name field.
func getTemporarilyLoadedManifests() map[string]*ExtensionManifest {
tempManifests := map[string]*ExtensionManifest{}
for name, manifest := range loadedManifests {
tempManifests[name] = manifest
}
maps.Copy(tempManifests, loadedManifests)
return tempManifests
}
+1
View File
@@ -326,6 +326,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.StringP("debug-file", "O", "", "path to debug output")
f.BoolP("evasion", "e", false, "enable evasion features (e.g. overwrite user space hooks)")
f.BoolP("skip-symbols", "l", false, "skip symbol obfuscation")
f.String("control-flow", "off", "control-flow obfuscation policy (off, balanced-v1)")
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
f.StringP("exports", "v", "StartW,VoidFunc,DllInstall,DllRegisterServer,DllUnregisterServer", "comma separated list of exports to include in the binary")
f.StringP("canary", "c", "", "canary domain(s)")
@@ -0,0 +1,87 @@
package generate
import (
"testing"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestParseControlFlowPolicy(t *testing.T) {
tests := []struct {
name string
value string
want clientpb.ControlFlowPolicy
wantErr bool
}{
{name: "off", value: "off", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED},
{name: "balanced", value: "balanced-v1", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1},
{name: "case and whitespace", value: " BALANCED-V1 ", want: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1},
{name: "unknown", value: "aggressive", wantErr: true},
{name: "empty", value: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseControlFlowPolicy(tt.value)
if tt.wantErr {
if err == nil {
t.Fatal("expected an error")
}
return
}
if err != nil {
t.Fatalf("parseControlFlowPolicy() error = %v", err)
}
if got != tt.want {
t.Fatalf("parseControlFlowPolicy() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidateControlFlowFlags(t *testing.T) {
disabled := clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED
balanced := clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1
if err := validateControlFlowFlags(disabled, true, true); err != nil {
t.Fatalf("disabled control flow should not constrain other flags: %v", err)
}
if err := validateControlFlowFlags(balanced, false, false); err != nil {
t.Fatalf("balanced control flow with symbol obfuscation should be valid: %v", err)
}
if err := validateControlFlowFlags(balanced, true, false); err == nil {
t.Fatal("expected --debug conflict")
}
if err := validateControlFlowFlags(balanced, false, true); err == nil {
t.Fatal("expected --skip-symbols conflict")
}
}
func TestApplyGenerateBeaconFormSetsControlFlow(t *testing.T) {
generateCmd := commandByUse(Commands(&console.SliverClient{}), consts.GenerateStr)
if generateCmd == nil {
t.Fatalf("missing %q command", consts.GenerateStr)
}
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
if beaconCmd == nil {
t.Fatalf("missing %q subcommand", consts.BeaconStr)
}
err := applyGenerateBeaconForm(beaconCmd, &forms.GenerateBeaconFormResult{
OS: "darwin",
Arch: "arm64",
Format: "exe",
C2Type: "mtls",
C2Value: "127.0.0.1:31337",
ControlFlow: "balanced-v1",
})
if err != nil {
t.Fatalf("applyGenerateBeaconForm() error = %v", err)
}
if got, err := beaconCmd.Flags().GetString("control-flow"); err != nil || got != "balanced-v1" {
t.Fatalf("control-flow flag = %q, %v; want balanced-v1", got, err)
}
}
+7 -2
View File
@@ -57,9 +57,11 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
save, _ = os.Getwd()
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
compile(name, config, spoofMetadata, save, con)
_, _ = compile(name, config, spoofMetadata, save, con)
} else {
externalBuild(name, config, spoofMetadata, save, con)
if _, err := externalBuild(name, config, spoofMetadata, save, con); err != nil {
con.PrintErrorf("%s\n", err)
}
}
}
@@ -113,6 +115,9 @@ func applyGenerateBeaconForm(cmd *cobra.Command, result *forms.GenerateBeaconFor
if err := cmd.Flags().Set("format", result.Format); err != nil {
return err
}
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
return err
}
name := strings.TrimSpace(result.Name)
if name != "" {
if err := cmd.Flags().Set("name", name); err != nil {
+129 -10
View File
@@ -74,7 +74,9 @@ const (
)
const (
crossCompilerInfoURL = "https://github.com/BishopFox/sliver/wiki/Cross-Compiling-Implants"
crossCompilerInfoURL = "https://github.com/BishopFox/sliver/wiki/Cross-Compiling-Implants"
controlFlowCapability = "garble.control-flow/balanced-v1"
localControlFlowCapability = "garble.control-flow.local/balanced-v1"
)
var (
@@ -90,9 +92,14 @@ var (
"windows/arm64": true,
}
// ErrNoExternalBuilder indicates that no external builders are registered.
ErrNoExternalBuilder = errors.New("no external builders are available")
ErrNoValidBuilders = errors.New("no valid external builders for target")
ErrNoSelection = errors.New("no selection")
// ErrNoValidBuilders indicates that no external builder supports the target.
ErrNoValidBuilders = errors.New("no valid external builders for target")
// ErrNoControlFlowBuilder indicates that no target-compatible builder supports control-flow obfuscation.
ErrNoControlFlowBuilder = errors.New("no target-compatible external builder supports control-flow obfuscation")
// ErrNoSelection indicates that the user did not select an external builder.
ErrNoSelection = errors.New("no selection")
)
const (
@@ -100,6 +107,38 @@ const (
imageFileDLLCharacteristic = 0x2000
)
func parseControlFlowPolicy(value string) (clientpb.ControlFlowPolicy, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "off":
return clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED, nil
case "balanced-v1":
return clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1, nil
default:
return clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED, fmt.Errorf("invalid control-flow policy %q (expected off or balanced-v1)", value)
}
}
func validateControlFlowFlags(policy clientpb.ControlFlowPolicy, debug, skipSymbols bool) error {
if policy == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
return nil
}
if debug || skipSymbols {
return errors.New("--control-flow=balanced-v1 requires symbol obfuscation and cannot be combined with --debug or --skip-symbols")
}
return nil
}
func controlFlowPolicyName(policy clientpb.ControlFlowPolicy) string {
switch policy {
case clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED:
return "disabled"
case clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1:
return "balanced-v1"
default:
return fmt.Sprintf("unknown (%d)", policy)
}
}
// GenerateCmd - The main command used to generate implant binaries
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if shouldRunGenerateForm(cmd, con, args) {
@@ -172,6 +211,9 @@ func applyGenerateForm(cmd *cobra.Command, result *forms.GenerateFormResult) err
if err := cmd.Flags().Set("format", result.Format); err != nil {
return err
}
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
return err
}
name := strings.TrimSpace(result.Name)
if name != "" {
if err := cmd.Flags().Set("name", name); err != nil {
@@ -329,12 +371,18 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
}
c2s = append(c2s, tcpPivotC2...)
var symbolObfuscation bool
if debug, _ := cmd.Flags().GetBool("debug"); debug {
symbolObfuscation = false
} else {
symbolObfuscation, _ = cmd.Flags().GetBool("skip-symbols")
symbolObfuscation = !symbolObfuscation
debug, _ := cmd.Flags().GetBool("debug")
skipSymbols, _ := cmd.Flags().GetBool("skip-symbols")
symbolObfuscation := !debug && !skipSymbols
controlFlowValue, _ := cmd.Flags().GetString("control-flow")
controlFlow, err := parseControlFlowPolicy(controlFlowValue)
if err != nil {
con.PrintErrorf("%s\n", err)
return "", nil
}
if err := validateControlFlowFlags(controlFlow, debug, skipSymbols); err != nil {
con.PrintErrorf("%s\n", err)
return "", nil
}
if len(mtlsC2) == 0 && len(wgC2) == 0 && len(httpC2) == 0 && len(dnsC2) == 0 && len(namedPipeC2) == 0 && len(tcpPivotC2) == 0 {
@@ -353,7 +401,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
}
}
debug, _ := cmd.Flags().GetBool("debug")
evasion, _ := cmd.Flags().GetBool("evasion")
templateName, _ := cmd.Flags().GetString("template")
@@ -516,6 +563,7 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
SGNEnabled: sgnEnabled,
ShellcodeEncoder: shellcodeEncoder,
ObfuscateSymbols: symbolObfuscation,
ControlFlow: controlFlow,
C2: c2s,
CanaryDomains: canaryDomains,
TemplateName: templateName,
@@ -1643,6 +1691,10 @@ func outputSavePath(save string, defaultName string, config *clientpb.ImplantCon
}
func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
if err := ensureServerSupportsControlFlow(config, con); err != nil {
return nil, err
}
potentialBuilders, err := findExternalBuilders(config, con)
if err != nil {
return nil, err
@@ -1670,6 +1722,9 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
} else if !config.Debug {
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
}
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
con.PrintInfof("Control-flow obfuscation policy: %s\n", console.StyleBold.Render(controlFlowPolicyName(config.ControlFlow)))
}
start := time.Now()
listenerID, listener := con.CreateEventListener()
@@ -1690,6 +1745,14 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
con.PrintErrorf("%s\n", err)
return nil, err
}
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED &&
(externalImplantConfig == nil || externalImplantConfig.Config == nil || externalImplantConfig.Config.ControlFlow != config.ControlFlow) {
return nil, fmt.Errorf(
"server returned control-flow policy %s for requested policy %s",
controlFlowPolicyName(externalImplantConfig.GetConfig().GetControlFlow()),
controlFlowPolicyName(config.ControlFlow),
)
}
con.Printf("done\n")
msgF := "Waiting for external builder to acknowledge build (template: %s) ... %s"
@@ -1818,6 +1881,11 @@ func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *c
}
func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
if err := ensureServerCanBuildControlFlow(config, con); err != nil {
con.PrintErrorf("%s\n", err)
return nil, err
}
if config.IsBeacon {
interval := time.Duration(config.BeaconInterval)
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
@@ -1829,6 +1897,9 @@ func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientp
} else if !config.Debug {
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
}
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
con.PrintInfof("Control-flow obfuscation policy: %s\n", console.StyleBold.Render(controlFlowPolicyName(config.ControlFlow)))
}
start := time.Now()
ctrl := make(chan bool)
@@ -2009,9 +2080,14 @@ func findExternalBuilders(config *clientpb.ImplantConfig, con *console.SliverCli
}
validBuilders := []*clientpb.Builder{}
targetCompatibleBuilder := false
for _, builder := range builders.Builders {
for _, target := range builder.Targets {
if target.GOOS == config.GOOS && target.GOARCH == config.GOARCH && config.Format == target.Format {
targetCompatibleBuilder = true
if config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED && !hasCapability(builder.Capabilities, controlFlowCapability) {
break
}
validBuilders = append(validBuilders, builder)
break
}
@@ -2019,12 +2095,55 @@ func findExternalBuilders(config *clientpb.ImplantConfig, con *console.SliverCli
}
if len(validBuilders) < 1 {
if targetCompatibleBuilder && config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
return []*clientpb.Builder{}, ErrNoControlFlowBuilder
}
return []*clientpb.Builder{}, ErrNoValidBuilders
}
return validBuilders, nil
}
func ensureServerSupportsControlFlow(config *clientpb.ImplantConfig, con *console.SliverClient) error {
if config == nil || config.ControlFlow == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
return nil
}
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
if err != nil {
return fmt.Errorf("failed to query server control-flow capability: %w", err)
}
if !hasCapability(compiler.Capabilities, controlFlowCapability) {
return fmt.Errorf("server does not support %s", controlFlowCapability)
}
return nil
}
func ensureServerCanBuildControlFlow(config *clientpb.ImplantConfig, con *console.SliverClient) error {
if config == nil || config.ControlFlow == clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
return nil
}
compiler, err := con.Rpc.GetCompiler(context.Background(), &commonpb.Empty{})
if err != nil {
return fmt.Errorf("failed to query server control-flow capability: %w", err)
}
if !hasCapability(compiler.Capabilities, controlFlowCapability) {
return fmt.Errorf("server does not support %s", controlFlowCapability)
}
if !hasCapability(compiler.Capabilities, localControlFlowCapability) {
return fmt.Errorf("server local compiler does not provide %s", localControlFlowCapability)
}
return nil
}
func hasCapability(capabilities []string, required string) bool {
for _, capability := range capabilities {
if capability == required {
return true
}
}
return false
}
func selectExternalBuilder(builders []*clientpb.Builder, _ *console.SliverClient) (*clientpb.Builder, error) {
choices := []string{}
for _, builder := range builders {
+27 -2
View File
@@ -58,7 +58,7 @@ func ProfilesNewCmd(cmd *cobra.Command, con *console.SliverClient, args []string
Name: name,
Config: config,
}
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
resp, err := saveImplantProfileWithControlFlowCheck(con, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
} else {
@@ -106,7 +106,7 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
Name: name,
Config: config,
}
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
resp, err := saveImplantProfileWithControlFlowCheck(con, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
} else {
@@ -114,6 +114,25 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
}
}
func saveImplantProfileWithControlFlowCheck(con *console.SliverClient, profile *clientpb.ImplantProfile) (*clientpb.ImplantProfile, error) {
if profile == nil || profile.Config == nil {
return nil, errors.New("implant profile config cannot be nil")
}
if err := ensureServerSupportsControlFlow(profile.Config, con); err != nil {
return nil, err
}
resp, err := con.Rpc.SaveImplantProfile(context.Background(), profile)
if err != nil {
return nil, err
}
if profile.Config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED &&
(resp == nil || resp.Config == nil || resp.Config.ControlFlow != profile.Config.ControlFlow) {
return nil, errors.New("server did not preserve the requested control-flow policy")
}
return resp, nil
}
func shouldRunGenerateProfilesNewForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
if con == nil || con.IsCLI {
return false
@@ -134,6 +153,9 @@ func applyGenerateProfilesNewForm(cmd *cobra.Command, result *forms.GenerateProf
if err := cmd.Flags().Set("format", result.Format); err != nil {
return "", err
}
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
return "", err
}
profileName := strings.TrimSpace(result.ProfileName)
if profileName == "" {
@@ -200,6 +222,9 @@ func applyGenerateProfilesNewBeaconForm(cmd *cobra.Command, result *forms.Genera
if err := cmd.Flags().Set("format", result.Format); err != nil {
return "", err
}
if err := cmd.Flags().Set("control-flow", result.ControlFlow); err != nil {
return "", err
}
profileName := strings.TrimSpace(result.ProfileName)
if profileName == "" {
+7
View File
@@ -60,6 +60,7 @@ func PrintProfiles(profiles []*clientpb.ImplantProfile, con *console.SliverClien
"Debug",
"Format",
"Obfuscation",
"Control Flow",
"Limitations",
"C2 Profile",
// "Nonce",
@@ -91,6 +92,7 @@ func PrintProfiles(profiles []*clientpb.ImplantProfile, con *console.SliverClien
fmt.Sprintf("%v", config.Debug),
fmt.Sprintf("%v", config.Format),
obfuscation,
controlFlowPolicyName(config.ControlFlow),
getLimitsString(config),
config.HTTPC2ConfigName,
// profile.ImplantID,
@@ -164,6 +166,7 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
} else {
properties["obsymbols"] = "disabled"
}
properties["obcontrolflow"] = controlFlowPolicyName(config.ControlFlow)
properties["shellcodeencoder"] = "n/a"
if config.Format == clientpb.OutputFormat_SHELLCODE {
@@ -323,6 +326,10 @@ func PrintProfileInfo(name string, con *console.SliverClient) {
"Obfuscation of symbols is",
properties["obsymbols"],
})
tw.AppendRow(table.Row{
"Control-flow obfuscation is",
properties["obcontrolflow"],
})
tw.AppendRow(table.Row{
"Shellcode encoder is",
properties["shellcodeencoder"],
+4 -3
View File
@@ -22,6 +22,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"
@@ -33,12 +34,12 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/util"
"github.com/gofrs/uuid"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"google.golang.org/protobuf/proto"
uuid "uuid"
)
// TrafficEncodersCmd - Generate traffic encoders command implementation.
@@ -67,7 +68,7 @@ func DisplayTrafficEncoders(encoderMap *clientpb.TrafficEncoderMap, con *console
})
allIDs := []uint64{}
for _, encoder := range encoderMap.Encoders {
isDuplicate := util.Contains(allIDs, encoder.ID)
isDuplicate := slices.Contains(allIDs, encoder.ID)
allIDs = append(allIDs, encoder.ID)
name := encoder.Wasm.Name
if isDuplicate {
@@ -94,7 +95,7 @@ func TrafficEncodersAddCmd(cmd *cobra.Command, con *console.SliverClient, args [
}
skipTests, _ := cmd.Flags().GetBool("skip-tests")
testID := uuid.Must(uuid.NewV4()).String()
testID := uuid.NewV4().String()
trafficEncoder := &clientpb.TrafficEncoder{
Wasm: &commonpb.File{
Name: filepath.Base(args[0]),
+3 -6
View File
@@ -540,10 +540,7 @@ func (m *editorModel) ensureCursorVisible() {
if m.top < 0 {
m.top = 0
}
maxTop := m.totalLines() - height
if maxTop < 0 {
maxTop = 0
}
maxTop := max(m.totalLines()-height, 0)
if m.top > maxTop {
m.top = maxTop
}
@@ -725,8 +722,8 @@ func baseName(path string) string {
return ""
}
clean := strings.ReplaceAll(path, "\\", "/")
if idx := strings.LastIndex(clean, "/"); idx != -1 {
return clean[idx+1:]
if _, after, ok := strings.CutLast(clean, "/"); ok {
return after
}
return path
}
+1 -4
View File
@@ -56,10 +56,7 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
fetchSize := maxSize + 1
if fetchSize < maxSize {
fetchSize = maxSize
}
fetchSize := max(maxSize+1, maxSize)
ctx, cancel := con.GrpcContext(cmd)
defer cancel()
+2 -2
View File
@@ -88,8 +88,8 @@ func filterCompletionValues(values []string, prefix string) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
candidate = value[:tab]
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
+39
View File
@@ -5,6 +5,7 @@ package opfor
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"os"
"path/filepath"
@@ -124,6 +125,44 @@ func expectedStringArguments(value string) []byte {
return arguments
}
func TestSliverArmoryCNACorpusPinnedHashes(t *testing.T) {
fixtures := []struct {
path string
sha256 string
}{
{
path: "bof_collection/cat.cna",
sha256: "94c7bcaae209a6355dcc8c126019f6e19a681173680955166b8c30cf97fc66f7",
},
{
path: "firefoxdump/firefoxdump.cna",
sha256: "c8c9ef28675d16dd1c8786f055c08ce2096eb0fedb16fad68b78f22c81b1bbde",
},
{
path: "operatorskit/finddotnet.cna",
sha256: "897dbee453ed504be7e9ace1229e7070f131a9351fb09a723629dd8e0337aa03",
},
{
path: "operatorskit/findsysmon.cna",
sha256: "1579dcca9e1c0ab7b20a646ff2db7705e05468c067a8ed6b3c35b553f10458d7",
},
}
for _, fixture := range fixtures {
fixture := fixture
t.Run(fixture.path, func(t *testing.T) {
content, err := os.ReadFile(filepath.Join("testdata", "corpus", filepath.FromSlash(fixture.path)))
if err != nil {
t.Fatalf("read pinned corpus fixture: %v", err)
}
digest := fmt.Sprintf("%x", sha256.Sum256(content))
if digest != fixture.sha256 {
t.Fatalf("SHA-256 = %s, want %s", digest, fixture.sha256)
}
})
}
}
type corpusDispatchTest struct {
name string
fixture string
+15 -9
View File
@@ -21,7 +21,6 @@ package portfwd
import (
"fmt"
"log"
"net"
"regexp"
"time"
@@ -50,14 +49,10 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
con.PrintErrorf("Must specify a remote target host:port\n")
return
}
remoteHost, remotePort, err := net.SplitHostPort(remoteAddr)
if err != nil {
con.PrintErrorf("Failed to parse remote target %s\n", err)
if err := validatePortfwdRemoteAddress(remoteAddr); err != nil {
con.PrintErrorf("Failed to parse remote target: %s\n", err)
return
}
if remotePort == "3389" {
con.PrintWarnf("RDP is generally broken over tunneled portfwds, we recommend using WireGuard portfwds\n")
}
bindAddr, _ := cmd.Flags().GetString("bind")
if bindAddr == "" {
con.PrintErrorf("Must specify a bind target host:port (e.g. 127.0.0.1:8000)")
@@ -88,14 +83,25 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
con.PrintErrorf("Failed to start port forward listener: %s\n", err)
return
}
core.Portfwds.Add(tcpProxy, channelProxy)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
}()
con.PrintInfof("Port forwarding %s -> %s:%s\n", bindAddr, remoteHost, remotePort)
con.PrintInfof("Port forwarding %s -> %s\n", bindAddr, remoteAddr)
}
func validatePortfwdRemoteAddress(remoteAddr string) error {
_, _, err := (&core.ChannelProxy{RemoteAddr: remoteAddr}).ValidatedHostPort()
return err
}
@@ -0,0 +1,38 @@
package portfwd
import "testing"
func TestValidatePortfwdRemoteAddress(t *testing.T) {
tests := []struct {
name string
address string
valid bool
}{
{name: "hostname RDP", address: "windows.example:3389", valid: true},
{name: "IPv4", address: "192.0.2.10:443", valid: true},
{name: "IPv6", address: "[2001:db8::10]:8443", valid: true},
{name: "missing port", address: "target.example", valid: false},
{name: "empty port", address: "target.example:", valid: false},
{name: "empty host", address: ":3389", valid: false},
{name: "zero port", address: "target.example:0", valid: false},
{name: "out of range port", address: "target.example:65536", valid: false},
{name: "service name", address: "target.example:https", valid: false},
{name: "unbracketed IPv6", address: "2001:db8::10:8443", valid: false},
{name: "hostname with space", address: "bad host:80", valid: false},
{name: "hostname with tab", address: "bad\thost:80", valid: false},
{name: "hostname with newline", address: "bad\nhost:80", valid: false},
{name: "IPv6 zone with space", address: "[fe80::1%bad zone]:80", valid: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validatePortfwdRemoteAddress(test.address)
if test.valid && err != nil {
t.Fatalf("validatePortfwdRemoteAddress(%q): %v", test.address, err)
}
if !test.valid && err == nil {
t.Fatalf("validatePortfwdRemoteAddress(%q) returned nil", test.address)
}
})
}
}
+24
View File
@@ -28,6 +28,7 @@ import (
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
@@ -53,6 +54,14 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
con.PrintErrorf("Failed to derive active session config.\n")
return
}
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("Failed to retrieve originating implant build %q: %v\n", session.Name, err)
return
}
if !applyGetSystemObfuscationPolicy(config, session.Name, builds) {
con.PrintWarnf("Originating implant build %q was not found; continuing with reconstructed legacy obfuscation settings.\n", session.Name)
}
/* 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
@@ -94,6 +103,21 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
}
}
func applyGetSystemObfuscationPolicy(config *clientpb.ImplantConfig, implantName string, builds *clientpb.ImplantBuilds) bool {
if config == nil || builds == nil {
return false
}
originatingConfig, ok := builds.GetConfigs()[implantName]
if !ok || originatingConfig == nil {
return false
}
config.Debug = originatingConfig.Debug
config.ObfuscateSymbols = originatingConfig.ObfuscateSymbols
config.ControlFlow = originatingConfig.ControlFlow
return true
}
// PrintGetSystem - Print the results of get system
func PrintGetSystem(getsystemResp *sliverpb.GetSystem, con *console.SliverClient) {
if getsystemResp.Response != nil && getsystemResp.Response.GetErr() != "" {
@@ -0,0 +1,35 @@
package privilege
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestApplyGetSystemObfuscationPolicy(t *testing.T) {
config := &clientpb.ImplantConfig{Debug: true}
builds := &clientpb.ImplantBuilds{Configs: map[string]*clientpb.ImplantConfig{
"source-build": {
Debug: false,
ObfuscateSymbols: true,
ControlFlow: clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1,
},
}}
if !applyGetSystemObfuscationPolicy(config, "source-build", builds) {
t.Fatal("applyGetSystemObfuscationPolicy() = false, want true")
}
if config.Debug || !config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_BALANCED_V1 {
t.Fatalf("obfuscation policy = %+v, want originating build values", config)
}
}
func TestApplyGetSystemObfuscationPolicyMissingBuildPreservesLegacyConfig(t *testing.T) {
config := &clientpb.ImplantConfig{Debug: true}
if applyGetSystemObfuscationPolicy(config, "missing-build", &clientpb.ImplantBuilds{}) {
t.Fatal("applyGetSystemObfuscationPolicy() = true for missing build, want false")
}
if !config.Debug || config.ObfuscateSymbols || config.ControlFlow != clientpb.ControlFlowPolicy_CONTROL_FLOW_DISABLED {
t.Fatalf("missing build changed legacy config: %+v", config)
}
}
+2 -8
View File
@@ -58,17 +58,11 @@ func RegCreateKeyCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if strings.Contains(regPath, "/") {
regPath = strings.ReplaceAll(regPath, "/", "\\")
}
pathBaseIdx := strings.LastIndex(regPath, `\`)
if pathBaseIdx < 0 {
finalPath, key, ok := strings.CutLast(regPath, `\`)
if !ok {
con.PrintErrorf("invalid path: %s", regPath)
return
}
if len(regPath) < pathBaseIdx+1 {
con.PrintErrorf("invalid path: %s", regPath)
return
}
finalPath := regPath[:pathBaseIdx]
key := regPath[pathBaseIdx+1:]
createKey, err := con.Rpc.RegistryCreateKey(context.Background(), &sliverpb.RegistryCreateKeyReq{
Hive: hive,
+2 -8
View File
@@ -58,17 +58,11 @@ func RegDeleteKeyCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if strings.Contains(regPath, "/") {
regPath = strings.ReplaceAll(regPath, "/", "\\")
}
pathBaseIdx := strings.LastIndex(regPath, `\`)
if pathBaseIdx < 0 {
finalPath, key, ok := strings.CutLast(regPath, `\`)
if !ok {
con.PrintErrorf("invalid path: %s", regPath)
return
}
if len(regPath) < pathBaseIdx+1 {
con.PrintErrorf("invalid path: %s", regPath)
return
}
finalPath := regPath[:pathBaseIdx]
key := regPath[pathBaseIdx+1:]
deleteKey, err := con.Rpc.RegistryDeleteKey(context.Background(), &sliverpb.RegistryDeleteKeyReq{
Hive: hive,
+3 -8
View File
@@ -111,17 +111,12 @@ func RegReadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if strings.Contains(regPath, "/") {
regPath = strings.ReplaceAll(regPath, "/", "\\")
}
pathBaseIdx := strings.LastIndex(regPath, `\`)
if pathBaseIdx < 0 {
var ok bool
finalPath, key, ok = strings.CutLast(regPath, `\`)
if !ok {
con.PrintErrorf("invalid path: %s", regPath)
return
}
if len(regPath) < pathBaseIdx+1 {
con.PrintErrorf("invalid path: %s", regPath)
return
}
finalPath = regPath[:pathBaseIdx]
key = regPath[pathBaseIdx+1:]
regRead, err := con.Rpc.RegistryRead(context.Background(), &sliverpb.RegistryReadReq{
Hive: hive,
+2 -8
View File
@@ -76,17 +76,11 @@ func RegWriteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if strings.Contains(regPath, "/") {
regPath = strings.ReplaceAll(regPath, "/", "\\")
}
pathBaseIdx := strings.LastIndex(regPath, `\`)
if pathBaseIdx < 0 {
finalPath, key, ok := strings.CutLast(regPath, `\`)
if !ok {
con.PrintErrorf("invalid path: %s", regPath)
return
}
if len(regPath) < pathBaseIdx+1 {
con.PrintErrorf("invalid path: %s", regPath)
return
}
finalPath := regPath[:pathBaseIdx]
key := regPath[pathBaseIdx+1:]
switch valType {
case sliverpb.RegistryTypeBinary:
var (
+2 -2
View File
@@ -125,8 +125,8 @@ func filterCompletionValues(values []string, prefix string) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
candidate = value[:tab]
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
+124 -85
View File
@@ -27,6 +27,7 @@ import (
"github.com/bishopfox/sliver/client/command/kill"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
@@ -131,51 +132,29 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
if err != nil {
width = 999
}
tw := renderSessions(sessions, filter, filterRegex, con, width, time.Now())
con.Printf("%s\n", tw.Render())
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
wideTermWidth := con.Settings.SmallTermWidth < width
windowsSessionInList := false
func renderSessions(sessions map[string]*clientpb.Session, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
hasIntegrity := false
for _, session := range sessions {
if session.OS == "windows" {
windowsSessionInList = true
hasIntegrity = true
break
}
}
if wideTermWidth {
if windowsSessionInList {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Integrity",
"Operating System",
"Locale",
"Last Message",
"Health",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Operating System",
"Locale",
"Last Message",
"Health",
})
}
return targettable.Fit(width, hasIntegrity, func(layout targettable.Layout) table.Writer {
return buildSessionsTable(sessions, filter, filterRegex, con, layout, hasIntegrity, now)
})
}
} else {
func buildSessionsTable(sessions map[string]*clientpb.Session, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, hasIntegrity bool, now time.Time) table.Writer {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
if layout.Compact {
tw.AppendHeader(table.Row{
"ID",
"Transport",
@@ -185,31 +164,98 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
"Operating System",
"Health",
})
} else {
header := table.Row{
"ID",
"Name",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
}
if hasIntegrity && layout.IncludeIntegrity {
header = append(header, "Integrity")
}
header = append(header, "Operating System")
if layout.IncludeLocale {
header = append(header, "Locale")
}
header = append(header, "Last Message", "Health")
tw.AppendHeader(header)
}
tw.SortBy([]table.SortBy{
{Name: "ID", Mode: table.Asc},
})
for _, session := range sessions {
style := console.StyleNormal
if con.ActiveTarget.GetSession() != nil && con.ActiveTarget.GetSession().ID == session.ID {
style = console.StyleGreen
activeSessionID := ""
if con.ActiveTarget != nil {
if activeSession := con.ActiveTarget.GetSession(); activeSession != nil {
activeSessionID = activeSession.ID
}
var SessionHealth string
}
for _, session := range sessions {
username := strings.TrimPrefix(session.Username, session.Hostname+"\\") // For non-AD Windows users
lastMessage := formatRelativeTime(time.Unix(session.LastCheckin, 0), now)
health := "[ALIVE]"
if session.IsDead {
SessionHealth = console.StyleBoldRed.Render("[DEAD]")
} else {
SessionHealth = console.StyleBoldGreen.Render("[ALIVE]")
health = "[DEAD]"
}
burned := ""
if session.Burned {
burned = "🔥"
}
username := strings.TrimPrefix(session.Username, session.Hostname+"\\") // For non-AD Windows users
canonicalEntries := []string{
ShortSessionID(session.ID),
session.Name,
session.Transport,
session.RemoteAddress,
session.Hostname,
username,
fmt.Sprintf("%s (%d)", session.Filename, session.PID),
}
if hasIntegrity {
canonicalEntries = append(canonicalEntries, session.Integrity)
}
canonicalEntries = append(canonicalEntries,
fmt.Sprintf("%s/%s", session.OS, session.Arch),
session.Locale,
lastMessage,
burned+health,
)
if !matchesSessionFilter(canonicalEntries, filter, filterRegex) {
continue
}
style := console.StyleNormal
if activeSessionID == session.ID {
style = console.StyleGreen
}
var sessionHealth string
if session.IsDead {
sessionHealth = console.StyleBoldRed.Render("[DEAD]")
} else {
sessionHealth = console.StyleBoldGreen.Render("[ALIVE]")
}
var rowEntries []string
if wideTermWidth {
if layout.Compact {
rowEntries = []string{
style.Render(ShortSessionID(session.ID)),
style.Render(session.Transport),
style.Render(session.RemoteAddress),
style.Render(session.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
burned + sessionHealth,
}
} else {
processName := session.Filename
if layout.ProcessBasename {
processName = targettable.ProcessName(processName)
}
rowEntries = []string{
style.Render(ShortSessionID(session.ID)),
style.Render(session.Name),
@@ -217,57 +263,50 @@ func PrintSessions(sessions map[string]*clientpb.Session, filter string, filterR
style.Render(session.RemoteAddress),
style.Render(session.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s (%d)", session.Filename, session.PID)),
style.Render(fmt.Sprintf("%s (%d)", processName, session.PID)),
}
if windowsSessionInList {
if hasIntegrity && layout.IncludeIntegrity {
rowEntries = append(rowEntries, style.Render(session.Integrity))
}
rowEntries = append(rowEntries, []string{
style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
style.Render(session.Locale),
con.FormatDateDelta(time.Unix(session.LastCheckin, 0), wideTermWidth, false),
burned + SessionHealth,
}...)
} else {
rowEntries = []string{
style.Render(ShortSessionID(session.ID)),
style.Render(session.Transport),
style.Render(session.RemoteAddress),
style.Render(session.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)),
burned + SessionHealth,
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", session.OS, session.Arch)))
if layout.IncludeLocale {
rowEntries = append(rowEntries, style.Render(session.Locale))
}
rowEntries = append(rowEntries, lastMessage, burned+sessionHealth)
}
// Build the row struct
row := table.Row{}
for _, entry := range rowEntries {
row = append(row, entry)
}
// Apply filters if any
if filter == "" && filterRegex == nil {
tw.AppendRow(row)
} else {
for _, rowEntry := range rowEntries {
if filter != "" {
if strings.Contains(rowEntry, filter) {
tw.AppendRow(row)
break
}
}
if filterRegex != nil {
if filterRegex.MatchString(rowEntry) {
tw.AppendRow(row)
break
}
}
}
}
tw.AppendRow(row)
}
con.Printf("%s\n", tw.Render())
return tw
}
func matchesSessionFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
if filter == "" && filterRegex == nil {
return true
}
for _, entry := range entries {
if filter != "" && strings.Contains(entry, filter) {
return true
}
if filterRegex != nil && filterRegex.MatchString(entry) {
return true
}
}
return false
}
func formatRelativeTime(t time.Time, now time.Time) string {
if t.After(now) {
return fmt.Sprintf("in %s", t.Sub(now).Round(time.Second))
}
return fmt.Sprintf("%s ago", now.Sub(t).Round(time.Second))
}
// ShortSessionID - Shorten the session ID.
+276
View File
@@ -0,0 +1,276 @@
package sessions
import (
"regexp"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
)
var sessionsTestNow = time.Date(2026, time.September, 3, 14, 25, 41, 0, time.UTC)
func TestRenderSessionsKeepsFullProcessPathWhenItFits(t *testing.T) {
session := sessionsTestSession("windows", `C:\Users\vagrant\AppData\Local\Temp\sliver.exe`)
sessions := sessionMap(session)
con := sessionsTestConsole()
hasIntegrity := true
fullLayout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, fullLayout, hasIntegrity, sessionsTestNow))
rendered := renderedSessionTable(renderSessions(sessions, "", nil, con, width, sessionsTestNow))
if !strings.Contains(rendered, session.Filename+" (456)") {
t.Fatalf("full process path was not retained:\n%s", rendered)
}
assertSessionTableFits(t, rendered, width)
}
func TestRenderSessionsUsesTargetProcessBasenameBeforeOmittingColumns(t *testing.T) {
tests := []struct {
name string
operatingSys string
filename string
basename string
hasIntegrity bool
}{
{
name: "windows path",
operatingSys: "windows",
filename: `C:\Users\vagrant\AppData\Local\Temp\a-very-long-directory-name\sliver.exe`,
basename: "sliver.exe",
hasIntegrity: true,
},
{
name: "POSIX path",
operatingSys: "linux",
filename: "/opt/sliver/a-very-long-directory-name/implant/sliver",
basename: "sliver",
hasIntegrity: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
session := sessionsTestSession(test.operatingSys, test.filename)
sessions := sessionMap(session)
con := sessionsTestConsole()
fullLayout := targettable.Layout{IncludeIntegrity: test.hasIntegrity, IncludeLocale: true}
basenameLayout := targettable.Layout{ProcessBasename: true, IncludeIntegrity: test.hasIntegrity, IncludeLocale: true}
fullWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, fullLayout, test.hasIntegrity, sessionsTestNow))
basenameWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, basenameLayout, test.hasIntegrity, sessionsTestNow))
if basenameWidth >= fullWidth {
t.Fatalf("test fixture does not make basename table narrower: basename=%d full=%d", basenameWidth, fullWidth)
}
rendered := renderedSessionTable(renderSessions(sessions, "", nil, con, basenameWidth, sessionsTestNow))
if !strings.Contains(rendered, test.basename+" (456)") {
t.Fatalf("process basename was not rendered:\n%s", rendered)
}
if strings.Contains(rendered, test.filename) {
t.Fatalf("full process path was rendered after basename fallback:\n%s", rendered)
}
if !strings.Contains(rendered, "Locale") {
t.Fatalf("Locale was omitted before it was needed:\n%s", rendered)
}
if test.hasIntegrity && !strings.Contains(rendered, "Integrity") {
t.Fatalf("Integrity was omitted before it was needed:\n%s", rendered)
}
assertSessionTableFits(t, rendered, basenameWidth)
})
}
}
func TestRenderSessionsOmitsIntegrityBeforeLocale(t *testing.T) {
session := sessionsTestSession("windows", `C:\Users\vagrant\AppData\Local\Temp\a-very-long-directory-name\sliver.exe`)
session.Integrity = "System-With-An-Extremely-Long-Integrity-Label"
session.Locale = "extraordinarily-long-locale"
sessions := sessionMap(session)
con := sessionsTestConsole()
hasIntegrity := true
withoutIntegrity := targettable.Layout{ProcessBasename: true, IncludeLocale: true}
withoutIntegrityOrLocale := targettable.Layout{ProcessBasename: true}
withoutIntegrityWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, withoutIntegrity, hasIntegrity, sessionsTestNow))
withoutIntegrityOrLocaleWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, withoutIntegrityOrLocale, hasIntegrity, sessionsTestNow))
if withoutIntegrityOrLocaleWidth >= withoutIntegrityWidth {
t.Fatalf("test fixture does not make Locale omission narrower: without Locale=%d with Locale=%d", withoutIntegrityOrLocaleWidth, withoutIntegrityWidth)
}
t.Run("Integrity omitted first", func(t *testing.T) {
rendered := renderedSessionTable(renderSessions(sessions, "", nil, con, withoutIntegrityWidth, sessionsTestNow))
if strings.Contains(rendered, "Integrity") || strings.Contains(rendered, session.Integrity) {
t.Fatalf("Integrity column was retained:\n%s", rendered)
}
if !strings.Contains(rendered, "Locale") || !strings.Contains(rendered, session.Locale) {
t.Fatalf("Locale should remain after Integrity is omitted:\n%s", rendered)
}
if !strings.Contains(rendered, "Process (PID)") {
t.Fatalf("renderer unexpectedly used the compact fallback:\n%s", rendered)
}
assertSessionTableFits(t, rendered, withoutIntegrityWidth)
})
t.Run("Locale omitted second", func(t *testing.T) {
rendered := renderedSessionTable(renderSessions(sessions, "", nil, con, withoutIntegrityOrLocaleWidth, sessionsTestNow))
if strings.Contains(rendered, "Integrity") || strings.Contains(rendered, "Locale") {
t.Fatalf("optional columns were retained:\n%s", rendered)
}
if !strings.Contains(rendered, "Process (PID)") {
t.Fatalf("renderer unexpectedly used the compact fallback:\n%s", rendered)
}
assertSessionTableFits(t, rendered, withoutIntegrityOrLocaleWidth)
})
}
func TestRenderSessionsUsesDirectionalRelativeLastMessage(t *testing.T) {
tests := []struct {
name string
lastCheckin time.Time
want string
}{
{name: "past", lastCheckin: sessionsTestNow.Add(-36 * time.Second), want: "36s ago"},
{name: "future clock skew", lastCheckin: sessionsTestNow.Add(5 * time.Second), want: "in 5s"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
session := sessionsTestSession("linux", "/opt/sliver/sliver")
session.LastCheckin = test.lastCheckin.Unix()
rendered := renderedSessionTable(renderSessions(sessionMap(session), "", nil, sessionsTestConsole(), 10_000, sessionsTestNow))
if !strings.Contains(rendered, test.want) {
t.Fatalf("relative Last Message %q was not rendered:\n%s", test.want, rendered)
}
if strings.Contains(rendered, "2026") || strings.Contains(rendered, test.lastCheckin.Format(time.UnixDate)) {
t.Fatalf("Last Message unexpectedly contains a calendar date:\n%s", rendered)
}
})
}
}
func TestRenderSessionsOmitsIntegrityForNonWindowsSessions(t *testing.T) {
session := sessionsTestSession("linux", "/opt/sliver/sliver")
rendered := renderedSessionTable(renderSessions(sessionMap(session), "", nil, sessionsTestConsole(), 10_000, sessionsTestNow))
if strings.Contains(rendered, "Integrity") {
t.Fatalf("non-Windows sessions table contains an Integrity column:\n%s", rendered)
}
if !strings.Contains(rendered, "Locale") {
t.Fatalf("wide non-Windows sessions table is missing Locale:\n%s", rendered)
}
}
func TestRenderSessionsCompactFallbackPreservesLegacyColumns(t *testing.T) {
session := sessionsTestSession("windows", `C:\Users\vagrant\AppData\Local\Temp\sliver.exe`)
sessions := sessionMap(session)
con := sessionsTestConsole()
compactWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, targettable.Layout{Compact: true}, true, sessionsTestNow))
withoutOptionalWidth := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, targettable.Layout{ProcessBasename: true}, true, sessionsTestNow))
if compactWidth >= withoutOptionalWidth {
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
}
rendered := renderedSessionTable(renderSessions(sessions, "", nil, con, compactWidth, sessionsTestNow))
header := strings.SplitN(rendered, "\n", 2)[0]
for _, retained := range []string{"ID", "Transport", "Remote Address", "Hostname", "Username", "Operating System", "Health"} {
if !strings.Contains(header, retained) {
t.Fatalf("compact fallback is missing legacy column %q:\n%s", retained, rendered)
}
}
for _, omitted := range []string{"Process (PID)", "Integrity", "Locale", "Last Message"} {
if strings.Contains(header, omitted) {
t.Fatalf("compact fallback unexpectedly contains column %q:\n%s", omitted, rendered)
}
}
if strings.Contains(rendered, session.Name) {
t.Fatalf("compact fallback unexpectedly contains the session name:\n%s", rendered)
}
assertSessionTableFits(t, rendered, compactWidth)
}
func TestRenderSessionsFiltersCanonicalValuesHiddenByLayout(t *testing.T) {
session := sessionsTestSession("windows", `C:\hidden-directory-token\sliver.exe`)
session.Integrity = "hidden-integrity-token"
session.Locale = "hidden-locale-token"
sessions := sessionMap(session)
con := sessionsTestConsole()
layout := targettable.Layout{ProcessBasename: true}
width := renderedSessionTableWidth(buildSessionsTable(sessions, "", nil, con, layout, true, sessionsTestNow))
tests := []struct {
name string
filter string
filterRegex *regexp.Regexp
hiddenValue string
}{
{name: "substring in shortened path", filter: "hidden-directory-token", hiddenValue: "hidden-directory-token"},
{name: "regexp in omitted columns", filterRegex: regexp.MustCompile(`hidden-(?:integrity|locale)-token`), hiddenValue: "hidden-integrity-token"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rendered := renderedSessionTable(renderSessions(sessions, test.filter, test.filterRegex, con, width, sessionsTestNow))
if !strings.Contains(rendered, ShortSessionID(session.ID)) {
t.Fatalf("session matching a canonical value was filtered out:\n%s", rendered)
}
if strings.Contains(rendered, test.hiddenValue) {
t.Fatalf("hidden canonical value leaked into rendered layout:\n%s", rendered)
}
if !strings.Contains(rendered, "Process (PID)") || strings.Contains(rendered, "Locale") || strings.Contains(rendered, "Integrity") {
t.Fatalf("test did not exercise the shortened, omitted-column layout:\n%s", rendered)
}
assertSessionTableFits(t, rendered, width)
})
}
}
func sessionsTestSession(operatingSystem string, filename string) *clientpb.Session {
return &clientpb.Session{
ID: "01234567-89ab-cdef-0123-456789abcdef",
Name: "winterfell-session",
Transport: "mtls",
RemoteAddress: "10.13.37.10:61839",
Hostname: "winterfell",
Username: `winterfell\vagrant`,
Filename: filename,
PID: 456,
Integrity: "High",
OS: operatingSystem,
Arch: "amd64",
Locale: "en-US",
LastCheckin: sessionsTestNow.Add(-36 * time.Second).Unix(),
}
}
func sessionsTestConsole() *console.SliverClient {
return &console.SliverClient{
Settings: &assets.ClientSettings{TableStyle: settings.SliverDefault.Name},
}
}
func sessionMap(sessions ...*clientpb.Session) map[string]*clientpb.Session {
result := make(map[string]*clientpb.Session, len(sessions))
for _, session := range sessions {
result[session.ID] = session
}
return result
}
func renderedSessionTable(writer table.Writer) string {
return text.StripEscape(writer.Render())
}
func renderedSessionTableWidth(writer table.Writer) int {
return text.LongestLineLen(writer.Render())
}
func assertSessionTableFits(t *testing.T, rendered string, width int) {
t.Helper()
if got := text.LongestLineLen(rendered); got > width {
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
}
}
+4 -4
View File
@@ -43,8 +43,9 @@ func runAttachedIO(tunnel *core.TunnelIO, con *console.SliverClient, remoteOS st
stdinQueue := make(chan shellInputChunk, stdinQueueDepth)
stopWriter := make(chan struct{})
var writerWG sync.WaitGroup
writerWG.Add(1)
go runShellInputWriter(tunnel, stdinQueue, stopWriter, &writerWG)
writerWG.Go(func() {
runShellInputWriter(tunnel, stdinQueue, stopWriter)
})
defer func() {
close(stopWriter)
@@ -96,8 +97,7 @@ func runAttachedIO(tunnel *core.TunnelIO, con *console.SliverClient, remoteOS st
return detached, closeRequested
}
func runShellInputWriter(tunnel *core.TunnelIO, stdinQueue <-chan shellInputChunk, stop <-chan struct{}, writerWG *sync.WaitGroup) {
defer writerWG.Done()
func runShellInputWriter(tunnel *core.TunnelIO, stdinQueue <-chan shellInputChunk, stop <-chan struct{}) {
defer recoverShellInputWriter()
for {
+2 -4
View File
@@ -73,13 +73,11 @@ func (s *managedShell) SetOutput(w io.Writer) {
}
func (s *managedShell) startReader(remove func()) {
s.readerWG.Add(1)
go func() {
defer s.readerWG.Done()
s.readerWG.Go(func() {
_, err := io.Copy(s.output, s.tunnel)
log.Printf("Shell tunnel reader (id=%d tunnel=%d) exited: %v", s.ID, s.TunnelID, err)
remove()
}()
})
}
// swapWriter is an io.Writer whose underlying destination can be swapped at runtime.
+21 -13
View File
@@ -203,7 +203,7 @@ func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
// Best-effort graceful request before closing the tunnel.
requestRemoteShellExit(sh.Tunnel())
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
if err := closeShellTunnel(con, sh.Tunnel()); err != nil {
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
}
@@ -285,11 +285,11 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
case <-tunnel.Done():
bindTimer.Stop()
con.PrintErrorf("Shell tunnel closed before it was bound\n")
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
case <-bindTimer.C:
con.PrintErrorf("Timed out binding shell tunnel\n")
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
@@ -318,12 +318,12 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
cancelShell()
if err != nil {
con.PrintErrorf("%s\n", err)
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
if shell.Response != nil && shell.Response.Err != "" {
con.PrintErrorf("Error: %s\n", shell.Response.Err)
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
@@ -393,20 +393,28 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
shells.Remove(managed.ID)
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellExited
}
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
err := closeShellTunnel(con, tunnelID, sessionID)
func backgroundCloseShell(con *console.SliverClient, tunnel *core.TunnelIO) {
if tunnel == nil {
return
}
err := closeShellTunnel(con, tunnel)
if err != nil {
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
log.Printf("Background close tunnel %d failed: %v", tunnel.ID, err)
}
}
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
core.GetTunnels().Close(tunnelID)
func closeShellTunnel(con *console.SliverClient, tunnel *core.TunnelIO) error {
if tunnel == nil {
return nil
}
if !core.GetTunnels().CloseIf(tunnel) {
return nil
}
if con == nil || con.Rpc == nil {
return nil
}
@@ -414,8 +422,8 @@ func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID stri
ctx, cancel := context.WithTimeout(context.Background(), shellCloseTimeout)
defer cancel()
_, err := con.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnelID,
SessionID: sessionID,
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
})
return err
}
+64 -1
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
@@ -15,9 +16,21 @@ import (
type closeTunnelTestRPC struct {
rpcpb.SliverRPCClient
deadline time.Time
calls int
}
type closeShellTestStream struct {
grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData]
sent chan *sliverpb.TunnelData
}
func (stream *closeShellTestStream) Send(frame *sliverpb.TunnelData) error {
stream.sent <- frame
return nil
}
func (r *closeTunnelTestRPC) CloseTunnel(ctx context.Context, _ *sliverpb.Tunnel, _ ...grpc.CallOption) (*commonpb.Empty, error) {
r.calls++
deadline, ok := ctx.Deadline()
if ok {
r.deadline = deadline
@@ -26,12 +39,23 @@ func (r *closeTunnelTestRPC) CloseTunnel(ctx context.Context, _ *sliverpb.Tunnel
}
func TestCloseShellTunnelUsesBoundedContext(t *testing.T) {
tunnels := core.GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &closeShellTestStream{sent: make(chan *sliverpb.TunnelData, 1)}
tunnels.SetStream(stream)
tunnel := tunnels.Start(99, "session")
waitForShellBindFrame(t, stream.sent)
rpc := &closeTunnelTestRPC{}
con := &console.SliverClient{Rpc: rpc}
if err := closeShellTunnel(con, 99, "session"); err != nil {
if err := closeShellTunnel(con, tunnel); err != nil {
t.Fatalf("close shell tunnel: %v", err)
}
if rpc.calls != 1 {
t.Fatalf("CloseTunnel RPC calls = %d, want 1", rpc.calls)
}
if rpc.deadline.IsZero() {
t.Fatal("CloseTunnel RPC context has no deadline")
}
@@ -39,3 +63,42 @@ func TestCloseShellTunnelUsesBoundedContext(t *testing.T) {
t.Fatalf("CloseTunnel deadline is %v away, want within (0, %v]", remaining, shellCloseTimeout)
}
}
func TestCloseShellTunnelDoesNotCloseReplacementGeneration(t *testing.T) {
tunnels := core.GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &closeShellTestStream{sent: make(chan *sliverpb.TunnelData, 2)}
tunnels.SetStream(stream)
old := tunnels.Start(100, "old-session")
waitForShellBindFrame(t, stream.sent)
replacement := tunnels.Start(100, "new-session")
waitForShellBindFrame(t, stream.sent)
rpc := &closeTunnelTestRPC{}
con := &console.SliverClient{Rpc: rpc}
if err := closeShellTunnel(con, old); err != nil {
t.Fatalf("close stale shell tunnel: %v", err)
}
if rpc.calls != 0 {
t.Fatalf("stale shell cleanup sent %d numeric CloseTunnel RPCs, want 0", rpc.calls)
}
if current := tunnels.Get(100); current != replacement {
t.Fatal("stale shell cleanup removed the replacement tunnel")
}
select {
case <-replacement.Done():
t.Fatal("stale shell cleanup closed the replacement tunnel")
default:
}
}
func waitForShellBindFrame(t *testing.T, frames <-chan *sliverpb.TunnelData) {
t.Helper()
select {
case <-frames:
case <-time.After(time.Second):
t.Fatal("timed out waiting for shell tunnel bind frame")
}
}
+65
View File
@@ -0,0 +1,65 @@
// Package targettable contains shared layout helpers for the sessions and
// beacons tables.
package targettable
import (
"path"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
)
// Layout describes one rendering candidate for a target table.
type Layout struct {
ProcessBasename bool
IncludeIntegrity bool
IncludeLocale bool
Compact bool
}
// Fit returns the first table that fits within width. Layouts are tried from
// most to least detailed: full process path, process basename, no Integrity,
// no Locale, and finally the legacy compact layout bounded to width. When no
// Integrity column exists, that omission step is skipped. A non-positive width
// is treated as unknown and keeps the full layout.
func Fit(width int, hasIntegrity bool, build func(Layout) table.Writer) table.Writer {
layout := Layout{
IncludeIntegrity: hasIntegrity,
IncludeLocale: true,
}
if candidate := build(layout); width <= 0 || text.LongestLineLen(candidate.Render()) <= width {
return candidate
}
layout.ProcessBasename = true
if candidate := build(layout); text.LongestLineLen(candidate.Render()) <= width {
return candidate
}
if hasIntegrity {
layout.IncludeIntegrity = false
if candidate := build(layout); text.LongestLineLen(candidate.Render()) <= width {
return candidate
}
}
layout.IncludeLocale = false
if candidate := build(layout); text.LongestLineLen(candidate.Render()) <= width {
return candidate
}
compact := build(Layout{Compact: true})
compact.Style().Size.WidthMax = width
return compact
}
// ProcessName returns the final path component of a target process path.
// Target paths can use a different separator than the client host, so Windows
// separators are normalized before applying path.Base.
func ProcessName(processPath string) string {
if processPath == "" {
return ""
}
return path.Base(strings.ReplaceAll(processPath, `\`, "/"))
}
@@ -0,0 +1,215 @@
package targettable
import (
"strings"
"testing"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
)
func TestFitSelectsLayoutsInOrder(t *testing.T) {
tests := []struct {
name string
width int
hasIntegrity bool
wantWidth int
wantCalls []Layout
}{
{
name: "full path exact fit",
width: 50,
hasIntegrity: true,
wantWidth: 50,
wantCalls: []Layout{
{IncludeIntegrity: true, IncludeLocale: true},
},
},
{
name: "process basename",
width: 40,
hasIntegrity: true,
wantWidth: 40,
wantCalls: []Layout{
{IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeIntegrity: true, IncludeLocale: true},
},
},
{
name: "omit integrity",
width: 30,
hasIntegrity: true,
wantWidth: 30,
wantCalls: []Layout{
{IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeLocale: true},
},
},
{
name: "omit locale",
width: 20,
hasIntegrity: true,
wantWidth: 20,
wantCalls: []Layout{
{IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeLocale: true},
{ProcessBasename: true},
},
},
{
name: "compact fallback",
width: 19,
hasIntegrity: true,
wantWidth: 10,
wantCalls: []Layout{
{IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeIntegrity: true, IncludeLocale: true},
{ProcessBasename: true, IncludeLocale: true},
{ProcessBasename: true},
{Compact: true},
},
},
{
name: "skip absent integrity",
width: 20,
hasIntegrity: false,
wantWidth: 20,
wantCalls: []Layout{
{IncludeLocale: true},
{ProcessBasename: true, IncludeLocale: true},
{ProcessBasename: true},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
calls := []Layout{}
got := Fit(tt.width, tt.hasIntegrity, func(layout Layout) table.Writer {
calls = append(calls, layout)
return tableWithWidth(layoutWidth(layout))
})
if gotWidth := text.LongestLineLen(got.Render()); gotWidth != tt.wantWidth {
t.Fatalf("selected table width = %d, want %d", gotWidth, tt.wantWidth)
}
if len(calls) != len(tt.wantCalls) {
t.Fatalf("build calls = %#v, want %#v", calls, tt.wantCalls)
}
for index := range calls {
if calls[index] != tt.wantCalls[index] {
t.Fatalf("build call %d = %#v, want %#v", index, calls[index], tt.wantCalls[index])
}
}
})
}
}
func TestFitUsesANSIVisibleWidthAndAcceptsExactFit(t *testing.T) {
const colored = "\x1b[31m12345\x1b[0m"
buildCalls := 0
build := func(Layout) table.Writer {
buildCalls++
tw := table.NewWriter()
tw.SetStyle(singleCellStyle())
tw.AppendRow(table.Row{colored})
return tw
}
exactWidth := text.LongestLineLen(build(Layout{}).Render())
buildCalls = 0
got := Fit(exactWidth, true, build)
if buildCalls != 1 {
t.Fatalf("build calls = %d, want 1 for an ANSI-aware exact fit", buildCalls)
}
if gotWidth := text.LongestLineLen(got.Render()); gotWidth != exactWidth {
t.Fatalf("selected table width = %d, want %d", gotWidth, exactWidth)
}
}
func TestFitBoundsOversizedCompactFallback(t *testing.T) {
const width = 20
got := Fit(width, true, func(layout Layout) table.Writer {
if layout.Compact {
return tableWithWidth(40)
}
return tableWithWidth(50)
})
if gotWidth := text.LongestLineLen(got.Render()); gotWidth > width {
t.Fatalf("compact table width = %d, want at most %d", gotWidth, width)
}
}
func TestFitTreatsUnknownWidthAsUnlimited(t *testing.T) {
buildCalls := 0
got := Fit(0, true, func(layout Layout) table.Writer {
buildCalls++
return tableWithWidth(layoutWidth(layout))
})
if buildCalls != 1 {
t.Fatalf("build calls = %d, want 1 for an unknown terminal width", buildCalls)
}
if gotWidth := text.LongestLineLen(got.Render()); gotWidth != 50 {
t.Fatalf("selected table width = %d, want full-width table of 50", gotWidth)
}
}
func TestProcessName(t *testing.T) {
tests := []struct {
name string
path string
want string
}{
{name: "windows", path: `C:\Users\vagrant\AppData\Local\Temp\sliver.exe`, want: "sliver.exe"},
{name: "UNC", path: `\\server\share\sliver.exe`, want: "sliver.exe"},
{name: "POSIX", path: "/usr/local/bin/sliver", want: "sliver"},
{name: "mixed separators", path: `C:\Temp/subdir\sliver.exe`, want: "sliver.exe"},
{name: "base name", path: "sliver.exe", want: "sliver.exe"},
{name: "empty", path: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ProcessName(tt.path); got != tt.want {
t.Fatalf("ProcessName(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func layoutWidth(layout Layout) int {
switch {
case layout.Compact:
return 10
case !layout.ProcessBasename:
return 50
case layout.IncludeIntegrity:
return 40
case layout.IncludeLocale:
return 30
default:
return 20
}
}
func tableWithWidth(width int) table.Writer {
tw := table.NewWriter()
tw.SetStyle(singleCellStyle())
tw.AppendRow(table.Row{strings.Repeat("x", width)})
return tw
}
func singleCellStyle() table.Style {
return table.Style{
Format: table.FormatOptions{
Footer: text.FormatDefault,
Header: text.FormatDefault,
Row: text.FormatDefault,
},
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ func SelectBeaconTask(tasks []*clientpb.BeaconTask) (*clientpb.BeaconTask, error
buf := bytes.NewBufferString("")
table := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
for _, task := range tasks {
shortID := strings.Split(task.ID, "-")[0]
shortID, _, _ := strings.Cut(task.ID, "-")
fmt.Fprintf(table, "%s\t%s\t%s\t\n", shortID, task.Description, prettyState(task.State))
}
table.Flush()
+2 -4
View File
@@ -545,12 +545,10 @@ func isRetryableDownloadError(err error) bool {
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
var netErr net.Error
if errors.As(err, &netErr) {
if netErr, ok := errors.AsType[net.Error](err); ok {
return netErr.Timeout() || netErr.Temporary()
}
var statusErr httpStatusError
if errors.As(err, &statusErr) {
if statusErr, ok := errors.AsType[httpStatusError](err); ok {
switch statusErr.status {
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return true
+2 -2
View File
@@ -99,8 +99,8 @@ func filterCompletionValues(values []string, prefix string) []string {
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
candidate = value[:tab]
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
+3 -2
View File
@@ -24,6 +24,7 @@ import (
"io"
"os"
"path/filepath"
"slices"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
@@ -98,7 +99,7 @@ func WasmCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func isRegistered(name string, cmd *cobra.Command, con *console.SliverClient) bool {
// Check if we have already registered this wasm module
if wasmRegistrationCache[idOf(con)] != nil {
if util.Contains(wasmRegistrationCache[idOf(con)], name) {
if slices.Contains(wasmRegistrationCache[idOf(con)], name) {
return true
}
}
@@ -274,7 +275,7 @@ func WasmLsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
} else {
for _, extName := range loaded.Names {
cacheLine := ""
if util.Contains(wasmRegistrationCache[idOf(con)], extName) {
if slices.Contains(wasmRegistrationCache[idOf(con)], extName) {
cacheLine = " (cached)"
} else {
wasmRegistrationCache[idOf(con)] = append(wasmRegistrationCache[idOf(con)], extName)
+9 -9
View File
@@ -24,6 +24,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strconv"
@@ -42,14 +43,13 @@ import (
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/util"
"github.com/gofrs/uuid"
"github.com/kballard/go-shellquote"
"github.com/reeflective/console"
"github.com/reeflective/readline"
"github.com/spf13/cobra"
"golang.org/x/exp/slog"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
uuid "uuid"
)
const (
@@ -377,7 +377,7 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
}
con.PrintEventErrorf("%s %s has been burned (DNS Canary)", StyleBold.Render("WARNING:"), event.Session.Name)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
shortID, _, _ := strings.Cut(session.ID, "-")
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
}
@@ -398,7 +398,7 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
}
con.PrintEventErrorf("%s %s has been burned (seen on %s)", StyleBold.Render("WARNING:"), event.Session.Name, msg)
for _, session := range sessions {
shortID := strings.Split(session.ID, "-")[0]
shortID, _, _ := strings.Cut(session.ID, "-")
con.PrintErrorf("\t🔥 Session %s is affected", shortID)
}
@@ -418,20 +418,20 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
case consts.SessionOpenedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
shortID, _, _ := strings.Cut(session.ID, "-")
con.emitConsoleNotification("info", true, "Session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
case consts.SessionUpdateEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
shortID, _, _ := strings.Cut(session.ID, "-")
con.emitConsoleNotification("info", false, "Session %s has been updated - %v", shortID, currentTime)
case consts.SessionClosedEvent:
session := event.Session
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(session.ID, "-")[0]
shortID, _, _ := strings.Cut(session.ID, "-")
con.emitConsoleNotification("error", true, "Lost session %s %s - %s (%s) - %s/%s - %v",
shortID, session.Name, session.RemoteAddress, session.Hostname, session.OS, session.Arch, currentTime)
activeSession := con.ActiveTarget.GetSession()
@@ -446,7 +446,7 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
beacon := &clientpb.Beacon{}
proto.Unmarshal(event.Data, beacon)
currentTime := time.Now().Format(time.RFC1123)
shortID := strings.Split(beacon.ID, "-")[0]
shortID, _, _ := strings.Cut(beacon.ID, "-")
con.emitConsoleNotification("info", true, "Beacon %s %s - %s (%s) - %s/%s - %v",
shortID, beacon.Name, beacon.RemoteAddress, beacon.Hostname, beacon.OS, beacon.Arch, currentTime)
@@ -462,7 +462,7 @@ func (con *SliverClient) startEventLoop(ctx context.Context, rpc rpcpb.SliverRPC
// CreateEventListener - creates a new event listener and returns its ID.
func (con *SliverClient) CreateEventListener() (string, <-chan *clientpb.Event) {
listener := make(chan *clientpb.Event, 100)
listenerID, _ := uuid.NewV4()
listenerID := uuid.NewV4()
con.EventListeners.Store(listenerID.String(), listener)
return listenerID.String(), listener
}
+3 -6
View File
@@ -26,6 +26,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
@@ -37,7 +38,6 @@ import (
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/moloch--/asciicast"
"golang.org/x/exp/slog"
"golang.org/x/term"
)
@@ -275,11 +275,8 @@ func buildStdoutSyncFrame(marker []byte, seq uint64) []byte {
}
func pendingMarkerPrefixLen(pending []byte, marker []byte) int {
max := len(marker) - 1
if max > len(pending) {
max = len(pending)
}
for size := max; size > 0; size-- {
maxPrefix := min(len(marker)-1, len(pending))
for size := maxPrefix; size > 0; size-- {
if bytes.Equal(pending[len(pending)-size:], marker[:size]) {
return size
}
+2 -3
View File
@@ -21,6 +21,7 @@ package console
import (
"bytes"
"fmt"
"maps"
"strings"
"sync"
"text/template"
@@ -109,9 +110,7 @@ func toPromptPalette(p theme.Palette) promptPalette {
Default: p.Default,
Mods: map[int]string{},
}
for k, v := range p.Mods {
out.Mods[k] = v
}
maps.Copy(out.Mods, p.Mods)
return out
}
+199 -36
View File
@@ -21,12 +21,15 @@ package core
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
"unicode"
"github.com/bishopfox/sliver/client/tcpproxy"
"github.com/bishopfox/sliver/protobuf/clientpb"
@@ -45,6 +48,11 @@ var (
portfwdID = 0
)
const (
portfwdBindTimeout = 5 * time.Second
portfwdCloseTimeout = 5 * time.Second
)
// PortfwdMeta - Metadata about a portfwd listener
type PortfwdMeta struct {
ID int
@@ -91,13 +99,20 @@ func (f *portfwds) Add(tcpProxy *tcpproxy.Proxy, channelProxy *ChannelProxy) *Po
// Remove - Remove a TCP proxy instance
func (f *portfwds) Remove(portfwdID int) bool {
f.mutex.Lock()
defer f.mutex.Unlock()
if portfwd, ok := f.forwards[portfwdID]; ok {
portfwd.TCPProxy.Close()
portfwd, ok := f.forwards[portfwdID]
if ok {
delete(f.forwards, portfwdID)
return true
}
return false
f.mutex.Unlock()
if !ok {
return false
}
// Remove registry ownership before joining connection workers. A slow or
// broken peer must not hold the global inventory mutex and block unrelated
// proxy lifecycle operations.
portfwd.ChannelProxy.Stop()
_ = portfwd.TCPProxy.Close()
return true
}
// List - List all TCP proxy instances
@@ -123,42 +138,126 @@ type ChannelProxy struct {
RemoteAddr string
KeepAlivePeriod time.Duration
DialTimeout time.Duration
connectionsMutex sync.Mutex
connections map[net.Conn]struct{}
lifecycleCtx context.Context
lifecycleCancel context.CancelFunc
stopped bool
handlerWG sync.WaitGroup
}
// HandleConn - Handle a TCP connection
func (p *ChannelProxy) HandleConn(conn net.Conn) {
log.Printf("[tcpproxy] Handling new connection")
ctx := context.Background()
if conn == nil {
return
}
ctx, tracked := p.trackConnection(conn)
if !tracked {
_ = conn.Close()
return
}
defer p.handlerWG.Done()
defer p.untrackConnection(conn)
defer func() { _ = conn.Close() }()
var cancel context.CancelFunc
if p.DialTimeout >= 0 {
ctx, cancel = context.WithTimeout(ctx, p.dialTimeout())
}
tunnel, err := p.dialImplant(ctx)
if cancel != nil {
defer cancel()
}
tunnel, err := p.dialImplant(ctx)
if err != nil {
return
}
// Cleanup
defer func() {
conn.Close()
GetTunnels().Close(tunnel.ID)
errs := make(chan error, 2)
copyWG := sync.WaitGroup{}
copyWG.Add(2)
go func() {
defer copyWG.Done()
errs <- toImplantLoop(conn, tunnel)
}()
go func() {
defer copyWG.Done()
errs <- fromImplantLoop(conn, tunnel)
}()
errs := make(chan error, 1)
go toImplantLoop(conn, tunnel, errs)
go fromImplantLoop(conn, tunnel, errs)
// Block until error, then cleanup
err = <-errs
// The first completed direction or a receive-admission failure owns shutdown.
// Closing both endpoints wakes the peer direction; join it before allowing
// this handler to finish.
select {
case err = <-errs:
case <-tunnel.receiveFailed():
// A receive-overflow close normally allows queued final frames to
// drain. That is unsafe for a port forward whose local peer stopped
// reading: close the accepted socket so both copy loops wake and the
// bounded remote cleanup below can run.
_ = conn.Close()
err = <-errs
}
if err != nil {
log.Printf("[tcpproxy] Closing tunnel %d with error %s", tunnel.ID, err)
}
_ = conn.Close()
GetTunnels().CloseIf(tunnel)
copyWG.Wait()
if err := p.closeTunnel(tunnel); err != nil {
log.Printf("[tcpproxy] Failed to close tunnel %d: %v", tunnel.ID, err)
}
}
// HostPort - Returns the host and port of the TCP proxy
// Stop closes every connection accepted by this target and rejects handlers
// that race the listener shutdown performed by Portfwds.Remove.
func (p *ChannelProxy) Stop() {
p.connectionsMutex.Lock()
p.stopped = true
lifecycleCancel := p.lifecycleCancel
connections := make([]net.Conn, 0, len(p.connections))
for connection := range p.connections {
connections = append(connections, connection)
}
p.connectionsMutex.Unlock()
if lifecycleCancel != nil {
lifecycleCancel()
}
for _, connection := range connections {
_ = connection.Close()
}
p.handlerWG.Wait()
}
func (p *ChannelProxy) trackConnection(connection net.Conn) (context.Context, bool) {
p.connectionsMutex.Lock()
defer p.connectionsMutex.Unlock()
if p.stopped {
return nil, false
}
if p.lifecycleCtx == nil {
p.lifecycleCtx, p.lifecycleCancel = context.WithCancel(context.Background())
}
if p.connections == nil {
p.connections = map[net.Conn]struct{}{}
}
// Add is serialized with Stop's stopped transition. Once Stop releases this
// mutex, no later Add can race its Wait.
p.handlerWG.Add(1)
p.connections[connection] = struct{}{}
return p.lifecycleCtx, true
}
func (p *ChannelProxy) untrackConnection(connection net.Conn) {
p.connectionsMutex.Lock()
delete(p.connections, connection)
p.connectionsMutex.Unlock()
}
// HostPort returns the remote host and port of the TCP proxy. Invalid values
// retain the historical empty-host/8080 fallback for source and behavioral
// compatibility. New call sites that must reject malformed destinations should
// use ValidatedHostPort.
func (p *ChannelProxy) HostPort() (string, uint32) {
defaultPort := uint32(8080)
host, rawPort, err := net.SplitHostPort(p.RemoteAddr)
@@ -179,6 +278,30 @@ func (p *ChannelProxy) HostPort() (string, uint32) {
return host, port
}
// ValidatedHostPort validates and returns the configured remote destination.
func (p *ChannelProxy) ValidatedHostPort() (string, uint32, error) {
host, rawPort, err := net.SplitHostPort(p.RemoteAddr)
if err != nil {
return "", 0, fmt.Errorf("invalid remote target %q: %w", p.RemoteAddr, err)
}
if host == "" {
return "", 0, fmt.Errorf("invalid remote target %q: host is required", p.RemoteAddr)
}
if strings.IndexFunc(host, func(value rune) bool {
return unicode.IsSpace(value) || unicode.IsControl(value)
}) >= 0 {
return "", 0, fmt.Errorf("invalid remote target %q: host contains whitespace or control characters", p.RemoteAddr)
}
portNumber, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return "", 0, fmt.Errorf("invalid remote target %q: port must be a number from 1 to 65535", p.RemoteAddr)
}
if portNumber == 0 {
return "", 0, fmt.Errorf("invalid remote target %q: port must be a number from 1 to 65535", p.RemoteAddr)
}
return host, uint32(portNumber), nil
}
// Port - Returns the TCP port of the proxy
func (p *ChannelProxy) Port() uint32 {
_, port := p.HostPort()
@@ -191,7 +314,11 @@ func (p *ChannelProxy) Host() string {
return host
}
func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
func (p *ChannelProxy) dialImplant(ctx context.Context) (_ *TunnelIO, resultErr error) {
host, port, err := p.ValidatedHostPort()
if err != nil {
return nil, err
}
log.Printf("[tcpproxy] Dialing implant to create tunnel ...")
@@ -206,16 +333,34 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
log.Printf("[tcpproxy] Created new tunnel with id %d (session %s)", rpcTunnel.TunnelID, p.Session.ID)
tunnel := GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
setupComplete := false
defer func() {
if !setupComplete {
resultErr = errors.Join(resultErr, p.closeTunnel(tunnel))
}
}()
log.Printf("[tcpproxy] Binding tunnel to portfwd %d", p.Port())
bindTimer := time.NewTimer(portfwdBindTimeout)
defer bindTimer.Stop()
select {
case <-tunnel.Bound():
case <-tunnel.Done():
return nil, errors.New("port forward tunnel closed before it was bound")
case <-ctx.Done():
return nil, ctx.Err()
case <-bindTimer.C:
return nil, errors.New("timed out binding port forward tunnel")
}
log.Printf("[tcpproxy] Binding tunnel to portfwd %d", port)
portfwdResp, err := p.Rpc.Portfwd(ctx, &sliverpb.PortfwdReq{
Request: &commonpb.Request{
SessionID: p.Session.ID,
},
Host: p.Host(),
Port: p.Port(),
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
Host: host,
Port: port,
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
})
if err != nil {
@@ -223,17 +368,36 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
}
// Close tunnel in case of error on the implant side
if portfwdResp.Response != nil && portfwdResp.Response.Err != "" {
p.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnel.ID,
SessionID: p.Session.ID,
})
return nil, errors.New(portfwdResp.Response.Err)
}
log.Printf("Portfwd response: %v", portfwdResp)
select {
case <-tunnel.Done():
return nil, errors.New("port forward tunnel closed during setup")
default:
}
setupComplete = true
return tunnel, nil
}
func (p *ChannelProxy) closeTunnel(tunnel *TunnelIO) error {
if tunnel == nil {
return nil
}
GetTunnels().CloseIf(tunnel)
if p.Rpc == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), portfwdCloseTimeout)
defer cancel()
_, err := p.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
})
return err
}
// func (p *ChannelProxy) keepAlivePeriod() time.Duration {
// if p.KeepAlivePeriod != 0 {
// return p.KeepAlivePeriod
@@ -248,23 +412,22 @@ func (p *ChannelProxy) dialTimeout() time.Duration {
return 30 * time.Second
}
func toImplantLoop(conn net.Conn, tunnel *TunnelIO, errs chan<- error) {
func toImplantLoop(conn net.Conn, tunnel *TunnelIO) error {
if wc, ok := conn.(*tcpproxy.Conn); ok && len(wc.Peeked) > 0 {
if _, err := tunnel.Write(wc.Peeked); err != nil {
errs <- err
return
return err
}
wc.Peeked = nil
}
n, err := io.Copy(tunnel, conn)
log.Printf("[tcpproxy] Closing to-implant after %d byte(s)", n)
errs <- err
return err
}
func fromImplantLoop(conn net.Conn, tunnel *TunnelIO, errs chan<- error) {
func fromImplantLoop(conn net.Conn, tunnel *TunnelIO) error {
n, err := io.Copy(conn, tunnel)
log.Printf("[tcpproxy] Closing from-implant after %d byte(s)", n)
errs <- err
return err
}
func nextPortfwdID() int {
+822
View File
@@ -0,0 +1,822 @@
package core
import (
"bytes"
"context"
"errors"
"io"
"net"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/client/tcpproxy"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/grpc"
)
type portfwdCloseObservation struct {
request *sliverpb.Tunnel
hasDeadline bool
deadlineRemaining time.Duration
}
type portfwdLifecycleTestStream struct {
grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData]
sent chan *sliverpb.TunnelData
dataEntered chan struct{}
releaseData chan struct{}
dataEnteredOnce sync.Once
releaseDataOnce sync.Once
}
type portfwdBlockingAddr string
func (address portfwdBlockingAddr) Network() string { return "test" }
func (address portfwdBlockingAddr) String() string { return string(address) }
type portfwdBlockingConn struct {
readStarted chan struct{}
releaseRead chan struct{}
closed chan struct{}
readOnce sync.Once
releaseOnce sync.Once
closeOnce sync.Once
}
func newPortfwdBlockingConn() *portfwdBlockingConn {
return &portfwdBlockingConn{
readStarted: make(chan struct{}),
releaseRead: make(chan struct{}),
closed: make(chan struct{}),
}
}
func (connection *portfwdBlockingConn) Read([]byte) (int, error) {
connection.readOnce.Do(func() { close(connection.readStarted) })
<-connection.releaseRead
return 0, io.EOF
}
func (connection *portfwdBlockingConn) Write(data []byte) (int, error) {
select {
case <-connection.closed:
return 0, net.ErrClosed
default:
return len(data), nil
}
}
func (connection *portfwdBlockingConn) Close() error {
connection.closeOnce.Do(func() { close(connection.closed) })
return nil
}
func (connection *portfwdBlockingConn) LocalAddr() net.Addr {
return portfwdBlockingAddr("local")
}
func (connection *portfwdBlockingConn) RemoteAddr() net.Addr {
return portfwdBlockingAddr("remote")
}
func (connection *portfwdBlockingConn) SetDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) SetReadDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) SetWriteDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) release() {
connection.releaseOnce.Do(func() { close(connection.releaseRead) })
}
func (stream *portfwdLifecycleTestStream) Send(data *sliverpb.TunnelData) error {
if len(data.GetData()) > 0 && stream.dataEntered != nil {
stream.dataEnteredOnce.Do(func() { close(stream.dataEntered) })
<-stream.releaseData
}
stream.sent <- data
return nil
}
func (stream *portfwdLifecycleTestStream) releaseBlockedData() {
if stream.releaseData != nil {
stream.releaseDataOnce.Do(func() { close(stream.releaseData) })
}
}
type portfwdLifecycleTestRPC struct {
rpcpb.SliverRPCClient
createResponse *sliverpb.Tunnel
createErr error
portfwdResponse *sliverpb.Portfwd
portfwdErr error
portfwdCalls chan *sliverpb.PortfwdReq
closeCalls chan portfwdCloseObservation
createCalls chan *sliverpb.Tunnel
blockCreate bool
createEntered chan struct{}
createCanceled chan struct{}
createOnce sync.Once
cancelOnce sync.Once
}
func newPortfwdLifecycleTestRPC(tunnelID uint64, sessionID string) *portfwdLifecycleTestRPC {
return &portfwdLifecycleTestRPC{
createResponse: &sliverpb.Tunnel{TunnelID: tunnelID, SessionID: sessionID},
portfwdResponse: &sliverpb.Portfwd{Response: &commonpb.Response{}},
portfwdCalls: make(chan *sliverpb.PortfwdReq, 4),
closeCalls: make(chan portfwdCloseObservation, 4),
createCalls: make(chan *sliverpb.Tunnel, 4),
}
}
func (rpc *portfwdLifecycleTestRPC) CreateTunnel(ctx context.Context, request *sliverpb.Tunnel, _ ...grpc.CallOption) (*sliverpb.Tunnel, error) {
rpc.createCalls <- request
if rpc.blockCreate {
rpc.createOnce.Do(func() { close(rpc.createEntered) })
<-ctx.Done()
rpc.cancelOnce.Do(func() { close(rpc.createCanceled) })
return nil, ctx.Err()
}
return rpc.createResponse, rpc.createErr
}
func (rpc *portfwdLifecycleTestRPC) Portfwd(_ context.Context, request *sliverpb.PortfwdReq, _ ...grpc.CallOption) (*sliverpb.Portfwd, error) {
rpc.portfwdCalls <- request
return rpc.portfwdResponse, rpc.portfwdErr
}
func (rpc *portfwdLifecycleTestRPC) CloseTunnel(ctx context.Context, request *sliverpb.Tunnel, _ ...grpc.CallOption) (*commonpb.Empty, error) {
deadline, hasDeadline := ctx.Deadline()
observation := portfwdCloseObservation{
request: request,
hasDeadline: hasDeadline,
}
if hasDeadline {
observation.deadlineRemaining = time.Until(deadline)
}
rpc.closeCalls <- observation
return &commonpb.Empty{}, nil
}
func TestChannelProxyWaitsForTunnelBindBeforePortfwd(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(301)
sessionID = "portfwd-bind-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
type dialResult struct {
tunnel *TunnelIO
err error
}
result := make(chan dialResult, 1)
go func() {
tunnel, err := proxy.dialImplant(context.Background())
result <- dialResult{tunnel: tunnel, err: err}
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
select {
case <-rpc.portfwdCalls:
t.Fatal("Portfwd RPC ran before the tunnel bind acknowledgement")
case <-time.After(50 * time.Millisecond):
}
tunnel.markBound()
select {
case request := <-rpc.portfwdCalls:
if request.TunnelID != tunnelID {
t.Fatalf("Portfwd tunnel ID = %d, want %d", request.TunnelID, tunnelID)
}
case <-time.After(time.Second):
t.Fatal("Portfwd RPC did not run after tunnel bind acknowledgement")
}
select {
case outcome := <-result:
if outcome.err != nil {
t.Fatalf("dialImplant: %v", outcome.err)
}
if outcome.tunnel != tunnel {
t.Fatal("dialImplant returned a different tunnel generation")
}
if err := proxy.closeTunnel(outcome.tunnel); err != nil {
t.Fatalf("close test tunnel: %v", err)
}
case <-time.After(time.Second):
t.Fatal("dialImplant did not return after Portfwd response")
}
}
//nolint:gocyclo // Final payload ordering and both local/remote cleanup form one relay lifecycle.
func TestChannelProxyForwardsFinalPayloadBeforeCloseTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := &portfwdLifecycleTestStream{
sent: make(chan *sliverpb.TunnelData, 4),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
t.Cleanup(stream.releaseBlockedData)
GetTunnels().SetStream(stream)
const (
tunnelID = uint64(311)
sessionID = "portfwd-final-payload-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
t.Cleanup(func() {
_ = serverConnection.Close()
_ = clientConnection.Close()
})
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("final-payload handler did not establish its port forward")
}
payload := []byte("last local bytes before EOF")
localWriteDone := make(chan error, 1)
go func() {
_, err := clientConnection.Write(payload)
if closeErr := clientConnection.Close(); err == nil {
err = closeErr
}
localWriteDone <- err
}()
select {
case <-stream.dataEntered:
case <-time.After(time.Second):
t.Fatal("final payload did not enter the tunnel stream Send")
}
select {
case err := <-localWriteDone:
if err != nil {
t.Fatalf("write final local payload: %v", err)
}
case <-time.After(time.Second):
t.Fatal("local writer did not finish")
}
select {
case observation := <-rpc.closeCalls:
t.Fatalf("CloseTunnel overtook the blocked final stream Send: %+v", observation.request)
case <-time.After(50 * time.Millisecond):
}
select {
case <-handlerDone:
t.Fatal("port-forward handler returned before the final stream Send completed")
default:
}
stream.releaseBlockedData()
select {
case frame := <-stream.sent:
if frame.GetTunnelID() != tunnelID || !bytes.Equal(frame.GetData(), payload) {
t.Fatalf("final stream frame = %+v, want tunnel=%d data=%q", frame, tunnelID, payload)
}
case <-time.After(time.Second):
t.Fatal("final payload was not sent after the stream was released")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("port-forward handler did not finish after sending the final payload")
}
}
func TestChannelProxyRejectsInvalidRemoteBeforeCreateTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(308, "invalid-remote-session")
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "invalid-remote-session"},
RemoteAddr: "target.example:not-a-port",
}
tunnel, err := proxy.dialImplant(context.Background())
if err == nil {
t.Fatal("dialImplant accepted an invalid remote target")
}
if tunnel != nil {
t.Fatal("dialImplant returned a tunnel for an invalid remote target")
}
select {
case request := <-rpc.createCalls:
t.Fatalf("invalid remote target invoked CreateTunnel with %+v", request)
default:
}
if active := GetTunnels().Get(308); active != nil {
t.Fatal("invalid remote target registered a client tunnel")
}
}
//nolint:gocyclo // The compatibility table verifies both legacy and strict result contracts.
func TestChannelProxyHostPortCompatibilityAndValidatedTargets(t *testing.T) {
tests := []struct {
name string
remoteAddr string
wantHost string
wantPort uint32
}{
{name: "hostname", remoteAddr: "target.example:443", wantHost: "target.example", wantPort: 443},
{name: "IPv4", remoteAddr: "192.0.2.10:8080", wantHost: "192.0.2.10", wantPort: 8080},
{name: "IPv6", remoteAddr: "[2001:db8::10]:8443", wantHost: "2001:db8::10", wantPort: 8443},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
proxy := &ChannelProxy{RemoteAddr: test.remoteAddr}
host, port := proxy.HostPort()
if host != test.wantHost || port != test.wantPort {
t.Fatalf("HostPort(%q) = (%q, %d), want (%q, %d)", test.remoteAddr, host, port, test.wantHost, test.wantPort)
}
host, port, err := proxy.ValidatedHostPort()
if err != nil {
t.Fatalf("ValidatedHostPort(%q): %v", test.remoteAddr, err)
}
if host != test.wantHost || port != test.wantPort {
t.Fatalf("ValidatedHostPort(%q) = (%q, %d), want (%q, %d)", test.remoteAddr, host, port, test.wantHost, test.wantPort)
}
})
}
proxy := &ChannelProxy{RemoteAddr: "target.example:not-a-port"}
if host, port := proxy.HostPort(); host != "" || port != 8080 {
t.Fatalf("legacy HostPort invalid fallback = (%q, %d), want (\"\", 8080)", host, port)
}
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Fatalf("ValidatedHostPort invalid result = (%q, %d, %v), want empty/zero/error", host, port, err)
}
// HostPort historically allowed an empty host and returned its parsed port.
// Validation-sensitive setup rejects the same value through the new API.
proxy.RemoteAddr = ":80"
if host, port := proxy.HostPort(); host != "" || port != 80 {
t.Fatalf("legacy HostPort empty-host result = (%q, %d), want (\"\", 80)", host, port)
}
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Fatalf("ValidatedHostPort empty-host result = (%q, %d, %v), want empty/zero/error", host, port, err)
}
for _, remoteAddr := range []string{
"bad host:80",
"bad\thost:80",
"bad\nhost:80",
"[fe80::1%bad zone]:80",
} {
proxy.RemoteAddr = remoteAddr
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Errorf("ValidatedHostPort whitespace/control result for %q = (%q, %d, %v), want empty/zero/error", remoteAddr, host, port, err)
}
}
}
func TestChannelProxySetupFailureClosesClientAndServerTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(302)
sessionID = "portfwd-failure-session"
)
portfwdErr := errors.New("test Portfwd RPC failure")
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
rpc.portfwdErr = portfwdErr
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
result := make(chan error, 1)
go func() {
_, err := proxy.dialImplant(context.Background())
result <- err
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case err := <-result:
if !errors.Is(err, portfwdErr) {
t.Fatalf("dialImplant error = %v, want %v", err, portfwdErr)
}
case <-time.After(time.Second):
t.Fatal("dialImplant setup failure did not return")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("failed setup retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyImplantSetupErrorClosesClientAndServerTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(305)
sessionID = "portfwd-implant-failure-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
rpc.portfwdResponse.Response.Err = "target dial refused"
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
result := make(chan error, 1)
go func() {
_, err := proxy.dialImplant(context.Background())
result <- err
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case err := <-result:
if err == nil || err.Error() != "target dial refused" {
t.Fatalf("dialImplant error = %v, want target dial refused", err)
}
case <-time.After(time.Second):
t.Fatal("dialImplant implant failure did not return")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("implant setup failure retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyClosesAcceptedConnectionWhenCreateTunnelFails(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(303, "portfwd-create-failure")
rpc.createErr = errors.New("test CreateTunnel failure")
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "portfwd-create-failure"},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatal(err)
}
buffer := make([]byte, 1)
count, err := clientConnection.Read(buffer)
if count != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("accepted connection read = (%d, %v), want (0, EOF)", count, err)
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return after CreateTunnel failure")
}
}
func TestChannelProxyStopCancelsCreateTunnelWithoutDialTimeout(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(306, "portfwd-cancel-create")
rpc.blockCreate = true
rpc.createEntered = make(chan struct{})
rpc.createCanceled = make(chan struct{})
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "portfwd-cancel-create"},
RemoteAddr: "127.0.0.1:8080",
DialTimeout: -1,
}
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
select {
case <-rpc.createEntered:
case <-time.After(time.Second):
t.Fatal("CreateTunnel did not start")
}
proxy.Stop()
select {
case <-rpc.createCanceled:
case <-time.After(time.Second):
t.Fatal("Stop did not cancel the in-flight CreateTunnel RPC")
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return after Stop canceled CreateTunnel")
}
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err == nil {
buffer := make([]byte, 1)
count, readErr := clientConnection.Read(buffer)
if count != 0 || (!errors.Is(readErr, io.EOF) && !errors.Is(readErr, io.ErrClosedPipe)) {
t.Fatalf("connection read after Stop = (%d, %v), want (0, EOF or closed pipe)", count, readErr)
}
} else if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after Stop: %v", err)
}
}
func TestChannelProxyStopWaitsForBlockedHandler(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(309)
sessionID = "portfwd-stop-join-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
connection := newPortfwdBlockingConn()
t.Cleanup(connection.release)
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(connection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("blocked handler did not establish its port forward")
}
select {
case <-connection.readStarted:
case <-time.After(time.Second):
t.Fatal("handler copy loop did not enter the blocking connection read")
}
stopDone := make(chan struct{})
go func() {
proxy.Stop()
close(stopDone)
}()
select {
case <-connection.closed:
case <-time.After(time.Second):
t.Fatal("Stop did not close the tracked connection")
}
select {
case <-stopDone:
t.Fatal("Stop returned before the blocked handler copy loop joined")
case <-time.After(50 * time.Millisecond):
}
connection.release()
select {
case <-stopDone:
case <-time.After(time.Second):
t.Fatal("Stop did not return after the blocked handler was released")
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return before Stop completed")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("joined handler retained its client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyReceiveOverflowClosesLocalAndRemoteTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(310)
sessionID = "portfwd-overflow-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
t.Cleanup(func() { _ = clientConnection.Close() })
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("overflow test did not establish its port forward")
}
var admissionErr error
for index := 0; index < tunnelRecvBufferSize+2; index++ {
admissionErr = tunnel.RecvData([]byte{byte(index)})
if admissionErr != nil {
break
}
}
if !errors.Is(admissionErr, errTunnelReceiveQueueFull) {
t.Fatalf("receive overflow error = %v, want %v", admissionErr, errTunnelReceiveQueueFull)
}
tunnel.failReceive()
GetTunnels().CloseIf(tunnel)
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err == nil {
for {
if _, err := clientConnection.Read(make([]byte, 1)); err != nil {
break
}
}
} else if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after receive overflow: %v", err)
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("receive overflow did not stop the port-forward handler")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestPortfwdsRemoveClosesEstablishedConnectionAndTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(304)
sessionID = "portfwd-remove-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
channelProxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
registered := Portfwds.Add(&tcpproxy.Proxy{}, channelProxy)
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
channelProxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("established connection did not invoke Portfwd")
}
if !Portfwds.Remove(registered.ID) {
t.Fatal("Portfwds.Remove did not remove the registered proxy")
}
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
// net.Pipe may report the peer teardown while setting the deadline,
// before a subsequent Read can observe EOF.
if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after remove: %v", err)
}
} else {
buffer := make([]byte, 1)
count, err := clientConnection.Read(buffer)
if count != 0 || (!errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe)) {
t.Fatalf("established connection read after remove = (%d, %v), want (0, EOF or closed pipe)", count, err)
}
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("established handler did not stop after Portfwds.Remove")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("Portfwds.Remove retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyStaleCloseDoesNotCloseReplacementTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const tunnelID = uint64(307)
rpc := newPortfwdLifecycleTestRPC(tunnelID, "old-session")
proxy := &ChannelProxy{Rpc: rpc}
old := GetTunnels().Start(tunnelID, "old-session")
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
replacement := GetTunnels().Start(tunnelID, "new-session")
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
if err := proxy.closeTunnel(old); err != nil {
t.Fatalf("close stale port forward tunnel: %v", err)
}
if current := GetTunnels().Get(tunnelID); current != replacement {
t.Fatal("stale port forward cleanup removed the replacement tunnel")
}
select {
case <-replacement.Done():
t.Fatal("stale port forward cleanup closed the replacement tunnel")
default:
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, "old-session")
}
func resetPortfwdLifecycleTestState(t *testing.T) {
t.Helper()
for _, metadata := range Portfwds.List() {
Portfwds.Remove(metadata.ID)
}
GetTunnels().Reset()
t.Cleanup(func() {
for _, metadata := range Portfwds.List() {
Portfwds.Remove(metadata.ID)
}
GetTunnels().Reset()
})
}
func waitForPortfwdLifecycleTunnel(t *testing.T, tunnelID uint64) *TunnelIO {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if tunnel := GetTunnels().Get(tunnelID); tunnel != nil {
return tunnel
}
time.Sleep(time.Millisecond)
}
t.Fatalf("timed out waiting for client tunnel %d", tunnelID)
return nil
}
func installPortfwdLifecycleTestStream() *portfwdLifecycleTestStream {
stream := &portfwdLifecycleTestStream{sent: make(chan *sliverpb.TunnelData, 4)}
GetTunnels().SetStream(stream)
return stream
}
func waitForPortfwdLifecycleBindFrame(t *testing.T, stream *portfwdLifecycleTestStream, tunnelID uint64) {
t.Helper()
select {
case frame := <-stream.sent:
if frame.GetTunnelID() != tunnelID || len(frame.GetData()) != 0 {
t.Fatalf("bind frame = %+v, want empty data for tunnel %d", frame, tunnelID)
}
case <-time.After(time.Second):
t.Fatalf("timed out waiting for tunnel %d bind frame", tunnelID)
}
}
func assertBoundedPortfwdClose(t *testing.T, observations <-chan portfwdCloseObservation, tunnelID uint64, sessionID string) {
t.Helper()
select {
case observation := <-observations:
if observation.request.GetTunnelID() != tunnelID || observation.request.GetSessionID() != sessionID {
t.Fatalf("CloseTunnel request = %+v, want tunnel=%d session=%q", observation.request, tunnelID, sessionID)
}
if !observation.hasDeadline {
t.Fatal("CloseTunnel RPC context has no deadline")
}
if observation.deadlineRemaining <= 0 || observation.deadlineRemaining > portfwdCloseTimeout {
t.Fatalf("CloseTunnel deadline remaining = %v, want within (0, %v]", observation.deadlineRemaining, portfwdCloseTimeout)
}
case <-time.After(time.Second):
t.Fatal("CloseTunnel RPC was not called")
}
}
+967 -109
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+39 -9
View File
@@ -25,6 +25,8 @@ import (
"log"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// TunnelLoop - Parses incoming tunnel messages and distributes them
@@ -32,6 +34,18 @@ import (
// to session/tunnel objects
// Expected to be called only once during initialization
func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
return tunnelLoop(ctx, rpc, nil)
}
// TunnelLoopWithReady runs TunnelLoop and closes ready after the tunnel data
// stream has been installed. Callers can use that signal to avoid accepting
// local proxy connections before tunnel bind frames can be sent.
func TunnelLoopWithReady(ctx context.Context, rpc rpcpb.SliverRPCClient, ready chan<- struct{}) error {
return tunnelLoop(ctx, rpc, ready)
}
//nolint:gocyclo // The shared stream loop keeps receive, ownership, close, and cancellation decisions together.
func tunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient, ready chan<- struct{}) error {
log.Println("Starting tunnel data loop ...")
defer log.Printf("Warning: TunnelLoop exited")
@@ -44,26 +58,42 @@ func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
tunnels := GetTunnels()
streamGeneration := tunnels.SetStream(stream)
defer tunnels.CloseStream(streamGeneration)
if ready != nil {
close(ready)
}
for {
log.Printf("Waiting for TunnelData ...")
incoming, err := stream.Recv()
log.Printf("Recv stream msg: %v", incoming)
log.Printf("Recv stream err: %s", err)
if incoming != nil {
log.Printf(
"Recv stream msg: tunnel=%d session=%s bytes=%d closed=%t",
incoming.TunnelID,
incoming.SessionID,
len(incoming.Data),
incoming.Closed,
)
}
if err != nil {
log.Printf("Recv stream err: %s", err)
}
if err == io.EOF {
log.Printf("EOF Error: Tunnel data stream closed")
return nil
}
if err != nil {
if errors.Is(err, context.Canceled) {
// A canceled status is only a clean shutdown when this caller
// canceled the stream. A remote/server cancellation while ctx is
// still live is a real tunnel-loop failure and must be surfaced.
if ctx.Err() != nil && (errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
return nil
}
log.Printf("Tunnel data read error: %s", err)
return err
}
log.Printf("Received TunnelData for tunnel %d", incoming.TunnelID)
tunnel := GetTunnels().Get(incoming.TunnelID)
tunnel := tunnels.Get(incoming.TunnelID)
if tunnel != nil {
if !incoming.Closed && len(incoming.GetData()) == 0 {
@@ -76,18 +106,18 @@ func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
}
data := incoming.GetData()
log.Printf("This is data on tunnel %d: %s", tunnel.ID, data)
if !incoming.Closed {
log.Printf("Received data on tunnel %d", tunnel.ID)
log.Printf("Received %d byte(s) on tunnel %d", len(data), tunnel.ID)
err = tunnel.RecvData(data)
if err != nil {
log.Printf("Warning! Error sending data to tunnel %d, %v", tunnel.ID, err)
log.Printf("Warning! Closing tunnel %d after receive admission failed: %v", tunnel.ID, err)
tunnel.failReceive()
tunnels.CloseIf(tunnel)
}
} else {
log.Printf("Closing tunnel %d", tunnel.ID)
GetTunnels().Close(tunnel.ID)
tunnels.CloseIf(tunnel)
}
} else {
log.Printf("Received tunnel data for non-existent tunnel id %d", incoming.TunnelID)
+176 -38
View File
@@ -19,16 +19,33 @@ package core
*/
import (
"bytes"
"errors"
"io"
"log"
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
const tunnelRecvBufferSize = 16
const (
// HTTP C2 may deliver up to 64 concurrent responses while a command-specific
// reader is starting or briefly backpressured. Match the 128-frame generic
// server/implant transport window so that ordinary bursts remain isolated and
// bounded instead of resetting an otherwise healthy port forward.
tunnelRecvBufferSize = 128
tunnelRecvByteLimit = tunnelRecvBufferSize * sliverpb.MaxTunnelFrameBytes
)
var errClosedTunnel = errors.New("closed tunnel")
var (
errClosedTunnel = errors.New("closed tunnel")
errTunnelReceiveQueueFull = errors.New("tunnel receive queue capacity exceeded")
errTunnelReceiveFrameTooLarge = errors.New("tunnel receive frame exceeds payload limit")
)
type tunnelWriteRequest struct {
data []byte
result chan error
}
// TunnelIO - Duplex data tunnel, compatible with both io.ReadWriter
type TunnelIO struct {
@@ -38,12 +55,26 @@ type TunnelIO struct {
Send chan []byte
Recv chan []byte
done chan struct{}
closeOnce sync.Once
bound chan struct{}
boundOnce sync.Once
isOpen bool
mutex *sync.RWMutex
writeRequests chan tunnelWriteRequest
done chan struct{}
closeOnce sync.Once
recvFailed chan struct{}
recvFailedOnce sync.Once
bound chan struct{}
boundOnce sync.Once
readMu sync.Mutex
remainder []byte
recvBudgetMu sync.Mutex
recvFrames int
recvBytes int
recvFrameLimit int
recvByteLimit int
isOpen bool
mutex *sync.RWMutex
// readSelectHook is an unexported scheduling seam for deterministic tests of
// the close-vs-final-frame select. Production instances leave it nil.
readSelectHook func(doneSelected bool)
}
// NewTunnelIO - Single entry point for creating instance of new TunnelIO
@@ -57,15 +88,22 @@ func NewTunnelIO(tunnelID uint64, sessionID string) *TunnelIO {
// A tunnel can receive its first frames before the command-specific
// reader is running. Keep that short startup burst from blocking the
// single client TunnelLoop while retaining bounded backpressure.
Recv: make(chan []byte, tunnelRecvBufferSize),
done: make(chan struct{}),
bound: make(chan struct{}),
mutex: &sync.RWMutex{},
Recv: make(chan []byte, tunnelRecvBufferSize),
writeRequests: make(chan tunnelWriteRequest),
done: make(chan struct{}),
recvFailed: make(chan struct{}),
bound: make(chan struct{}),
recvFrameLimit: tunnelRecvBufferSize,
recvByteLimit: tunnelRecvByteLimit,
mutex: &sync.RWMutex{},
}
}
// Write - Writer method for interface
func (tun *TunnelIO) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
if !tun.IsOpen() {
log.Printf("Warning: Write on closed tunnel %d", tun.ID)
return 0, io.EOF
@@ -75,8 +113,7 @@ func (tun *TunnelIO) Write(data []byte) (int, error) {
dataCopy := make([]byte, len(data))
n := copy(dataCopy, data)
log.Printf("Write %d bytes", n)
log.Printf("This bytes is: %s", dataCopy)
log.Printf("Write %d byte(s) on tunnel %d", n, tun.ID)
select {
case <-tun.done:
@@ -84,16 +121,49 @@ func (tun *TunnelIO) Write(data []byte) (int, error) {
default:
}
request := tunnelWriteRequest{
data: dataCopy,
result: make(chan error, 1),
}
select {
case tun.Send <- dataCopy:
return n, nil
case tun.writeRequests <- request:
case <-tun.done:
return 0, io.EOF
}
// A successful Write means the exact tunnel generation's gRPC stream Send
// completed. This prevents a full-close owner from deleting the generation
// after a channel handoff but before the final payload reaches the stream.
select {
case err := <-request.result:
if err != nil {
return 0, err
}
return n, nil
case <-tun.done:
// Prefer an already-published Send result when completion and close race.
select {
case err := <-request.result:
if err != nil {
return 0, err
}
return n, nil
default:
return 0, io.EOF
}
}
}
// Read - Reader method for interface
func (tun *TunnelIO) Read(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
tun.readMu.Lock()
defer tun.readMu.Unlock()
if len(tun.remainder) > 0 {
return tun.copyRecvData(data, nil), nil
}
// Prefer already queued output over the close signal so callers can drain
// the final frames before observing EOF.
select {
@@ -102,21 +172,53 @@ func (tun *TunnelIO) Read(data []byte) (int, error) {
default:
}
if tun.readSelectHook != nil {
tun.readSelectHook(false)
}
select {
case recvData := <-tun.Recv:
return tun.copyRecvData(data, recvData), nil
case <-tun.done:
log.Printf("Warning: Read on closed tunnel %d", tun.ID)
return 0, io.EOF
if tun.readSelectHook != nil {
tun.readSelectHook(true)
}
// RecvData that won admission immediately before Close may have queued
// after the first poll. Prefer that final admitted frame over EOF.
select {
case recvData := <-tun.Recv:
return tun.copyRecvData(data, recvData), nil
default:
log.Printf("Warning: Read on closed tunnel %d", tun.ID)
return 0, io.EOF
}
}
}
func (tun *TunnelIO) copyRecvData(data []byte, recvData []byte) int {
var buff bytes.Buffer
log.Printf("Read %d bytes", len(recvData))
buff.Write(recvData)
if recvData != nil {
tun.remainder = recvData
log.Printf("Read frame with %d byte(s) on tunnel %d", len(recvData), tun.ID)
}
n := copy(data, tun.remainder)
tun.remainder = tun.remainder[n:]
tun.releaseRecvBudget(n, len(tun.remainder) == 0)
if len(tun.remainder) == 0 {
tun.remainder = nil
}
return n
}
return copy(data, buff.Bytes())
func (tun *TunnelIO) releaseRecvBudget(bytes int, frameComplete bool) {
tun.recvBudgetMu.Lock()
if bytes >= tun.recvBytes {
tun.recvBytes = 0
} else {
tun.recvBytes -= bytes
}
if frameComplete && tun.recvFrames > 0 {
tun.recvFrames--
}
tun.recvBudgetMu.Unlock()
}
// Close - Close tunnel IO operations
@@ -147,6 +249,20 @@ func (tun *TunnelIO) Done() <-chan struct{} {
return tun.done
}
// receiveFailed is closed only for local receive-admission failure. Ordinary
// peer close leaves it open so owners can still drain already admitted final
// frames before closing their local endpoint.
func (tun *TunnelIO) receiveFailed() <-chan struct{} {
return tun.recvFailed
}
func (tun *TunnelIO) failReceive() {
tun.recvFailedOnce.Do(func() {
close(tun.recvFailed)
})
_ = tun.Close()
}
// Bound is closed after the server acknowledges the tunnel's streaming bind.
func (tun *TunnelIO) Bound() <-chan struct{} {
return tun.bound
@@ -185,23 +301,45 @@ func (tun *TunnelIO) Open() error {
return nil
}
// RecvData - safely queues data on the bounded internal Recv channel.
// It blocks only when that startup buffer is full.
// RecvData admits data to the bounded receive queue without blocking the
// process-wide TunnelLoop. The reservation includes unread short-buffer
// remainder, so both the frame count and byte footprint remain bounded.
func (tun *TunnelIO) RecvData(data []byte) error {
if !tun.IsOpen() {
return errClosedTunnel
}
select {
case <-tun.done:
return errClosedTunnel
default:
}
select {
case tun.Recv <- data:
if len(data) == 0 {
return nil
case <-tun.done:
}
if len(data) > sliverpb.MaxTunnelFrameBytes {
return errTunnelReceiveFrameTooLarge
}
tun.mutex.RLock()
defer tun.mutex.RUnlock()
if !tun.isOpen {
return errClosedTunnel
}
return tun.queueRecvData(data)
}
// queueRecvData completes admission after the caller has established that the
// tunnel is open. Keeping this step separate also lets tests hold the Read done
// branch at the exact historical race point without relying on scheduler luck.
func (tun *TunnelIO) queueRecvData(data []byte) error {
dataCopy := append([]byte(nil), data...)
tun.recvBudgetMu.Lock()
defer tun.recvBudgetMu.Unlock()
if tun.recvFrames >= tun.recvFrameLimit || tun.recvBytes+len(dataCopy) > tun.recvByteLimit {
return errTunnelReceiveQueueFull
}
tun.recvFrames++
tun.recvBytes += len(dataCopy)
select {
case tun.Recv <- dataCopy:
return nil
default:
tun.recvFrames--
tun.recvBytes -= len(dataCopy)
return errTunnelReceiveQueueFull
}
}
+183 -35
View File
@@ -4,8 +4,11 @@ import (
"bytes"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestTunnelIOBuffersDataBeforeReaderStarts(t *testing.T) {
@@ -46,56 +49,108 @@ func TestTunnelIOBuffersDataBeforeReaderStarts(t *testing.T) {
}
}
func TestTunnelIOCloseUnblocksFullReceiveBuffer(t *testing.T) {
func TestTunnelIOAdmitsHTTPTransportBurstAndDrainsInOrder(t *testing.T) {
const (
httpBurstFrames = 64
httpFrameBytes = 32 * 1024
)
tunnel := NewTunnelIO(10, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
// HTTP C2 may make this entire response window runnable before the local
// port-forward reader is scheduled. Admission must remain nonblocking and
// preserve every frame rather than treating that ordinary burst as overload.
for sequence := 0; sequence < httpBurstFrames; sequence++ {
payload := bytes.Repeat([]byte{byte(sequence)}, httpFrameBytes)
if err := tunnel.RecvData(payload); err != nil {
t.Fatalf("admit HTTP burst frame %d: %v", sequence, err)
}
}
if !tunnel.IsOpen() {
t.Fatal("ordinary HTTP response burst closed the tunnel")
}
buffer := make([]byte, httpFrameBytes)
for sequence := 0; sequence < httpBurstFrames; sequence++ {
n, err := tunnel.Read(buffer)
if err != nil {
t.Fatalf("drain HTTP burst frame %d: %v", sequence, err)
}
if n != len(buffer) {
t.Fatalf("drain HTTP burst frame %d length = %d, want %d", sequence, n, len(buffer))
}
want := byte(sequence)
for offset, got := range buffer {
if got != want {
t.Fatalf("drain HTTP burst frame %d byte %d = %d, want %d", sequence, offset, got, want)
}
}
}
tunnel.recvBudgetMu.Lock()
frames, size := tunnel.recvFrames, tunnel.recvBytes
tunnel.recvBudgetMu.Unlock()
if frames != 0 || size != 0 {
t.Fatalf("receive budget after HTTP burst drain = (%d frames, %d bytes), want zero", frames, size)
}
}
func TestTunnelIORejectsFullReceiveBufferWithoutBlocking(t *testing.T) {
tunnel := NewTunnelIO(2, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
for i := 0; i < tunnelRecvBufferSize; i++ {
recvDone := make(chan error, 1)
go func() {
recvDone <- tunnel.RecvData([]byte{byte(i)})
}()
select {
case err := <-recvDone:
if err != nil {
t.Fatalf("fill receive buffer frame %d: %v", i, err)
}
case <-time.After(time.Second):
t.Fatalf("frame %d blocked before receive buffer was full", i)
if err := tunnel.RecvData([]byte{byte(i)}); err != nil {
t.Fatalf("fill receive buffer frame %d: %v", i, err)
}
}
blockedRecv := make(chan error, 1)
recvDone := make(chan error, 1)
go func() {
blockedRecv <- tunnel.RecvData([]byte("seventeenth frame"))
recvDone <- tunnel.RecvData([]byte("frame beyond receive window"))
}()
select {
case err := <-blockedRecv:
t.Fatalf("frame beyond bounded receive capacity returned before close: %v", err)
case <-time.After(50 * time.Millisecond):
case err := <-recvDone:
if !errors.Is(err, errTunnelReceiveQueueFull) {
t.Fatalf("frame beyond bounded receive capacity = %v, want %v", err, errTunnelReceiveQueueFull)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("frame beyond bounded receive capacity blocked")
}
}
func TestTunnelIOReceiveAdmissionBoundsBytesAndFrameSize(t *testing.T) {
tunnel := NewTunnelIO(8, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
// Use a small budget to exercise byte admission independently of the
// production frame-count window.
tunnel.recvByteLimit = 3
if err := tunnel.RecvData([]byte("abc")); err != nil {
t.Fatalf("fill receive byte budget: %v", err)
}
if err := tunnel.RecvData([]byte("d")); !errors.Is(err, errTunnelReceiveQueueFull) {
t.Fatalf("frame beyond receive byte budget = %v, want %v", err, errTunnelReceiveQueueFull)
}
closeDone := make(chan error, 1)
go func() {
closeDone <- tunnel.Close()
}()
select {
case err := <-closeDone:
if err != nil {
t.Fatalf("close full tunnel: %v", err)
}
case <-time.After(time.Second):
t.Fatal("Close blocked behind RecvData on a full receive buffer")
buffer := make([]byte, 3)
if n, err := tunnel.Read(buffer); n != 3 || err != nil || string(buffer) != "abc" {
t.Fatalf("read byte-budget frame = (%d, %q, %v)", n, buffer, err)
}
select {
case err := <-blockedRecv:
if !errors.Is(err, errClosedTunnel) {
t.Fatalf("blocked RecvData error = %v, want %v", err, errClosedTunnel)
}
case <-time.After(time.Second):
t.Fatal("blocked RecvData did not wake when tunnel closed")
if err := tunnel.RecvData([]byte("d")); err != nil {
t.Fatalf("admit after byte budget released: %v", err)
}
if err := tunnel.RecvData(make([]byte, sliverpb.MaxTunnelFrameBytes+1)); !errors.Is(err, errTunnelReceiveFrameTooLarge) {
t.Fatalf("oversized receive frame = %v, want %v", err, errTunnelReceiveFrameTooLarge)
}
}
@@ -203,3 +258,96 @@ func TestTunnelIOCloseBeforeOpenIsTerminal(t *testing.T) {
t.Fatal("tunnel reopened after terminal Close")
}
}
//nolint:gocyclo // Partial reads, terminal ordering, and EOF form one adapter contract.
func TestTunnelIOReadRetainsShortBufferRemainderAfterClose(t *testing.T) {
tunnel := NewTunnelIO(7, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
if n, err := tunnel.Read(make([]byte, 0)); n != 0 || err != nil {
t.Fatalf("zero-length read = (%d, %v), want (0, nil)", n, err)
}
if n, err := tunnel.Write(nil); n != 0 || err != nil {
t.Fatalf("zero-length write = (%d, %v), want (0, nil)", n, err)
}
select {
case data := <-tunnel.Send:
t.Fatalf("zero-length write queued data %x", data)
default:
}
if err := tunnel.RecvData([]byte("abcdef")); err != nil {
t.Fatalf("queue short-buffer payload: %v", err)
}
first := make([]byte, 2)
if n, err := tunnel.Read(first); n != 2 || err != nil || string(first) != "ab" {
t.Fatalf("first short read = (%d, %q, %v), want (2, ab, nil)", n, first, err)
}
if err := tunnel.Close(); err != nil {
t.Fatalf("close tunnel with remainder: %v", err)
}
second := make([]byte, 4)
if n, err := tunnel.Read(second); n != 4 || err != nil || string(second) != "cdef" {
t.Fatalf("remainder read = (%d, %q, %v), want (4, cdef, nil)", n, second, err)
}
if n, err := tunnel.Read(make([]byte, 1)); n != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("post-remainder read = (%d, %v), want (0, EOF)", n, err)
}
}
func TestTunnelIOReadDrainsFrameAfterDoneCaseSelected(t *testing.T) {
tunnel := NewTunnelIO(9, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
selectReady := make(chan struct{})
doneSelected := make(chan struct{})
releaseDoneBranch := make(chan struct{})
var readyOnce sync.Once
var doneOnce sync.Once
tunnel.readSelectHook = func(selected bool) {
if !selected {
readyOnce.Do(func() { close(selectReady) })
return
}
doneOnce.Do(func() { close(doneSelected) })
<-releaseDoneBranch
}
type readResult struct {
data []byte
n int
err error
}
readDone := make(chan readResult, 1)
go func() {
buffer := make([]byte, 32)
n, err := tunnel.Read(buffer)
readDone <- readResult{data: buffer[:n], n: n, err: err}
}()
waitForTestSignal(t, selectReady, "tunnel Read blocking select")
if err := tunnel.Close(); err != nil {
t.Fatalf("close tunnel: %v", err)
}
waitForTestSignal(t, doneSelected, "tunnel Read done case")
payload := []byte("last admitted frame")
if err := tunnel.queueRecvData(payload); err != nil {
t.Fatalf("queue final admitted frame: %v", err)
}
close(releaseDoneBranch)
select {
case result := <-readDone:
if result.n != len(payload) || result.err != nil || !bytes.Equal(result.data, payload) {
t.Fatalf("Read after done case = (%d, %q, %v), want (%d, %q, nil)", result.n, result.data, result.err, len(payload), payload)
}
case <-time.After(time.Second):
t.Fatal("Read did not drain the final admitted frame")
}
tunnel.readSelectHook = nil
if n, err := tunnel.Read(make([]byte, 1)); n != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("Read after final drain = (%d, %v), want (0, EOF)", n, err)
}
}
+217 -3
View File
@@ -11,17 +11,23 @@ import (
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type tunnelLoopTestRPC struct {
rpcpb.SliverRPCClient
stream *tunnelLoopTestStream
opened chan struct{}
stream *tunnelLoopTestStream
opened chan struct{}
startErr error
}
func (r *tunnelLoopTestRPC) TunnelData(ctx context.Context, _ ...grpc.CallOption) (grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData], error) {
r.stream.ctx = ctx
close(r.opened)
if r.startErr != nil {
return nil, r.startErr
}
r.stream.ctx = ctx
return r.stream, nil
}
@@ -79,6 +85,57 @@ func TestTunnelBindSendFailureClosesTunnel(t *testing.T) {
}
}
func TestTunnelLoopWithReadySignalsAfterStreamInstallation(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
ready := make(chan struct{})
loopDone := make(chan error, 1)
go func() { loopDone <- TunnelLoopWithReady(ctx, rpc, ready) }()
t.Cleanup(func() {
cancel()
select {
case <-loopDone:
case <-time.After(time.Second):
t.Error("TunnelLoopWithReady did not stop after cancellation")
}
tunnels.Reset()
})
waitForTestSignal(t, ready, "TunnelLoop readiness")
tunnel := tunnels.Start(10, "session")
select {
case bind := <-stream.sent:
if bind.TunnelID != tunnel.ID {
t.Fatalf("bind tunnel ID = %d, want %d", bind.TunnelID, tunnel.ID)
}
case <-time.After(time.Second):
t.Fatal("ready tunnel loop did not send a bind frame")
}
}
func TestTunnelLoopWithReadyDoesNotSignalOnStartupFailure(t *testing.T) {
startErr := errors.New("test stream startup failure")
rpc := &tunnelLoopTestRPC{opened: make(chan struct{}), startErr: startErr}
ready := make(chan struct{})
err := TunnelLoopWithReady(context.Background(), rpc, ready)
if !errors.Is(err, startErr) {
t.Fatalf("TunnelLoopWithReady error = %v, want %v", err, startErr)
}
select {
case <-ready:
t.Fatal("readiness was signaled after stream startup failed")
default:
}
}
func (s *tunnelLoopTestStream) Recv() (*sliverpb.TunnelData, error) {
s.recvCalls <- struct{}{}
select {
@@ -180,6 +237,99 @@ func TestTunnelLoopFiltersEmptyBindAcknowledgement(t *testing.T) {
}
}
//nolint:gocyclo // The overload and sibling-isolation assertions share one stream lifecycle.
func TestTunnelLoopOverloadedTunnelDoesNotBlockHealthyTunnel(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData, tunnelRecvBufferSize+4),
sent: make(chan *sliverpb.TunnelData, 2),
recvCalls: make(chan struct{}, tunnelRecvBufferSize+8),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
loopDone := make(chan error, 1)
go func() { loopDone <- TunnelLoop(ctx, rpc) }()
t.Cleanup(func() {
cancel()
select {
case <-loopDone:
case <-time.After(time.Second):
t.Error("TunnelLoop did not stop after cancellation")
}
tunnels.Reset()
})
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
stalled := tunnels.Start(11, "session")
healthy := tunnels.Start(12, "session")
bound := map[uint64]bool{}
for len(bound) < 2 {
select {
case frame := <-stream.sent:
bound[frame.TunnelID] = true
case <-time.After(time.Second):
t.Fatal("client did not send both tunnel bind frames")
}
}
for sequence := 0; sequence < tunnelRecvBufferSize; sequence++ {
stream.incoming <- &sliverpb.TunnelData{
TunnelID: stalled.ID,
SessionID: stalled.SessionID,
Data: []byte{byte(sequence)},
}
waitForTestSignal(t, stream.recvCalls, "receive after stalled tunnel frame")
}
stream.incoming <- &sliverpb.TunnelData{
TunnelID: stalled.ID,
SessionID: stalled.SessionID,
Data: []byte("overflow"),
}
waitForTestSignal(t, stream.recvCalls, "receive after stalled tunnel overflow")
healthyPayload := []byte("healthy tunnel data")
stream.incoming <- &sliverpb.TunnelData{
TunnelID: healthy.ID,
SessionID: healthy.SessionID,
Data: healthyPayload,
}
waitForTestSignal(t, stream.recvCalls, "receive after healthy tunnel frame")
select {
case <-stalled.Done():
case <-time.After(time.Second):
t.Fatal("overloaded tunnel was not closed")
}
if got := tunnels.Get(stalled.ID); got != nil {
t.Fatal("overloaded tunnel remained registered")
}
type healthyReadResult struct {
data []byte
n int
err error
}
healthyRead := make(chan healthyReadResult, 1)
go func() {
buffer := make([]byte, len(healthyPayload))
n, err := healthy.Read(buffer)
healthyRead <- healthyReadResult{data: buffer[:n], n: n, err: err}
}()
select {
case result := <-healthyRead:
if result.n != len(healthyPayload) || result.err != nil || !bytes.Equal(result.data, healthyPayload) {
t.Fatalf("healthy tunnel read = (%d, %q, %v), want (%d, %q, nil)", result.n, result.data, result.err, len(healthyPayload), healthyPayload)
}
case <-time.After(time.Second):
t.Fatal("healthy tunnel read blocked behind overloaded tunnel")
}
if !healthy.IsOpen() {
t.Fatal("healthy tunnel closed after unrelated tunnel overload")
}
}
func TestTunnelLoopClosesActiveTunnelsOnStreamEOF(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
@@ -225,6 +375,70 @@ func TestTunnelLoopClosesActiveTunnelsOnStreamEOF(t *testing.T) {
}
}
func TestTunnelLoopPropagatesRemoteGRPCCanceled(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
recvErr: make(chan error, 1),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
loopDone := make(chan error, 1)
go func() {
loopDone <- TunnelLoop(context.Background(), rpc)
}()
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
remoteErr := status.Error(codes.Canceled, "test remote shutdown")
stream.recvErr <- remoteErr
select {
case err := <-loopDone:
if status.Code(err) != codes.Canceled {
t.Fatalf("TunnelLoop remote canceled status result = %v, want %v", err, remoteErr)
}
case <-time.After(time.Second):
t.Fatal("TunnelLoop did not return after remote canceled gRPC status")
}
}
func TestTunnelLoopTreatsCallerCancellationAsCleanShutdown(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
recvErr: make(chan error, 1),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
loopDone := make(chan error, 1)
go func() {
loopDone <- TunnelLoop(ctx, rpc)
}()
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
cancel()
select {
case err := <-loopDone:
if err != nil {
t.Fatalf("TunnelLoop caller cancellation result = %v, want nil", err)
}
case <-time.After(time.Second):
t.Fatal("TunnelLoop did not return after caller cancellation")
}
}
func waitForTestSignal(t *testing.T, signal <-chan struct{}, description string) {
t.Helper()
select {
+83 -29
View File
@@ -34,6 +34,8 @@ var (
tunnelsSingletonLock = &sync.Mutex{}
)
var errSupersededTunnel = errors.New("superseded tunnel")
// GetTunnels - singleton function that returns or initializes all tunnels
func GetTunnels() *tunnels {
tunnelsSingletonLock.Lock()
@@ -98,10 +100,20 @@ func (t *tunnels) Get(tunnelID uint64) *TunnelIO {
// send - safe way to send a message to the stream
// protobuf stream allow only one writer at a time, so just in case there is a mutex for it
func (t *tunnels) send(tunnelData *sliverpb.TunnelData) error {
func (t *tunnels) send(expected *TunnelIO, tunnelData *sliverpb.TunnelData) error {
t.streamMutex.Lock()
defer t.streamMutex.Unlock()
// Start publishes replacement tunnel generations while holding streamMutex.
// Checking the exact pointer under the same lock ensures an old generation
// cannot send another frame after its replacement becomes visible.
t.mutex.RLock()
current := (*t.tunnels)[expected.ID]
t.mutex.RUnlock()
if current != expected {
return errSupersededTunnel
}
if t.stream == nil {
return errors.New("uninitizlied stream")
}
@@ -113,39 +125,59 @@ func (t *tunnels) send(tunnelData *sliverpb.TunnelData) error {
// Start - Add a tunnel to the core mapper
func (t *tunnels) Start(tunnelID uint64, sessionID string) *TunnelIO {
t.mutex.Lock()
defer t.mutex.Unlock()
tunnel := NewTunnelIO(tunnelID, sessionID)
if err := tunnel.Open(); err != nil {
log.Printf("Failed to open tunnel %d: %v", tunnelID, err)
return tunnel
}
// Publish the new generation atomically with respect to tunnel sends. If an
// ID is reused, the previous generation is made terminal before the new
// pointer becomes visible in the map.
t.streamMutex.Lock()
t.mutex.Lock()
previous := (*t.tunnels)[tunnelID]
if previous != nil {
_ = previous.Close()
}
(*t.tunnels)[tunnelID] = tunnel
t.mutex.Unlock()
t.streamMutex.Unlock()
go func(tunnel *TunnelIO) {
if err := tunnel.Open(); err != nil {
log.Printf("Failed to open tunnel %d: %v", tunnelID, err)
return
}
log.Printf("Tunnel now is open, %d", tunnelID)
for {
var (
data []byte
result chan error
)
select {
case <-tunnel.Done():
log.Printf("Tunnel send loop stopped. %d", tunnelID)
return
case data := <-tunnel.Send:
log.Printf("Send %d bytes on tunnel %d", len(data), tunnel.ID)
case request := <-tunnel.writeRequests:
data = request.data
result = request.result
case data = <-tunnel.Send:
}
err := t.send(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
Data: data,
})
if err != nil {
log.Printf("Error sending, %s", err)
t.Close(tunnel.ID)
return
}
log.Printf("Send %d bytes on tunnel %d", len(data), tunnel.ID)
err := t.send(tunnel, &sliverpb.TunnelData{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
Data: data,
})
if result != nil {
// The result channel is buffered so a concurrent Close can wake the
// writer without stranding this exact-generation send loop. Publish
// the result before closing the tunnel on failure.
result <- err
}
if err != nil {
log.Printf("Error sending, %s", err)
t.CloseIf(tunnel)
return
}
}
}(tunnel)
@@ -167,26 +199,48 @@ func (t *tunnels) Close(tunnelID uint64) {
tunnel := (*t.tunnels)[tunnelID]
if tunnel != nil {
tunnel.Close()
_ = tunnel.Close()
delete((*t.tunnels), tunnelID)
}
}
// CloseIf closes and removes expected only when it is still the current
// generation registered for its numeric tunnel ID.
func (t *tunnels) CloseIf(expected *TunnelIO) bool {
if expected == nil {
return false
}
t.mutex.Lock()
defer t.mutex.Unlock()
current := (*t.tunnels)[expected.ID]
if current != expected {
return false
}
log.Printf("Closing tunnel %d", expected.ID)
_ = expected.Close()
delete(*t.tunnels, expected.ID)
return true
}
// CloseForSession - closing all tunnels for specified session id
func (t *tunnels) CloseForSession(sessionID string) {
t.mutex.RLock()
defer t.mutex.RUnlock()
log.Printf("Closing all tunnels for session %s", sessionID)
for tunnelID, tunnel := range *t.tunnels {
tunnels := []*TunnelIO{}
for _, tunnel := range *t.tunnels {
if tunnel.SessionID == sessionID {
// Weird way to avoid deadlocks but let it be
go func(tunnelID uint64) {
GetTunnels().Close(tunnelID)
}(tunnelID)
tunnels = append(tunnels, tunnel)
}
}
t.mutex.RUnlock()
for _, tunnel := range tunnels {
go t.CloseIf(tunnel)
}
}
// Reset closes all open tunnels and clears the underlying storage/stream.
+247
View File
@@ -0,0 +1,247 @@
package core
import (
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type tunnelGenerationTestStream struct {
rpcpb.SliverRPC_TunnelDataClient
sent chan *sliverpb.TunnelData
dataEntered chan struct{}
releaseData chan struct{}
dataErr error
enterOnce sync.Once
}
func (stream *tunnelGenerationTestStream) Send(frame *sliverpb.TunnelData) error {
if len(frame.GetData()) > 0 && stream.dataEntered != nil {
stream.enterOnce.Do(func() { close(stream.dataEntered) })
<-stream.releaseData
if stream.dataErr != nil {
return stream.dataErr
}
}
stream.sent <- frame
return nil
}
func TestTunnelsCloseIfDoesNotCloseReplacementGeneration(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{sent: make(chan *sliverpb.TunnelData, 2)}
tunnels.SetStream(stream)
old := tunnels.Start(41, "old-session")
waitForTunnelGenerationFrame(t, stream.sent)
replacement := tunnels.Start(41, "new-session")
waitForTunnelGenerationFrame(t, stream.sent)
select {
case <-old.Done():
default:
t.Fatal("replaced tunnel generation remained open")
}
if current := tunnels.Get(41); current != replacement {
t.Fatal("replacement tunnel generation was not published")
}
if tunnels.CloseIf(old) {
t.Fatal("CloseIf reported closing a superseded tunnel generation")
}
select {
case <-replacement.Done():
t.Fatal("stale CloseIf closed the replacement tunnel generation")
default:
}
if !tunnels.CloseIf(replacement) {
t.Fatal("CloseIf did not close the current tunnel generation")
}
}
func TestTunnelSendFailureCannotClosePublishedReplacement(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
sendErr := errors.New("test old-generation send failure")
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
dataErr: sendErr,
}
tunnels.SetStream(stream)
old := tunnels.Start(42, "old-session")
waitForTunnelGenerationFrame(t, stream.sent)
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := old.Write([]byte("old generation"))
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "old-generation stream send")
select {
case result := <-writeDone:
t.Fatalf("Write completed before stream Send: (%d, %v)", result.n, result.err)
case <-time.After(50 * time.Millisecond):
}
replacementDone := make(chan *TunnelIO, 1)
go func() {
replacementDone <- tunnels.Start(42, "new-session")
}()
select {
case replacement := <-replacementDone:
t.Fatalf("replacement %p was published before the old send completed", replacement)
case <-time.After(50 * time.Millisecond):
}
if current := tunnels.Get(42); current != old {
t.Fatal("old generation stopped being current while its stream send was in flight")
}
close(stream.releaseData)
select {
case result := <-writeDone:
if result.n != 0 || result.err == nil {
t.Fatalf("failed stream Write = (%d, %v), want (0, error)", result.n, result.err)
}
case <-time.After(time.Second):
t.Fatal("Write did not return after stream Send failed")
}
var replacement *TunnelIO
select {
case replacement = <-replacementDone:
case <-time.After(time.Second):
t.Fatal("replacement Start did not return after the old send completed")
}
waitForTunnelGenerationFrame(t, stream.sent)
if current := tunnels.Get(42); current != replacement {
t.Fatal("old send failure removed the published replacement generation")
}
select {
case <-replacement.Done():
t.Fatal("old send failure closed the published replacement generation")
default:
}
tunnels.CloseIf(replacement)
}
func TestTunnelWriteWaitsForSuccessfulExactGenerationStreamSend(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
tunnels.SetStream(stream)
tunnel := tunnels.Start(43, "final-write-session")
waitForTunnelGenerationFrame(t, stream.sent)
payload := []byte("final payload")
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := tunnel.Write(payload)
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "final stream send")
select {
case result := <-writeDone:
t.Fatalf("Write completed before stream Send: (%d, %v)", result.n, result.err)
case <-time.After(50 * time.Millisecond):
}
close(stream.releaseData)
select {
case result := <-writeDone:
if result.n != len(payload) || result.err != nil {
t.Fatalf("successful stream Write = (%d, %v), want (%d, nil)", result.n, result.err, len(payload))
}
case <-time.After(time.Second):
t.Fatal("Write did not complete after successful stream Send")
}
select {
case frame := <-stream.sent:
if string(frame.GetData()) != string(payload) {
t.Fatalf("stream payload = %q, want %q", frame.GetData(), payload)
}
case <-time.After(time.Second):
t.Fatal("successful Write returned before the payload was recorded")
}
if !tunnels.CloseIf(tunnel) {
t.Fatal("failed to close final-write tunnel")
}
}
func TestTunnelCloseWakesWriteWaitingForStreamSend(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
tunnels.SetStream(stream)
tunnel := tunnels.Start(44, "closing-write-session")
waitForTunnelGenerationFrame(t, stream.sent)
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := tunnel.Write([]byte("blocked payload"))
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "blocked stream send")
if !tunnels.CloseIf(tunnel) {
t.Fatal("failed to close tunnel with in-flight Write")
}
select {
case result := <-writeDone:
if result.n != 0 || !errors.Is(result.err, io.EOF) {
t.Fatalf("Write after concurrent close = (%d, %v), want (0, EOF)", result.n, result.err)
}
case <-time.After(time.Second):
t.Fatal("Close did not wake Write waiting for stream Send")
}
close(stream.releaseData)
}
func newTunnelGenerationTestStore() *tunnels {
tunnelMap := map[uint64]*TunnelIO{}
return &tunnels{
tunnels: &tunnelMap,
mutex: &sync.RWMutex{},
streamMutex: &sync.Mutex{},
}
}
func waitForTunnelGenerationFrame(t *testing.T, frames <-chan *sliverpb.TunnelData) {
t.Helper()
select {
case <-frames:
case <-time.After(time.Second):
t.Fatal("timed out waiting for tunnel stream frame")
}
}
func waitForTunnelGenerationSignal(t *testing.T, signal <-chan struct{}, description string) {
t.Helper()
select {
case <-signal:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %s", description)
}
}
+20 -11
View File
@@ -15,22 +15,24 @@ var ErrUserAborted = huh.ErrUserAborted
// GenerateFormResult captures the inputs needed to drive the generate command.
type GenerateFormResult struct {
OS string
Arch string
Format string
Name string
C2Type string
C2Value string
Save string
OS string
Arch string
Format string
Name string
C2Type string
C2Value string
ControlFlow string
Save string
}
// GenerateForm prompts for core generate flags and returns the collected values.
func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
result := &GenerateFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
ControlFlow: "off",
}
commonPlatformsOnly := true
lastCommonPlatformsOnly := commonPlatformsOnly
@@ -78,6 +80,13 @@ func GenerateForm(compiler *clientpb.Compiler) (*GenerateFormResult, error) {
}, targetBindings).
Height(3).
Value(&result.Format),
huh.NewSelect[string]().
Title("Control-flow obfuscation").
Options(
huh.NewOption("Off", "off"),
huh.NewOption("Balanced (experimental)", "balanced-v1"),
).
Value(&result.ControlFlow),
),
huh.NewGroup(
huh.NewInput().
+26 -16
View File
@@ -13,27 +13,29 @@ import (
// GenerateBeaconFormResult captures the inputs needed to drive the generate beacon command.
type GenerateBeaconFormResult struct {
OS string
Arch string
Format string
Name string
C2Type string
C2Value string
Save string
Days string
Hours string
Minutes string
Seconds string
Jitter string
OS string
Arch string
Format string
Name string
C2Type string
C2Value string
ControlFlow string
Save string
Days string
Hours string
Minutes string
Seconds string
Jitter string
}
// GenerateBeaconForm prompts for generate beacon flags and returns the collected values.
func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult, error) {
result := &GenerateBeaconFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
ControlFlow: "off",
}
commonPlatformsOnly := true
lastCommonPlatformsOnly := commonPlatformsOnly
@@ -81,6 +83,14 @@ func GenerateBeaconForm(compiler *clientpb.Compiler) (*GenerateBeaconFormResult,
}, targetBindings).
Height(3).
Value(&result.Format),
huh.NewSelect[string]().
Title("Control-flow obfuscation").
Description("Experimental; balanced-v1 requires symbol obfuscation").
Options(
huh.NewOption("Off", "off"),
huh.NewOption("Balanced (experimental)", "balanced-v1"),
).
Value(&result.ControlFlow),
),
huh.NewGroup(
huh.NewInput().
+13 -4
View File
@@ -18,15 +18,17 @@ type GenerateProfilesNewFormResult struct {
Name string
C2Type string
C2Value string
ControlFlow string
}
// GenerateProfilesNewForm prompts for profiles new flags and returns the collected values.
func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
result := &GenerateProfilesNewFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
ControlFlow: "off",
}
form := huh.NewForm(
@@ -93,6 +95,13 @@ func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
}, &result.OS).
Height(3).
Value(&result.Format),
huh.NewSelect[string]().
Title("Control-flow obfuscation").
Options(
huh.NewOption("Off", "off"),
huh.NewOption("Balanced (experimental)", "balanced-v1"),
).
Value(&result.ControlFlow),
),
huh.NewGroup(
huh.NewInput().
+13 -4
View File
@@ -18,6 +18,7 @@ type GenerateProfilesNewBeaconFormResult struct {
Name string
C2Type string
C2Value string
ControlFlow string
Days string
Hours string
Minutes string
@@ -28,10 +29,11 @@ type GenerateProfilesNewBeaconFormResult struct {
// GenerateProfilesNewBeaconForm prompts for profiles new beacon flags and returns the collected values.
func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, error) {
result := &GenerateProfilesNewBeaconFormResult{
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
OS: "windows",
Arch: "amd64",
Format: "exe",
C2Type: "mtls",
ControlFlow: "off",
}
form := huh.NewForm(
@@ -98,6 +100,13 @@ func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, erro
}, &result.OS).
Height(3).
Value(&result.Format),
huh.NewSelect[string]().
Title("Control-flow obfuscation").
Options(
huh.NewOption("Off", "off"),
huh.NewOption("Balanced (experimental)", "balanced-v1"),
).
Value(&result.ControlFlow),
),
huh.NewGroup(
huh.NewInput().
+2 -2
View File
@@ -49,10 +49,10 @@ var (
// Internal ID management. Used during animating to ensure that frame messages
// are received only by animation components that sent them.
var lastID int64
var lastID atomic.Int64
func nextID() int {
return int(atomic.AddInt64(&lastID, 1))
return int(lastID.Add(1))
}
type animCache struct {
+12 -11
View File
@@ -2,6 +2,7 @@ package transport
import (
"testing"
"testing/synctest"
"time"
"github.com/bishopfox/sliver/client/assets"
@@ -36,6 +37,10 @@ func TestWireGuardTunnelCacheKeyIncludesConfigMaterial(t *testing.T) {
}
func TestCacheIdleWireGuardTunnelRemovesIdleTunnel(t *testing.T) {
synctest.Test(t, testCacheIdleWireGuardTunnelRemovesIdleTunnel)
}
func testCacheIdleWireGuardTunnelRemovesIdleTunnel(t *testing.T) {
resetWireGuardTunnelCacheForTest(t)
previousIdleTimeout := multiplayerWireGuardIdleTimeout
@@ -47,18 +52,14 @@ func TestCacheIdleWireGuardTunnelRemovesIdleTunnel(t *testing.T) {
const cacheKey = "test-cache-key"
cacheIdleWireGuardTunnel(cacheKey, &wireGuardTunnel{}, "100.65.0.1:31337")
deadline := time.Now().Add(250 * time.Millisecond)
for time.Now().Before(deadline) {
wireGuardTunnelCacheMu.Lock()
_, exists := wireGuardTunnelCache[cacheKey]
wireGuardTunnelCacheMu.Unlock()
if !exists {
return
}
time.Sleep(5 * time.Millisecond)
time.Sleep(multiplayerWireGuardIdleTimeout)
synctest.Wait()
wireGuardTunnelCacheMu.Lock()
_, exists := wireGuardTunnelCache[cacheKey]
wireGuardTunnelCacheMu.Unlock()
if exists {
t.Fatal("expected idle tunnel cache entry to be removed")
}
t.Fatal("expected idle tunnel cache entry to be removed")
}
func resetWireGuardTunnelCacheForTest(t *testing.T) {
+23 -23
View File
@@ -3851,9 +3851,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.33",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz",
"integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==",
"version": "2.11.20",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz",
"integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -3887,9 +3887,9 @@
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
"dev": true,
"funding": [
{
@@ -3907,11 +3907,11 @@
],
"license": "MIT",
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
"electron-to-chromium": "^1.5.328",
"node-releases": "^2.0.36",
"update-browserslist-db": "^1.2.3"
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
"electron-to-chromium": "^1.5.402",
"node-releases": "^2.0.53",
"update-browserslist-db": "^1.3.0"
},
"bin": {
"browserslist": "cli.js"
@@ -3981,9 +3981,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001793",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
"integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
"version": "1.0.30001810",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
"integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -4964,9 +4964,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.364",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
"integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
"version": "1.5.420",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
"integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
"dev": true,
"license": "ISC"
},
@@ -8498,9 +8498,9 @@
}
},
"node_modules/node-releases": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
"integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
"version": "2.0.54",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -10483,9 +10483,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
"dev": true,
"funding": [
{
@@ -2,7 +2,7 @@ You'll want to compile Sliver from a MacOS or Linux machine. Compiling from nati
To compile from source you'll need:
- Go v1.26.6 or later
- Go v1.27.1 or later
- `make` (on MacOS you may need to install XCode and accompanying cli tools)
```asciinema
@@ -42,6 +42,8 @@ Some commands such as `shell` and `portfwd` only work over interactive sessions.
Generating implants is done using the `generate` command, you must specify at least one C2 endpoint using `--mtls`, `--wg`, `--http`, or `--dns`. Note that when an implant attempts to connect to an endpoint specified using `--http` it will try both HTTPS and then HTTP (if HTTPS fails). We recommend using mTLS (`--mtls`) or WireGuard (`--wg`) whenever possible. You can also specify an output directory with `--save`, by default the implant will be saved to the current working directory.
Control-flow obfuscation is experimental and opt-in. Add `--control-flow=balanced-v1` to a session, beacon, or profile generation command to enable Sliver's versioned balanced policy. It requires symbol obfuscation and cannot be combined with `--debug` or `--skip-symbols`. The server admits one local control-flow build at a time because Garble's SSA transformation is memory intensive; external builds require a builder that advertises the matching capability. The default is `--control-flow=off`.
#### Session Mode
```asciinema
+31 -9
View File
@@ -139,32 +139,54 @@ function Get-TestDirectories {
}
function Get-ServerBinaryPath {
$candidates = @(
(Join-Path $PWD.Path "sliver-server.exe"),
(Join-Path $PWD.Path "sliver-server")
)
$serverName = "sliver-server"
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows
)) {
$serverName = "sliver-server.exe"
}
foreach ($candidate in $candidates) {
if (Test-Path -LiteralPath $candidate) {
return $candidate
}
$candidate = Join-Path $PWD.Path $serverName
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
return $candidate
}
return $null
}
function Get-PinnedGarbleAssetPath {
$garbleName = "garble"
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform(
[System.Runtime.InteropServices.OSPlatform]::Windows
)) {
$garbleName = "garble.exe"
}
$goBin = Join-Path (Join-Path $env:SLIVER_ROOT_DIR "go") "bin"
return (Join-Path $goBin $garbleName)
}
function Assert-PinnedGarbleAssetUnpacked {
$garblePath = Get-PinnedGarbleAssetPath
if (-not (Test-Path -LiteralPath $garblePath -PathType Leaf)) {
throw "Server asset unpack did not produce pinned Garble at $garblePath"
}
}
function Invoke-UnpackServerAssets {
$serverBinary = Get-ServerBinaryPath
if ($serverBinary) {
try {
Invoke-ExternalCommand -FilePath $serverBinary -Arguments @("unpack", "--force")
Assert-PinnedGarbleAssetUnpacked
return
} catch {
Write-Host "sliver-server unpack failed, falling back to go run ./server unpack --force"
Write-Host "sliver-server unpack failed ($($_.Exception.Message)), falling back to go run ./server unpack --force"
}
}
Invoke-ExternalCommand -FilePath "go" -Arguments @("run", "-tags=server,go_sqlite", "./server", "unpack", "--force")
Assert-PinnedGarbleAssetUnpacked
}
function Stop-TestProcesses {
+22 -16
View File
@@ -19,6 +19,12 @@
set -u
set -o pipefail
HOST_GO="$(command -v go 2>/dev/null || true)"
if [ -z "$HOST_GO" ] || [ ! -x "$HOST_GO" ]; then
echo "Host Go toolchain not found" >&2
exit 1
fi
SKIP_GENERATE=0
UNIT_ONLY=0
for arg in "$@"; do
@@ -128,11 +134,11 @@ should_skip_package() {
local go_list_error
local test_file_counts
if [ -n "$tags" ]; then
go_list_error="$(go list -e -f '{{if .Error}}{{.Error}}{{end}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
test_file_counts="$(go list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
go_list_error="$("$HOST_GO" list -e -f '{{if .Error}}{{.Error}}{{end}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
test_file_counts="$("$HOST_GO" list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "-tags=$tags" "$pkg" 2>/dev/null || true)"
else
go_list_error="$(go list -e -f '{{if .Error}}{{.Error}}{{end}}' "$pkg" 2>/dev/null || true)"
test_file_counts="$(go list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "$pkg" 2>/dev/null || true)"
go_list_error="$("$HOST_GO" list -e -f '{{if .Error}}{{.Error}}{{end}}' "$pkg" 2>/dev/null || true)"
test_file_counts="$("$HOST_GO" list -e -f '{{len .TestGoFiles}} {{len .XTestGoFiles}}' "$pkg" 2>/dev/null || true)"
fi
if [[ "$go_list_error" == *"build constraints exclude all Go files"* ]]; then
echo
@@ -166,7 +172,7 @@ unpack_server_assets() {
fi
echo "sliver-server unpack failed, falling back to go run ./server unpack --force"
fi
go run -tags=server,go_sqlite ./server unpack --force
"$HOST_GO" run -tags=server,go_sqlite ./server unpack --force
}
run_test_cmd "unpack server assets" unpack_server_assets || exit 1
@@ -207,7 +213,7 @@ for pkg in "${CLIENT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test -tags="client,$TAGS" "$pkg" || exit 1
done
## Comprehensive E2E support packages. The top-level lifecycle test skips
@@ -216,7 +222,7 @@ for pkg in "${E2E_SUPPORT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test -tags="client,$TAGS" "$pkg" || exit 1
done
## Implant
@@ -224,7 +230,7 @@ for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
continue
fi
run_test_cmd "$pkg" go test "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test "$pkg" || exit 1
done
## Server + Util
@@ -234,25 +240,25 @@ for pkg in "${SERVER_UTIL_TEST_PKGS[@]}"; do
fi
case "$pkg" in
./server/assets/traffic-encoders | ./server/encoders)
run_test_cmd "$pkg" go test -timeout 10m -tags="server,$TAGS" "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test -timeout 10m -tags="server,$TAGS" "$pkg" || exit 1
;;
./server/rpc)
run_test_cmd "$pkg" go test -vet=off -timeout 30m -tags="server,$TAGS" "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test -vet=off -timeout 30m -tags="server,$TAGS" "$pkg" || exit 1
;;
*)
run_test_cmd "$pkg" go test -tags="server,$TAGS" "$pkg" || exit 1
run_test_cmd "$pkg" "$HOST_GO" test -tags="server,$TAGS" "$pkg" || exit 1
;;
esac
done
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2" "$HOST_GO" test -tags="server,$TAGS" ./server/c2 || 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
run_test_cmd "./server/c2 (e2e yamux)" "$HOST_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)" "$HOST_GO" test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
fi
## Server generate
@@ -260,7 +266,7 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
# The package tests above no longer need their Go build cache. Reclaim it
# before the cross-platform generation matrix starts so long CI runs do not
# exhaust the runner's disk while retaining the implant compiler cache.
run_test_cmd "clean Go build cache" go clean -cache || exit 1
run_test_cmd "clean Go build cache" "$HOST_GO" clean -cache || exit 1
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
@@ -269,7 +275,7 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
GENERATE_TEST_PARALLEL="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
run_test_cmd "./server/generate" \
env GOMAXPROCS="$GENERATE_GOMAXPROCS" \
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
"$HOST_GO" test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo
if [ "$UNIT_ONLY" -eq 1 ]; then
+124 -126
View File
@@ -1,6 +1,6 @@
module github.com/bishopfox/sliver
go 1.26.6
go 1.27.1
replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v0.46.3-0.20231214181515-27e49f3c3b69
@@ -10,6 +10,7 @@ replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-202
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/glamour/v2 v2.0.1
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.6
@@ -19,7 +20,12 @@ require (
github.com/Ne0nd0g/go-clr v1.0.3
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/atotto/clipboard v0.1.4
github.com/carapace-sh/carapace v1.11.0
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
@@ -27,18 +33,21 @@ require (
github.com/glebarez/sqlite v1.11.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/hashicorp/yamux v0.1.2
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/klauspost/compress v1.18.4
github.com/lesnuages/go-winio v0.4.19
github.com/lesnuages/snitch v0.6.0
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/mark3labs/mcp-go v0.45.0
github.com/mattn/go-sqlite3 v1.14.32
github.com/miekg/dns v1.1.70
github.com/moloch--/asciicast v0.1.1
github.com/moloch--/go-keystone v0.0.2
github.com/moloch--/sgn v0.1.2
github.com/ncruces/go-sqlite3 v0.30.4
github.com/nikoksr/notify v1.5.0
@@ -60,19 +69,20 @@ require (
github.com/ulikunitz/xz v0.5.15
github.com/xlab/treeprint v1.2.0
github.com/yiya1989/sshkrb5 v0.0.1
golang.org/x/crypto v0.53.0
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
github.com/zeebo/xxh3 v1.1.0
golang.org/x/crypto v0.55.0
golang.org/x/mod v0.38.0
golang.org/x/net v0.57.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.44.0
golang.org/x/text v0.40.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/api v0.269.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
google.golang.org/grpc v1.83.1
google.golang.org/protobuf v1.36.12
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.2
gorm.io/driver/sqlite v1.6.0
@@ -84,25 +94,35 @@ require (
)
require (
cel.dev/expr v0.25.1 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/firestore v1.21.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
cloud.google.com/go/storage v1.60.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
filippo.io/hpke v0.4.0 // indirect
firebase.google.com/go/v4 v4.18.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PagerDuty/go-pagerduty v1.8.0 // indirect
github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20250718055228-285ecf400b48 // indirect
github.com/VirusTotal/vt-go v1.0.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/appleboy/go-fcm v1.2.6 // indirect
github.com/atc0005/go-teams-notify/v2 v2.14.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
@@ -113,6 +133,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ses v1.34.17 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sns v1.39.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
@@ -124,152 +145,78 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bwmarrin/discordgo v0.29.0 // indirect
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/demisto/goxforce v0.0.0-20160322194047-db8357535b1d // indirect
github.com/dghubble/oauth1 v0.7.3 // indirect
github.com/dghubble/sling v1.4.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gaissmai/bart v0.25.0 // indirect
github.com/gen2brain/shm v0.1.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
github.com/go-lark/lark v1.16.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/nftables v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gregdel/pushover v1.4.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/go-types v0.0.0-20240719050749-165e75e768f7 // indirect
github.com/kevinburke/rest v0.0.0-20250718180114-1a15e4f2364f // indirect
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailgun/mailgun-go/v5 v5.8.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/plivo/plivo-go/v7 v7.59.2 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/silenceper/wechat/v2 v2.1.11 // indirect
github.com/slack-go/slack v0.24.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.43885 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.mau.fi/util v0.9.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
)
require (
charm.land/bubbletea/v2 v2.0.2
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/VirusTotal/vt-go v1.0.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/atotto/clipboard v0.1.4
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssm v1.65.1 // indirect
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.3
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
github.com/demisto/goxforce v0.0.0-20160322194047-db8357535b1d // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gaissmai/bart v0.25.0 // indirect
github.com/gen2brain/shm v0.1.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/nftables v0.3.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
@@ -278,29 +225,54 @@ require (
github.com/jezek/xgb v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/go-types v0.0.0-20240719050749-165e75e768f7 // indirect
github.com/kevinburke/rest v0.0.0-20250718180114-1a15e4f2364f // indirect
github.com/kevinburke/twilio-go v0.0.0-20250718182727-5fe7adc01f29 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mark3labs/mcp-go v0.45.0
github.com/mailgun/errors v0.4.0 // indirect
github.com/mailgun/mailgun-go/v5 v5.8.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mileusna/viber v1.0.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moloch--/go-keystone v0.0.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/plivo/plivo-go/v7 v7.59.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/rsteube/carapace-shlex v0.1.2 // indirect
github.com/safchain/ethtool v0.6.2 // indirect
github.com/sendgrid/rest v2.6.9+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.16.1+incompatible // indirect
github.com/silenceper/wechat/v2 v2.1.11 // indirect
github.com/slack-go/slack v0.24.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
@@ -308,25 +280,51 @@ require (
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/textmagic/textmagic-rest-go-v2/v3 v3.0.43885 // indirect
github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.2.1 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zeebo/xxh3 v1.1.0
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.mau.fi/util v0.9.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/time v0.14.0
golang.org/x/tools v0.47.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect
gopkg.in/jcmturner/gokrb5.v7 v7.5.0 // indirect
gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
+46 -44
View File
@@ -2,8 +2,8 @@
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
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=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/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.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
@@ -57,8 +57,8 @@ github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563/go.mod h1:HMbzsK
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
@@ -93,8 +93,8 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8v
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
@@ -263,6 +263,8 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 h1:uh1GSejOhVPRQmoXZxY82TiewZB8QXiaP1skL7Nun3Y=
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7/go.mod h1:ncTaGuXc5v7AuiVekeJ0Nwh8Bf4cudukoj0qM/15UZE=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -310,8 +312,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-lark/lark v1.16.0 h1:U6BwkLM9wrZedSM7cIiMofganr8PCvJN+M75w2lf2Gg=
github.com/go-lark/lark v1.16.0/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
@@ -673,8 +675,6 @@ github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKC
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/opfor v0.0.1 h1:V4RMIO3zG3nCdWGm0R8IkPtlqQVoZyYhQ8zNiQdq0AI=
github.com/sliverarmory/opfor v0.0.1/go.mod h1:xrLTsijVqxIVtYxPsDVuXeFgPlHXVkcfXMuajseD9ng=
github.com/sliverarmory/opfor v0.0.2 h1:rqckVkt2zJsJXSZGC6BnY3T/73CfX/Kv8kbBUF5Cb50=
github.com/sliverarmory/opfor v0.0.2/go.mod h1:xrLTsijVqxIVtYxPsDVuXeFgPlHXVkcfXMuajseD9ng=
github.com/sliverarmory/reflektor v0.0.8 h1:0FttcH5GZcKq571oRns3gKTyJNU2T+c3XP/sbFIAYRY=
@@ -693,8 +693,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -805,24 +805,26 @@ go.mau.fi/util v0.9.3 h1:aqNF8KDIN8bFpFbybSk+mEBil7IHeBwlujfyTnvP0uU=
go.mau.fi/util v0.9.3/go.mod h1:krWWfBM1jWTb5f8NCa2TLqWMQuM81X7TGQjhMjBeXmQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
@@ -843,8 +845,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
@@ -863,8 +865,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -886,8 +888,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
@@ -954,8 +956,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -969,8 +971,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -987,8 +989,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1014,17 +1016,17 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 h1:RxhCsti413yL0IjU9dVvuTbCISo8gs3RW1jPMStck+4=
google.golang.org/genproto v0.0.0-20260226221140-a57be14db171/go.mod h1:uhvzakVEqAuXU3TC2JCsxIRe5f77l+JySE3EqPoMyqM=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1034,8 +1036,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+1 -2
View File
@@ -1,12 +1,11 @@
module github.com/bishopfox/sliver
go 1.26.6
go 1.27.1
require (
filippo.io/age v1.1.1
github.com/Ne0nd0g/go-clr v1.0.3
github.com/ebitengine/purego v0.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/hashicorp/yamux v0.1.2
github.com/kbinani/screenshot v0.0.0-20191211154542-3a185f1ce18f
github.com/lesnuages/go-winio v0.4.19
-2
View File
@@ -13,8 +13,6 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 h1:Y5Q2mEwfzjMt5+3u70Gtw93ZOu2UuPeeeTBDntF7FoY=
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5/go.mod h1:uF6rMu/1nvu+5DpiRLwusA6xB8zlkNoGzKn8lmYONUo=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+4 -3
View File
@@ -29,14 +29,15 @@ trap 'cleanup $? $LINENO' ERR
# build Go file with all imported packages
go run update-vendor.go "$tempDir"
cd "$tempDir"
# update vendor dir
# Update the vendor directory. The compatibility version intentionally tracks
# the Go release before the module's 1.27 minimum.
# go get gvisor.dev/gvisor/runsc@go
go mod tidy -compat=1.25
go mod tidy -compat=1.26
go mod vendor
# The lesnuages/go-winio fork provides RemoteClientMode, which Sliver needs,
# but its checked-in syscall wrappers predate syscall.SyscallN. Regenerate the
# wrappers with the pinned x/sys generator so Go 1.26 Windows/ARM64 debug builds
# wrappers with the pinned x/sys generator so Go 1.27 Windows/ARM64 debug builds
# do not link the oversized syscall.Syscall15 wrapper.
(
cd vendor/github.com/lesnuages/go-winio
+1 -1
View File
@@ -221,7 +221,7 @@ func loadEnglishDictionaryFromAssets() error {
if err != nil {
return err
}
for _, word := range strings.Split(string(englishData), "\n") {
for word := range strings.SplitSeq(string(englishData), "\n") {
rawEnglishDictionary = append(rawEnglishDictionary, strings.TrimSpace(word))
}
return nil
+1
View File
@@ -84,6 +84,7 @@ func pingHandler(data []byte, resp RPCResponse) {
resp(data, err)
}
//garble:controlflow block_splits=2 junk_jumps=2 flatten_passes=1 flatten_hardening=xor trash_blocks=0
func determineDirPathFilter(targetPath string) (string, string) {
// The filter
filter := ""
+18 -16
View File
@@ -49,10 +49,10 @@ func pivotListenersHandler(envelope *pb.Envelope, connection *transports.Connect
Listeners: pivots.GetListeners(),
Response: &commonpb.Response{},
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
func pivotStartListenerHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -62,38 +62,40 @@ func pivotStartListenerHandler(envelope *pb.Envelope, connection *transports.Con
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
if createListener, ok := pivots.SupportedPivotListeners[req.Type]; ok {
listener, err := createListener(req.BindAddress, connection.Send, req.Options...)
listener, err := createListener(req.BindAddress, connection.SendEnvelope, req.Options...)
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
go listener.Start()
pivots.AddListener(listener)
data, _ := proto.Marshal(listener.ToProtobuf())
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}) {
pivots.RemoveListener(listener.ID)
}
} else {
resp.Response.Err = "Unsupported pivot listener type"
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
}
@@ -104,18 +106,18 @@ func pivotStopListenerHandler(envelope *pb.Envelope, connection *transports.Conn
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
pivots.StopListener(req.ID)
pivots.RemoveListener(req.ID)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: []byte{},
}
})
}
func pivotPeerEnvelopeHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -136,9 +138,9 @@ func pivotPeerEnvelopeHandler(envelope *pb.Envelope, connection *transports.Conn
Type: pb.PeerFailureType_SEND_FAILURE,
Err: errStr,
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotPeerFailure,
Data: data,
}
})
}
}
+36 -17
View File
@@ -55,19 +55,20 @@ func rportFwdListenersHandler(envelope *pb.Envelope, connection *transports.Conn
for _, portfwd := range forwards {
portfwdListeners = append(portfwdListeners, &pb.RportFwdListener{
ID: uint32(portfwd.ID),
BindAddress: portfwd.BindAddr,
ForwardAddress: portfwd.RemoteAddr,
ID: uint32(portfwd.ID),
BindAddress: portfwd.BindAddr,
ForwardAddress: portfwd.RemoteAddr,
AuthorizationID: portfwd.AuthorizationID,
})
}
data, _ := proto.Marshal(&pb.RportFwdListeners{
Listeners: portfwdListeners,
Response: &commonpb.Response{},
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -77,10 +78,10 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
rportfwds := rportfwd.Portfwds.List()
@@ -89,10 +90,10 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
if r.BindAddr == req.BindAddress {
resp.Response.Err = "Already listening on " + r.BindAddr + "\n"
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
}
@@ -110,14 +111,28 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
Conn: connection,
RemoteAddr: req.ForwardAddress,
BindAddr: req.BindAddress,
AuthorizationID: req.AuthorizationID,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(req.BindAddress, channelProxy)
rportfwd := rportfwd.Portfwds.Add(tcpProxy, channelProxy)
if err := tcpProxy.Start(); err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.SendEnvelope(&pb.Envelope{ID: envelope.ID, Data: data})
return
}
forward := rportfwd.Portfwds.Add(tcpProxy, channelProxy)
go func() {
select {
case <-connection.Done():
rportfwd.Portfwds.RemoveIf(forward.ID, forward)
case <-forward.Done():
}
}()
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
// {{if .Config.Debug}}
log.Printf("Proxy error %s", err)
@@ -128,12 +143,15 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
resp.ForwardAddress = req.ForwardAddress
resp.BindPort = req.ForwardPort
resp.ForwardPort = req.ForwardPort
resp.ID = uint32(rportfwd.ID)
resp.AuthorizationID = req.AuthorizationID
resp.ID = uint32(forward.ID)
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}) {
rportfwd.Portfwds.Remove(forward.ID)
}
}
@@ -144,10 +162,10 @@ func rportFwdStopListenerHandler(envelope *pb.Envelope, connection *transports.C
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
@@ -156,13 +174,14 @@ func rportFwdStopListenerHandler(envelope *pb.Envelope, connection *transports.C
resp.ID = uint32(rportfwd.ID)
resp.BindAddress = rportfwd.ChannelProxy.BindAddr
resp.ForwardAddress = rportfwd.ChannelProxy.RemoteAddr
resp.AuthorizationID = rportfwd.ChannelProxy.AuthorizationID
} else {
resp.Response.Err = "Invalid ID\n"
}
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
@@ -0,0 +1,217 @@
package handlers
import (
"net"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/rportfwd"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type rportfwdDiscardWriteCloser struct{}
func (rportfwdDiscardWriteCloser) Write(data []byte) (int, error) { return len(data), nil }
func (rportfwdDiscardWriteCloser) Close() error { return nil }
func TestRportFwdListenerClosesWithOwningConnectionAndAllowsFreshBind(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
firstConnection := newRportFwdTestConnection()
first := startRportFwdTestListener(t, firstConnection, bindAddress, "first-authorization", 1)
if got := len(rportfwd.Portfwds.List()); got != 1 {
t.Fatalf("listener count = %d, want 1", got)
}
firstConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
secondConnection := newRportFwdTestConnection()
second := startRportFwdTestListener(t, secondConnection, bindAddress, "second-authorization", 2)
if second.ID == first.ID {
t.Fatalf("fresh connection reused listener generation %d", second.ID)
}
if second.AuthorizationID != "second-authorization" {
t.Fatalf("fresh listener authorization = %q", second.AuthorizationID)
}
secondConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
func TestRportFwdListenerSurvivesRetiredTunnelWindowRotation(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
connection := newRportFwdTestConnection()
startRportFwdTestListener(t, connection, bindAddress, "rotated-authorization", 3)
for tunnelID := uint64(1); tunnelID <= 5_000; tunnelID++ {
tunnel := transports.NewTunnel(tunnelID, rportfwdDiscardWriteCloser{})
if result := connection.TryAddTunnel(tunnel); result != transports.TunnelAdded {
tunnel.Close()
t.Fatalf("claim tunnel %d result = %v, want %v", tunnelID, result, transports.TunnelAdded)
}
if !connection.CloseTunnelRemote(tunnel) {
t.Fatalf("failed to retire tunnel %d", tunnelID)
}
}
select {
case <-connection.Done():
t.Fatal("retired tunnel window rotation closed the owning connection")
default:
}
if got := len(rportfwd.Portfwds.List()); got != 1 {
t.Fatalf("listener count after replay-window rotation = %d, want 1", got)
}
connection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
freshConnection := newRportFwdTestConnection()
startRportFwdTestListener(t, freshConnection, bindAddress, "fresh-authorization", 4)
freshConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
func TestRportFwdListenerFailedResponseAndStopCleanupRacesDoNotLeak(t *testing.T) {
t.Run("failed start response", func(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
connection := &transports.Connection{}
request := marshalRportFwdStartRequest(t, bindAddress, "failed-response-authorization")
rportFwdStartListenerHandler(&sliverpb.Envelope{ID: 5, Data: request}, connection)
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
connection.Cleanup()
})
t.Run("explicit stop and connection close", func(t *testing.T) {
for iteration := 0; iteration < 32; iteration++ {
isolateRPortFwdListenersNow()
bindAddress := reserveRportFwdBindAddress(t)
connection := newRportFwdTestConnection()
listener := startRportFwdTestListener(t, connection, bindAddress, "race-authorization", int64(100+iteration))
stopData, err := proto.Marshal(&sliverpb.RportFwdStopListenerReq{ID: listener.ID})
if err != nil {
t.Fatalf("marshal stop request: %v", err)
}
start := make(chan struct{})
var waitGroup sync.WaitGroup
waitGroup.Add(2)
go func() {
defer waitGroup.Done()
<-start
connection.Cleanup()
}()
go func() {
defer waitGroup.Done()
<-start
rportFwdStopListenerHandler(&sliverpb.Envelope{ID: int64(200 + iteration), Data: stopData}, connection)
}()
close(start)
waitGroup.Wait()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
isolateRPortFwdListenersNow()
})
}
func newRportFwdTestConnection() *transports.Connection {
return &transports.Connection{Send: make(chan *sliverpb.Envelope, 4)}
}
func startRportFwdTestListener(t *testing.T, connection *transports.Connection, bindAddress string, authorizationID string, envelopeID int64) *sliverpb.RportFwdListener {
t.Helper()
request := marshalRportFwdStartRequest(t, bindAddress, authorizationID)
rportFwdStartListenerHandler(&sliverpb.Envelope{ID: envelopeID, Data: request}, connection)
select {
case envelope := <-connection.Send:
response := &sliverpb.RportFwdListener{}
if err := proto.Unmarshal(envelope.Data, response); err != nil {
t.Fatalf("decode start response: %v", err)
}
if response.Response == nil {
t.Fatal("start response omitted status")
}
if response.Response.Err != "" {
t.Fatalf("start listener response error: %s", response.Response.Err)
}
return response
case <-time.After(time.Second):
t.Fatal("timed out waiting for start response")
return nil
}
}
func marshalRportFwdStartRequest(t *testing.T, bindAddress string, authorizationID string) []byte {
t.Helper()
request, err := proto.Marshal(&sliverpb.RportFwdStartListenerReq{
BindAddress: bindAddress,
ForwardAddress: "127.0.0.1:4444",
AuthorizationID: authorizationID,
})
if err != nil {
t.Fatalf("marshal start request: %v", err)
}
return request
}
func reserveRportFwdBindAddress(t *testing.T) string {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("reserve loopback listener: %v", err)
}
address := listener.Addr().String()
if err := listener.Close(); err != nil {
t.Fatalf("release reserved loopback listener: %v", err)
}
return address
}
func waitForRportFwdListenerCount(t *testing.T, want int) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if len(rportfwd.Portfwds.List()) == want {
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("listener count = %d, want %d", len(rportfwd.Portfwds.List()), want)
}
func waitForRportFwdBindRelease(t *testing.T, bindAddress string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
listener, err := net.Listen("tcp", bindAddress)
if err == nil {
_ = listener.Close()
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("listener bind %s was not released", bindAddress)
}
func isolateRportFwdListeners(t *testing.T) {
t.Helper()
isolateRPortFwdListenersNow()
t.Cleanup(isolateRPortFwdListenersNow)
}
func isolateRPortFwdListenersNow() {
for _, listener := range rportfwd.Portfwds.List() {
rportfwd.Portfwds.Remove(listener.ID)
}
}
@@ -19,6 +19,8 @@ package tunnel_handlers
*/
import (
"errors"
"time"
// {{if .Config.Debug}}
"log"
@@ -30,23 +32,85 @@ import (
"google.golang.org/protobuf/proto"
)
const tunnelTerminalCloseTimeout = 10 * time.Second
func closeTunnelLocal(connection *transports.Connection, tunnel *transports.Tunnel) bool {
if connection == nil || tunnel == nil {
return false
}
shell.StopSession(tunnel.ID)
return connection.CloseTunnelLocal(tunnel)
}
func closeTunnelRemote(connection *transports.Connection, tunnel *transports.Tunnel) bool {
if connection == nil || tunnel == nil {
return false
}
shell.StopSession(tunnel.ID)
return connection.CloseTunnelRemote(tunnel)
}
// TunnelCloseHandler applies a peer terminal frame to its exact tunnel generation.
func TunnelCloseHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
tunnelClose := &sliverpb.TunnelData{
Closed: true,
}
proto.Unmarshal(envelope.Data, tunnelClose)
// Closing the tunnel pipes alone does not guarantee that a Windows shell
// process exits. Stop the process that owns this tunnel first.
shell.StopSession(tunnelClose.TunnelID)
if err := proto.Unmarshal(envelope.Data, tunnelClose); err != nil || !tunnelClose.Closed {
return
}
handleTunnelClose(tunnelClose, connection, tunnelTerminalCloseTimeout)
}
func handleTunnelClose(tunnelClose *sliverpb.TunnelData, connection *transports.Connection, timeout time.Duration) {
if tunnelClose == nil || connection == nil {
return
}
tunnel := connection.Tunnel(tunnelClose.TunnelID)
if tunnel == nil {
// A close can overtake destination dialing and active-tunnel
// publication. Retire the exact connection-owned setup first, then
// re-read the active map in case publication won that race.
connection.CancelPendingTunnel(tunnelClose.TunnelID)
tunnel = connection.Tunnel(tunnelClose.TunnelID)
}
if tunnel != nil {
// {{if .Config.Debug}}
log.Printf("[tunnel] Closing tunnel with id %d", tunnel.ID)
// {{end}}
connection.RemoveTunnel(tunnel.ID)
tunnel.Close() // Call tunnel.Close instead of individually closing each Reader/Writer here
tunnelDataCache.DeleteTun(tunnel.ID)
// Sequence zero is the legacy protocol's immediate terminal. It must not
// wait behind an inbound destination write: closing the writer is what
// unblocks a stalled shell, port-forward, or WASM handler.
if tunnelClose.Sequence == 0 {
closeTunnelRemote(connection, tunnel)
return
}
// A sequenced terminal normally waits for every lower data frame. Arm the
// fail-closed deadline first because MarkPeerClose serializes with the
// inbound writer and that writer may be non-cooperative until Close.
tunnel.StartPeerCloseDeadline(timeout, func() {
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
})
ready, err := tunnel.MarkPeerClose(tunnelClose.Sequence)
if err != nil {
if errors.Is(err, transports.ErrTunnelClosed) {
return
}
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
return
}
if ready {
closeTunnelRemote(connection, tunnel)
return
}
} else {
// Preserve the shell startup tombstone as well; StopSession is a no-op
// for non-shell tunnel IDs.
shell.StopSession(tunnelClose.TunnelID)
// {{if .Config.Debug}}
log.Printf("[tunnel][tunnelCloseHandler] Received close message for unknown tunnel id %d", tunnelClose.TunnelID)
// {{end}}
@@ -1,84 +0,0 @@
package tunnel_handlers
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
var (
// TunnelID -> Sequence Number -> Data
tunnelDataCache = dataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.TunnelData{}}
)
type dataCache struct {
mutex *sync.RWMutex
cache map[uint64]map[uint64]*sliverpb.TunnelData
}
func (c *dataCache) Add(tunnelID uint64, sequence uint64, tunnelData *sliverpb.TunnelData) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
c.cache[tunnelID] = map[uint64]*sliverpb.TunnelData{}
}
c.cache[tunnelID][sequence] = tunnelData
}
func (c *dataCache) Get(tunnelID uint64, sequence uint64) (*sliverpb.TunnelData, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if _, ok := c.cache[tunnelID]; !ok {
return nil, false
}
val, ok := c.cache[tunnelID][sequence]
return val, ok
}
func (c *dataCache) DeleteTun(tunnelID uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.cache, tunnelID)
}
func (c *dataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
return
}
delete(c.cache[tunnelID], sequence)
}
func (c *dataCache) Len(tunnelID uint64) int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache[tunnelID])
}
@@ -19,6 +19,8 @@ package tunnel_handlers
*/
import (
"errors"
"io"
// {{if .Config.Debug}}
"log"
@@ -31,62 +33,82 @@ import (
func TunnelDataHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(envelope.Data, tunnelData)
if err := proto.Unmarshal(envelope.Data, tunnelData); err != nil {
return
}
tunnel := connection.Tunnel(tunnelData.TunnelID)
if tunnel != nil {
// Since we have no guarantees that we will receive tunnel data in the correct order, we need
// to ensure we write the data back to the reader in the correct order. The server will ensure
// that TunnelData protobuf objects are numbered in the correct order using the Sequence property.
// Similarly we ensure that any data we write-back to the server is also numbered correctly. To
// reassemble the data, we just dump it into the cache and then advance the writer until we no longer
// have sequential data. So we can receive `n` number of incorrectly ordered Protobuf objects and
// correctly write them back to the reader.
// Resend is a control frame in the legacy generic tunnel protocol, not
// zero-byte stream data. Reverse tunnels never emit it; reliable C2
// transports plus the bounded reorder actor handle delayed frames.
if tunnelData.Resend {
return
}
// {{if .Config.Debug}}
log.Printf("[tunnel] Cache tunnel %d (seq: %d)", tunnel.ID, tunnelData.Sequence)
log.Printf("[tunnel] Process tunnel %d (seq: %d)", tunnel.ID, tunnelData.Sequence)
// {{end}}
tunnelDataCache.Add(tunnel.ID, tunnelData.Sequence, tunnelData)
// NOTE: The read/write semantics can be a little mind boggling, just remember we're reading
// from the server and writing to the tunnel's reader (e.g. stdout), so that's why ReadSequence
// is used here whereas WriteSequence is used for data written back to the server
// Go through cache and write all sequential data to the reader
for recv, ok := tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()); ok; recv, ok = tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()) {
pending, err := tunnel.ProcessInbound(tunnelData.Sequence, tunnelData.Data, func(payload []byte) error {
// {{if .Config.Debug}}
log.Printf("[tunnel] Write %d bytes to tunnel %d (read seq: %d)", len(recv.Data), recv.TunnelID, recv.Sequence)
// {{end}}
tunnel.Writer.Write(recv.Data)
// Delete the entry we just wrote from the cache
tunnelDataCache.DeleteSeq(tunnel.ID, tunnel.ReadSequence())
tunnel.IncReadSequence() // Increment sequence counter
// {{if .Config.Debug}}
log.Printf("[message just received] %v", tunnelData)
log.Printf("[tunnel] Write %d bytes to tunnel %d", len(payload), tunnel.ID)
// {{end}}
written, writeErr := tunnel.Writer.Write(payload)
if writeErr != nil {
return writeErr
}
if written != len(payload) {
return io.ErrShortWrite
}
return nil
})
if err != nil {
if errors.Is(err, transports.ErrTunnelClosed) {
return
}
protocolFailure := errors.Is(err, transports.ErrTunnelTerminalSequence) ||
errors.Is(err, transports.ErrTunnelFrameTooLarge) ||
errors.Is(err, transports.ErrTunnelSequenceWindow) ||
errors.Is(err, transports.ErrTunnelPendingBytes)
if protocolFailure {
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
} else {
closeTunnelLocal(connection, tunnel)
}
return
}
if tunnel.PeerCloseReady() {
closeTunnelRemote(connection, tunnel)
return
}
//If cache is building up it probably means a msg was lost and the server is currently hung waiting for it.
//Send a Resend packet to have the msg resent from the cache
if tunnelDataCache.Len(tunnel.ID) > 3 {
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: tunnel.WriteSequence(), // The tunnel write sequence
Ack: tunnel.ReadSequence(),
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
if pending > 3 && !tunnel.IsReverse() {
err := connection.QueueTunnelControl(tunnel, func(sequence uint64, ack uint64) (*sliverpb.Envelope, error) {
data, marshalErr := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
})
if marshalErr != nil {
return nil, marshalErr
}
return &sliverpb.Envelope{Type: sliverpb.MsgTunnelData, Data: data}, nil
})
if err != nil {
// {{if .Config.Debug}}
log.Printf("[shell] Failed to marshal protobuf %s", err)
// {{end}}
} else {
if !errors.Is(err, transports.ErrTunnelClosed) {
// {{if .Config.Debug}}
log.Printf("[shell] Failed to marshal protobuf %s", err)
// {{end}}
}
closeTunnelLocal(connection, tunnel)
} else if err == nil {
// {{if .Config.Debug}}
log.Printf("[tunnel] Requesting resend of tunnelData seq: %d", tunnel.ReadSequence())
// {{end}}
connection.RequestResend(data)
}
}
@@ -0,0 +1,157 @@
package tunnel_handlers
import (
"bytes"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type recordingWriteCloser struct {
bytes.Buffer
closed bool
}
func (writer *recordingWriteCloser) Close() error {
writer.closed = true
return nil
}
func TestReverseTunnelIgnoresLegacyResendControlFrame(t *testing.T) {
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 1)}
writer := &recordingWriteCloser{}
tunnel := transports.NewReverseTunnel(0x5101, writer)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add reverse tunnel")
}
t.Cleanup(func() { connection.CloseTunnelRemote(tunnel) })
payload, err := proto.Marshal(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 0,
Resend: true,
Data: []byte("must-not-be-relayed"),
})
if err != nil {
t.Fatalf("marshal resend frame: %v", err)
}
TunnelDataHandler(&sliverpb.Envelope{Type: sliverpb.MsgTunnelData, Data: payload}, connection)
if got := writer.String(); got != "" {
t.Fatalf("resend control payload reached reverse destination: %q", got)
}
if got := tunnel.ReadSequence(); got != 0 {
t.Fatalf("resend control advanced reverse read sequence to %d", got)
}
if got := connection.Tunnel(tunnel.ID); got != tunnel {
t.Fatalf("resend control detached reverse tunnel: got %p, want %p", got, tunnel)
}
select {
case envelope := <-connection.Send:
t.Fatalf("resend control emitted unexpected envelope type %d", envelope.Type)
default:
}
}
type closeUnblocksWriteCloser struct {
started chan struct{}
released chan struct{}
startOnce sync.Once
closeOnce sync.Once
}
func newCloseUnblocksWriteCloser() *closeUnblocksWriteCloser {
return &closeUnblocksWriteCloser{
started: make(chan struct{}),
released: make(chan struct{}),
}
}
func (writer *closeUnblocksWriteCloser) Write([]byte) (int, error) {
writer.startOnce.Do(func() { close(writer.started) })
<-writer.released
return 0, io.ErrClosedPipe
}
func (writer *closeUnblocksWriteCloser) Close() error {
writer.closeOnce.Do(func() { close(writer.released) })
return nil
}
func TestTunnelCloseUnblocksInboundWriter(t *testing.T) {
tests := []struct {
name string
sequence uint64
closeTimeout time.Duration
wantC2Cleanup bool
}{
{name: "legacy immediate close", sequence: 0, closeTimeout: time.Second},
{name: "sequenced close deadline", sequence: 1, closeTimeout: 20 * time.Millisecond, wantC2Cleanup: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 1)}
writer := newCloseUnblocksWriteCloser()
tunnel := transports.NewTunnel(0x5201, writer)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add tunnel")
}
inboundDone := make(chan error, 1)
go func() {
_, err := tunnel.ProcessInbound(0, []byte("blocked"), func(data []byte) error {
_, writeErr := writer.Write(data)
return writeErr
})
inboundDone <- err
}()
select {
case <-writer.started:
case <-time.After(time.Second):
t.Fatal("inbound writer did not block")
}
closeDone := make(chan struct{})
go func() {
handleTunnelClose(&sliverpb.TunnelData{
Closed: true,
TunnelID: tunnel.ID,
Sequence: test.sequence,
}, connection, test.closeTimeout)
close(closeDone)
}()
select {
case <-closeDone:
case <-time.After(time.Second):
t.Fatal("tunnel close remained blocked behind inbound writer")
}
select {
case err := <-inboundDone:
if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("inbound error = %v, want %v", err, io.ErrClosedPipe)
}
case <-time.After(time.Second):
t.Fatal("closing tunnel did not unblock inbound writer")
}
if active := connection.Tunnel(tunnel.ID); active != nil {
t.Fatalf("closed tunnel remained published: %p", active)
}
select {
case <-connection.Done():
if !test.wantC2Cleanup {
t.Fatal("legacy close unexpectedly tore down the C2 connection")
}
default:
if test.wantC2Cleanup {
t.Fatal("incomplete sequenced close did not fail the C2 connection closed")
}
}
})
}
}
@@ -20,10 +20,10 @@ package tunnel_handlers
import (
"context"
"fmt"
"errors"
"io"
"net"
"sync"
"strconv"
"time"
// {{if .Config.Debug}}
@@ -37,7 +37,16 @@ import (
"google.golang.org/protobuf/proto"
)
const portfwdDialTimeout = 30 * time.Second
type portfwdDialFunc func(context.Context, string, string) (net.Conn, error)
// PortfwdReqHandler opens and registers one server-requested forward tunnel.
func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
handlePortfwdReq(envelope, connection, new(net.Dialer).DialContext, portfwdDialTimeout)
}
func handlePortfwdReq(envelope *sliverpb.Envelope, connection *transports.Connection, dial portfwdDialFunc, dialTimeout time.Duration) {
portfwdReq := &sliverpb.PortfwdReq{}
err := proto.Unmarshal(envelope.Data, portfwdReq)
if err != nil {
@@ -53,43 +62,44 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
return
}
var defaultDialer = new(net.Dialer)
pending, addResult := connection.BeginTunnel(portfwdReq.TunnelID, dialTimeout)
if addResult != transports.TunnelAdded {
reportPortfwdError(envelope, connection, errors.New("port forward tunnel setup is no longer active"))
return
}
defer pending.Cancel()
// TODO: Configurable context
remoteAddress := fmt.Sprintf("%s:%d", portfwdReq.Host, portfwdReq.Port)
remoteAddress := portfwdRemoteAddress(portfwdReq.Host, portfwdReq.Port)
// {{if .Config.Debug}}
log.Printf("[portfwd] Dialing -> %s", remoteAddress)
// {{end}}
ctx, cancelContext := context.WithCancel(context.Background())
dst, err := defaultDialer.DialContext(ctx, "tcp", remoteAddress)
dst, err := dial(pending.Context(), "tcp", remoteAddress)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[portfwd] Failed to dial remote address %s", err)
// {{end}}
cancelContext()
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
Response: &commonpb.Response{
Err: err.Error(),
},
})
reportError(envelope, connection, portfwdResp)
reportPortfwdError(envelope, connection, err)
return
}
if conn, ok := dst.(*net.TCPConn); ok {
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
var keepAliveErr error
if portfwdReq.KeepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
keepAliveErr = conn.SetKeepAlive(false)
} else if keepAliveErr = conn.SetKeepAlive(true); keepAliveErr == nil {
keepAlivePeriod := 30 * time.Second
if portfwdReq.KeepAlive > 0 {
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
keepAlivePeriod = time.Duration(portfwdReq.KeepAlive) * time.Second
}
keepAliveErr = conn.SetKeepAlivePeriod(keepAlivePeriod)
}
if keepAliveErr != nil {
_ = dst.Close()
reportPortfwdError(envelope, connection, keepAliveErr)
return
}
}
@@ -102,7 +112,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
dst,
dst,
)
connection.AddTunnel(tunnel)
if connection.PublishTunnel(pending, tunnel) != transports.TunnelAdded {
tunnel.Close()
reportPortfwdError(envelope, connection, errors.New("port forward tunnel setup was canceled"))
return
}
if connection.Tunnel(tunnel.ID) != tunnel {
tunnel.Close()
reportPortfwdError(envelope, connection, errors.New("port forward tunnel closed during setup"))
return
}
// Send portfwd response
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
@@ -111,36 +130,12 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: portfwdReq.TunnelID,
})
connection.Send <- &sliverpb.Envelope{
if !connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: portfwdResp,
}
once := sync.Once{}
cleanup := func(reason error) {
once.Do(func() {
// {{if .Config.Debug}}
log.Printf("[portfwd] Closing tunnel %d (%s)", tunnel.ID, reason)
// {{end}}
cleanupTunnel := connection.Tunnel(tunnel.ID)
if cleanupTunnel == nil {
return
}
tunnelClose, _ := proto.Marshal(&sliverpb.TunnelData{
Closed: true,
TunnelID: cleanupTunnel.ID,
})
connection.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelClose,
Data: tunnelClose,
}
connection.RemoveTunnel(cleanupTunnel.ID)
if dst != nil {
dst.Close()
}
cancelContext()
})
}) {
connection.CloseTunnelRemote(tunnel)
return
}
go func() {
@@ -151,10 +146,25 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
// portfwd only uses one reader, hence the tunnel.Readers[0]
n, err := io.Copy(tWriter, tunnel.Readers[0])
_ = n // avoid not used compiler error if debug mode is disabled
_ = err
// {{if .Config.Debug}}
log.Printf("[tunnel] Tunnel done, wrote %v bytes", n)
// {{end}}
cleanup(err)
// {{if .Config.Debug}}
log.Printf("[portfwd] Closing tunnel %d (%s)", tunnel.ID, err)
// {{end}}
connection.CloseTunnelLocal(tunnel)
}()
}
func reportPortfwdError(envelope *sliverpb.Envelope, connection *transports.Connection, err error) {
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
Response: &commonpb.Response{Err: err.Error()},
})
reportError(envelope, connection, portfwdResp)
}
func portfwdRemoteAddress(host string, port uint32) string {
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10))
}
Loaded 100 of 667 files, more files were not shown because too many files have changed in this diff. Show more