mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-09 17:09:39 -10:00
Compare commits
43
Commits
fix/shell-win
...
obf-cfo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d95da61556 | ||
|
|
5d5661947c | ||
|
|
aad8ca5d4e | ||
|
|
84bb2397c7 | ||
|
|
503d040885 | ||
|
|
801452eeff | ||
|
|
f6c7d73014 | ||
|
|
228757f656 | ||
|
|
738010e990 | ||
|
|
9dcf9b2d25 | ||
|
|
32ab020dbd | ||
|
|
6fcaf970b1 | ||
|
|
47c83bbaa0 | ||
|
|
7d76febbc9 | ||
|
|
f4c182427f | ||
|
|
836e33ade7 | ||
|
|
0aa7e5bf96 | ||
|
|
b46df4b05e | ||
|
|
cbc9200649 | ||
|
|
a0bf009c06 | ||
|
|
46f516fa3a | ||
|
|
1f82c38e11 | ||
|
|
6ac84ffc11 | ||
|
|
2638b5f384 | ||
|
|
cf19741b9d | ||
|
|
b2c0979b63 | ||
|
|
ea5e1412b2 | ||
|
|
bd948b8ab1 | ||
|
|
937a74d597 | ||
|
|
e747187dda | ||
|
|
ec992dea76 | ||
|
|
544c872881 | ||
|
|
4701b895b7 | ||
|
|
18c56ec3c5 | ||
|
|
a8c6d5e0a3 | ||
|
|
f447a170c6 | ||
|
|
d565592773 | ||
|
|
c21da4dceb | ||
|
|
b08e2ff5c0 | ||
|
|
55b0f2bce3 | ||
|
|
c481a4f5db | ||
|
|
73a567550d | ||
|
|
9be2cd8b34 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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/...
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+39
-9
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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().
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Generated
+23
-23
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user