Compare commits

..
Author SHA1 Message Date
moloch-- ad274cc260 Redesign unit tests 2026-02-25 08:44:17 -08:00
3231 changed files with 223699 additions and 572608 deletions

No files matched your search

-3
View File
@@ -1,3 +0,0 @@
# Preserve pinned upstream source and attribution files byte-for-byte.
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
+1 -1
View File
@@ -1 +1 @@
* @BishopFox/bf-red-team
* @BishopFox/red-team
+9 -28
View File
@@ -2,8 +2,8 @@ name: Release
on:
push:
tags:
- "v*.*.*"
branches:
- master
permissions:
contents: write
@@ -21,36 +21,17 @@ jobs:
with:
fetch-depth: 0
- name: Validate Release Tag
- name: Find Semver Tag
id: tag
run: |
git fetch --prune --tags -f origin
tag="${GITHUB_REF_NAME}"
if ! printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Tag $tag is not a release semver tag."
exit 0
fi
if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/master"; then
echo "Tag $tag does not point to a commit on origin/master."
exit 0
fi
git fetch --prune --tags -f
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
echo "tag=$tag" >> "$GITHUB_OUTPUT"
e2e:
name: End-to-end test suites
needs: tag-check
if: needs.tag-check.outputs.tag != ''
uses: ./.github/workflows/comprehensive-e2e.yml
release:
name: Release
needs:
- tag-check
- e2e
if: needs.tag-check.outputs.tag != '' && needs.e2e.result == 'success'
needs: tag-check
if: needs.tag-check.outputs.tag != ''
runs-on: ubuntu-latest
environment: release
timeout-minutes: 45
@@ -60,10 +41,10 @@ jobs:
with:
fetch-depth: 0
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
steps:
# Setup go environment
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Checkout repository
uses: actions/checkout@v6
-456
View File
@@ -1,456 +0,0 @@
name: Comprehensive e2e Tests
on:
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: comprehensive-e2e-${{ github.ref }}
cancel-in-progress: false
jobs:
authorize:
name: Authorize invocation
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Check actor repository permission
uses: actions/github-script@v9
env:
REQUESTING_ACTOR: ${{ github.triggering_actor }}
with:
script: |
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
if (isReleaseTag) {
core.info(`Authorized release test invocation for ${context.ref}`);
return;
}
const actor = process.env.REQUESTING_ACTOR || context.actor;
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: actor,
});
core.info(`${actor} has repository permission ${data.permission} (${data.role_name})`);
if (data.permission !== "admin") {
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
}
darwin:
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
needs: authorize
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
goarch: amd64
server_arch: amd64
- runner: macos-15
goarch: arm64
server_arch: arm64
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 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: darwin
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: darwin
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./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
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
linux-native:
name: comprehensive-e2e-linux-${{ matrix.goarch }}
needs: authorize
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
goarch: amd64
server_arch: amd64
- runner: ubuntu-24.04-arm
goarch: arm64
server_arch: arm64
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/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: linux
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: linux
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./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
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
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
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: arm64
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 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: windows
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o sliver-server.exe ./server
- name: Compile comprehensive E2E driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: windows
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e.exe ./test/e2e
- name: Run comprehensive E2E suite
run: >-
./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
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
aggregate-coverage:
name: Aggregate comprehensive E2E coverage
needs:
- authorize
- darwin
- linux-native
- linux-386
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
outputs:
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
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
uses: actions/download-artifact@v8
with:
pattern: comprehensive-e2e-target-*
path: coverage-input
merge-multiple: true
- name: Prepare aggregate artifact
run: |
mkdir -p coverage-input coverage-summary
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
- name: Aggregate coverage and enforce required cells
id: aggregate
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/report
-input coverage-input
-output coverage-summary
- name: Publish command coverage table
id: publish_command_coverage
if: ${{ always() }}
run: |
detailed_report="coverage-summary/coverage-summary.md"
report="coverage-summary/command-coverage.md"
if [[ ! -s "${report}" ]]; then
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
exit 1
fi
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
if grep -Fxq "${delimiter}" "${report}"; then
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
exit 1
fi
{
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
cat "${report}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
if [[ -f "${detailed_report}" ]]; then
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
fi
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
- name: Upload aggregate coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-coverage-summary
path: coverage-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed or not-run coverage
if: ${{ steps.aggregate.outcome != 'success' }}
run: exit 1
reflektor:
name: Existing Reflektor test matrix
needs: authorize
uses: ./.github/workflows/reflektor-tests.yml
shellcode:
name: Shellcode E2E test matrix
needs: authorize
uses: ./.github/workflows/shellcode-tests.yml
-56
View File
@@ -1,56 +0,0 @@
name: golangci-lint
on:
push:
branches:
- master
pull_request:
branches:
- master
paths:
- ".golangci.yml"
- ".github/workflows/golangci-lint.yml"
- "go.mod"
- "go.sum"
- "go.work"
- "go.work.sum"
- "Makefile"
- "vendor/**"
- "**/*.go"
permissions:
contents: read
jobs:
lint:
name: golangci-lint (${{ matrix.goos }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
goos:
- linux
- darwin
- windows
env:
GOOS: ${{ matrix.goos }}
GOARCH: amd64
CGO_ENABLED: "0"
steps:
- name: Check Out Code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Go 1.26.6
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
# 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/...
-214
View File
@@ -1,214 +0,0 @@
name: Reflektor Integration Test Matrix
on:
workflow_call:
permissions:
contents: read
jobs:
darwin:
name: reflektor-darwin-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
goarch: amd64
server_arch: amd64
- runner: macos-15
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: darwin
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-target-os darwin
-target-arch ${{ matrix.goarch }}
linux-native:
name: reflektor-linux-${{ matrix.goarch }}-native
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
goarch: amd64
server_arch: amd64
- runner: ubuntu-24.04-arm
goarch: arm64
server_arch: arm64
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: linux
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e
-repo .
-server ./sliver-server
-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 }}
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
- 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
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -mod=vendor ./util/cmd/assets
- name: Build Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
- name: Build Reflektor integration driver
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.goarch }}
GOOS: windows
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
- name: Load Sliver with Reflektor
run: >-
./sliver-reflektor-e2e.exe
-repo .
-server ./sliver-server.exe
-target-os windows
-target-arch ${{ matrix.goarch }}
-208
View File
@@ -1,208 +0,0 @@
name: Shellcode E2E Test Matrix
on:
workflow_call:
outputs:
shellcode_coverage_markdown:
description: Shellcode E2E coverage table in Markdown
value: ${{ jobs.aggregate.outputs.shellcode_coverage_markdown }}
permissions:
contents: read
jobs:
target:
name: shellcode-e2e-${{ matrix.target_os }}-${{ matrix.target_arch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 360
strategy:
fail-fast: false
matrix:
include:
- runner: macos-26
target_os: darwin
target_arch: arm64
server_os: darwin
server_arch: arm64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: ubuntu-24.04
target_os: linux
target_arch: amd64
server_os: linux
server_arch: amd64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: ubuntu-24.04-arm
target_os: linux
target_arch: arm64
server_os: linux
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
server_os: windows
server_arch: amd64
server_executable: ./sliver-server.exe
test_executable: ./sliver-shellcode-e2e.exe
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare target artifact
run: |
mkdir -p shellcode-results
printf '%s/%s\n' \
'${{ matrix.target_os }}' \
'${{ matrix.target_arch }}' \
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
run: go run -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
- name: Build unmodified Sliver server
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.server_arch }}
GOOS: ${{ matrix.server_os }}
run: >-
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
-o ${{ matrix.server_executable }} ./server
- name: Compile shellcode E2E client
env:
CGO_ENABLED: "0"
GOARCH: ${{ matrix.target_arch }}
GOOS: ${{ matrix.target_os }}
run: >-
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o ${{ matrix.test_executable }} ./test/e2e
- name: Run shellcode generation and execution tests
run: >-
${{ matrix.test_executable }}
-test.v
-test.run=^TestShellcodeE2E$
-test.timeout=0
-repo .
-server ${{ matrix.server_executable }}
-server-arch ${{ matrix.server_arch }}
-target-os ${{ matrix.target_os }}
-target-arch ${{ matrix.target_arch }}
-results ./shellcode-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
-shellcode-sgn-samples 4
- name: Upload target shellcode results
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
path: shellcode-results
if-no-files-found: error
overwrite: true
retention-days: 14
aggregate:
name: Aggregate shellcode E2E coverage
needs: target
if: ${{ always() }}
outputs:
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
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: Prepare aggregate artifact
run: |
mkdir -p shellcode-input shellcode-summary
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
- name: Download target shellcode results
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: shellcode-e2e-target-*
path: shellcode-input
merge-multiple: true
- name: Aggregate shellcode coverage
id: aggregate
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
-input ./shellcode-input
-output ./shellcode-summary
- name: Publish shellcode coverage table
id: publish_shellcode_coverage
if: ${{ always() }}
run: |
report="shellcode-summary/shellcode-coverage.md"
if [[ ! -s "${report}" ]]; then
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
exit 1
fi
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
if grep -Fxq "${delimiter}" "${report}"; then
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
exit 1
fi
{
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
cat "${report}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
- name: Upload aggregate shellcode coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: shellcode-e2e-coverage-summary
path: shellcode-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed or not-run shellcode coverage
if: ${{ always() && steps.aggregate.outcome != 'success' }}
run: exit 1
+8 -53
View File
@@ -6,13 +6,11 @@ on:
paths:
- "go.mod"
- "go.sum"
- ".github/workflows/unit-tests.yml"
- "Makefile"
- "**/*.go"
- "**/*.c"
- "**/*.h"
- "**/*.sh"
- "**/*.ps1"
types:
- opened
- reopened
@@ -20,15 +18,15 @@ on:
- ready_for_review
jobs:
linux-build-test:
name: Linux Build & Test
linux-windows-build-test:
name: Linux & Windows Test
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: OS Packages
run: |
@@ -66,59 +64,16 @@ jobs:
- name: Test Linux
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
windows-build-test:
name: Windows Build & Test
runs-on: windows-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
- name: Install make
run: choco install make -y --no-progress
- name: Check Out Code
uses: actions/checkout@v6
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make Windows (amd64)
run: make windows-amd64
- name: Determine go-tests flags
id: go-tests-flags
shell: pwsh
run: |
$baseSha = "${{ github.event.pull_request.base.sha }}"
$headSha = "${{ github.sha }}"
$changedFiles = git diff --name-only $baseSha $headSha
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
} else {
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
}
- name: Test Windows
shell: pwsh
run: |
make clean
make
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
macos-build-test:
name: MacOS Test
runs-on: macos-latest
timeout-minutes: 180
steps:
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Check Out Code
uses: actions/checkout@v6
@@ -153,10 +108,10 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.26.6
- name: Go 1.25
uses: actions/setup-go@v6
with:
go-version: "1.26.6"
go-version: "1.25.6"
- name: Check Out Code
uses: actions/checkout@v6
-5
View File
@@ -1,8 +1,3 @@
/crush
/examples
/.vscode
/.vscode/*
/.devcontainer
/.idea
-27
View File
@@ -1,27 +0,0 @@
version: "2"
run:
timeout: 10m
modules-download-mode: vendor
build-tags:
- client
- server
- osusergo
- netgo
- go_sqlite
# Raw implant entry points are Go templates; lint their supporting packages.
- sliver_lint
linters:
default: standard
enable:
- gocyclo
- misspell
- revive
settings:
gocyclo:
min-complexity: 15
formatters:
enable:
- gofmt
+1 -1
View File
@@ -4,7 +4,7 @@
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
## Coding Style & Naming Conventions
+144 -291
View File
@@ -2,31 +2,31 @@
# Makefile for Sliver
#
.DEFAULT_GOAL := default
GO ?= go
ARTIFACT_SUFFIX ?=
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
GOLANGCI_LINT ?= golangci-lint
# The tree has legacy lint findings, so gate issues introduced by this change.
GOLANGCI_LINT_BASE ?= master
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
GOLANGCI_LINT_PACKAGES = \
./client/... \
./server/... \
./util/... \
./protobuf/... \
./docs \
./implant/...
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 26
MIN_SUPPORTED_GO_MINOR_VERSION = 24
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
@@ -34,298 +34,151 @@ ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
.PHONY: lint
lint:
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
ifneq ($(OS),Windows_NT)
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# Targets
#
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# >>> WINDOWS <<<
# Targets
#
else
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
SHELL := cmd.exe
.SHELLFLAGS := /C
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
COMMA := ,
ifeq ($(MAKECMDGOALS), linux)
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
define windows_exec
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
endef
define windows_go_build
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
endef
.PHONY: default
default: clean validate-go-version
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
.PHONY: pb
pb: validate-pb-compilers
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: validate-pb-compilers
validate-pb-compilers:
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
validate-go-version:
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
.PHONY: clean-all
clean-all: clean
-rmdir /S /Q server\assets\fs\darwin\amd64
-rmdir /S /Q server\assets\fs\darwin\arm64
-rmdir /S /Q server\assets\fs\windows\amd64
-rmdir /S /Q server\assets\fs\linux\amd64
-del /Q /F server\assets\fs\*.zip 2>NUL
-del /Q /F .downloaded_assets 2>NUL
.PHONY: clean
clean:
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
.downloaded_assets:
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
endif
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
+1 -1
View File
@@ -4,7 +4,7 @@ Sliver is an open source cross-platform adversary emulation/red team framework,
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![golangci-lint](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Release](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml/badge.svg)](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/BishopFox/sliver)](https://goreportcard.com/report/github.com/BishopFox/sliver) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
+7 -18
View File
@@ -34,26 +34,15 @@ const (
ConfigDirName = "configs"
)
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
type ClientWGConfig struct {
Enabled bool `json:"enabled,omitempty"`
ServerPubKey string `json:"server_pub_key"`
ClientPrivateKey string `json:"client_private_key"`
ClientPubKey string `json:"client_pub_key"`
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
}
// ClientConfig - Client JSON config
type ClientConfig struct {
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
WG *ClientWGConfig `json:"wg,omitempty"`
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
}
// GetConfigDir - Returns the path to the config dir
-29
View File
@@ -1,29 +0,0 @@
package assets
import (
"encoding/json"
"testing"
)
func TestClientWGConfigEnabledIsPositiveOptIn(t *testing.T) {
legacyJSON := []byte(`{"wg":{"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
legacy := &ClientConfig{}
if err := json.Unmarshal(legacyJSON, legacy); err != nil {
t.Fatalf("unmarshal legacy config: %v", err)
}
if legacy.WG == nil {
t.Fatal("expected legacy WireGuard config")
}
if legacy.WG.Enabled {
t.Fatal("expected an absent enabled marker to remain disabled")
}
enabledJSON := []byte(`{"wg":{"enabled":true,"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
enabled := &ClientConfig{}
if err := json.Unmarshal(enabledJSON, enabled); err != nil {
t.Fatalf("unmarshal enabled config: %v", err)
}
if enabled.WG == nil || !enabled.WG.Enabled {
t.Fatal("expected enabled marker to opt in to WireGuard")
}
}
-3
View File
@@ -52,9 +52,6 @@ func init() {
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
// Create the console client, without any RPC or commands bound to it yet.
// This created before anything so that multiple commands can make use of
-177
View File
@@ -1,177 +0,0 @@
package cli
import (
"fmt"
"io"
"strings"
"time"
"unicode/utf8"
bspinner "charm.land/bubbles/v2/spinner"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
)
const (
minConnectSpinnerDuration = 900 * time.Millisecond
minConnectStatusDuration = 350 * time.Millisecond
)
type connectResult struct {
rpc rpcpb.SliverRPCClient
conn *grpc.ClientConn
err error
}
func connectWithSpinner(out io.Writer, target string, connect func(transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error)) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusCh := make(chan string, 8)
resultCh := make(chan connectResult, 1)
go func() {
rpc, conn, err := connect(func(status string) {
sendStatus(statusCh, status)
})
resultCh <- connectResult{rpc: rpc, conn: conn, err: err}
}()
currentStatus := ""
currentStatusSince := time.Time{}
queuedStatus := ""
lastWidth := 0
spinner := bspinner.New(bspinner.WithSpinner(bspinner.Line))
ticker := time.NewTicker(spinner.Spinner.FPS)
defer ticker.Stop()
startedAt := time.Now()
var pendingResult *connectResult
setStatus := func(status string) {
status = strings.TrimSpace(status)
if status == "" || status == currentStatus {
return
}
currentStatus = status
currentStatusSince = time.Now()
}
stageStatus := func(status string) {
status = strings.TrimSpace(status)
if status == "" {
return
}
if currentStatus == "" || time.Since(currentStatusSince) >= minConnectStatusDuration {
queuedStatus = ""
setStatus(status)
return
}
queuedStatus = status
}
flushQueuedStatus := func() {
if queuedStatus == "" || time.Since(currentStatusSince) < minConnectStatusDuration {
return
}
nextStatus := queuedStatus
queuedStatus = ""
setStatus(nextStatus)
}
canReturnPendingResult := func() bool {
if pendingResult == nil {
return false
}
if pendingResult.err != nil {
return true
}
if time.Since(startedAt) < minConnectSpinnerDuration {
return false
}
if queuedStatus != "" {
return false
}
if !currentStatusSince.IsZero() && time.Since(currentStatusSince) < minConnectStatusDuration {
return false
}
return true
}
render := func() {
line := fmt.Sprintf("%s %s", spinner.View(), formatConnectSpinnerMessage(target, currentStatus))
lastWidth = writeSpinnerLine(out, line, lastWidth)
}
render()
for {
select {
case status := <-statusCh:
stageStatus(status)
spinner, _ = spinner.Update(spinner.Tick())
render()
case result := <-resultCh:
pendingResult = &result
case <-ticker.C:
spinner, _ = spinner.Update(spinner.Tick())
flushQueuedStatus()
render()
if canReturnPendingResult() {
result := *pendingResult
pendingResult = nil
clearSpinnerLine(out, lastWidth)
return result.rpc, result.conn, result.err
}
}
if pendingResult != nil && pendingResult.err != nil {
result := *pendingResult
pendingResult = nil
clearSpinnerLine(out, lastWidth)
return result.rpc, result.conn, result.err
}
}
}
func sendStatus(statusCh chan string, status string) {
status = strings.TrimSpace(status)
if status == "" {
return
}
statusCh <- status
}
func formatConnectSpinnerMessage(target string, status string) string {
target = strings.TrimSpace(target)
status = strings.TrimSpace(status)
if target == "" {
if status == "" {
return "Connecting ..."
}
return fmt.Sprintf("Connecting (%s) ...", status)
}
if status == "" {
return fmt.Sprintf("Connecting to %s ...", target)
}
return fmt.Sprintf("Connecting to %s (%s) ...", target, status)
}
func writeSpinnerLine(out io.Writer, line string, lastWidth int) int {
width := utf8.RuneCountInString(line)
padding := ""
if lastWidth > width {
padding = strings.Repeat(" ", lastWidth-width)
}
fmt.Fprintf(out, "\r%s%s", line, padding)
if width > lastWidth {
return width
}
return lastWidth
}
func clearSpinnerLine(out io.Writer, lastWidth int) {
if lastWidth <= 0 {
return
}
fmt.Fprintf(out, "\r%s\r", strings.Repeat(" ", lastWidth))
}
-132
View File
@@ -1,132 +0,0 @@
package cli
import (
"bytes"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
)
func TestFormatConnectSpinnerMessage(t *testing.T) {
tests := []struct {
name string
target string
status string
want string
}{
{
name: "target only",
target: "127.0.0.1:31337",
want: "Connecting to 127.0.0.1:31337 ...",
},
{
name: "target with status",
target: "127.0.0.1:31337",
status: "wireguard",
want: "Connecting to 127.0.0.1:31337 (wireguard) ...",
},
{
name: "status without target",
status: "grpc/mtls",
want: "Connecting (grpc/mtls) ...",
},
{
name: "trim whitespace",
target: " 127.0.0.1:31337 ",
status: " grpc/mtls over wireguard ",
want: "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := formatConnectSpinnerMessage(test.target, test.status); got != test.want {
t.Fatalf("expected %q, got %q", test.want, got)
}
})
}
}
func TestConnectWithSpinnerOutput(t *testing.T) {
var out bytes.Buffer
spinnerDelay := 2 * (100 * time.Millisecond)
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
statusFn("wireguard")
time.Sleep(spinnerDelay)
statusFn("grpc/mtls over wireguard")
time.Sleep(spinnerDelay)
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
t.Fatalf("expected wireguard status in output, got %q", got)
}
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
t.Fatalf("expected grpc over wireguard status in output, got %q", got)
}
frameCount := 0
for _, frame := range []string{"|", "/", "-", "\\"} {
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337") {
frameCount++
}
}
if frameCount < 2 {
t.Fatalf("expected multiple spinner frames in output, got %q", got)
}
}
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) {
statusFn("grpc/mtls")
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
frameCount := 0
for _, frame := range []string{"|", "/", "-", "\\"} {
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337 (grpc/mtls) ...") {
frameCount++
}
}
if frameCount < 2 {
t.Fatalf("expected multiple spinner frames for fast success, got %q", got)
}
}
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) {
statusFn("wireguard")
statusFn("grpc/mtls over wireguard")
return nil, nil, nil
})
if err != nil {
t.Fatalf("connectWithSpinner returned error: %v", err)
}
got := out.String()
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
t.Fatalf("expected fast success output to include wireguard status, got %q", got)
}
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
t.Fatalf("expected fast success output to include grpc over wireguard status, got %q", got)
}
if strings.Contains(got, "Connected to 127.0.0.1:31337") {
t.Fatalf("did not expect past-tense success output, got %q", got)
}
}
+7 -14
View File
@@ -20,7 +20,6 @@ package cli
import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command"
@@ -45,9 +44,6 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
pre = func(cmd *cobra.Command, _ []string) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}
configs := assets.GetConfigs()
if len(configs) == 0 {
@@ -65,20 +61,17 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
return nil
}
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
// Don't clobber output when simply running an implant command from system shell.
if run {
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
}
var rpc rpcpb.SliverRPCClient
var ln *grpc.ClientConn
// Don't clobber output when simply running an implant command from system shell.
if run {
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
return transport.MTLSConnectWithStatus(config, statusFn)
})
} else {
rpc, ln, err = transport.MTLSConnect(config)
}
rpc, ln, err = transport.MTLSConnect(config)
if err != nil {
fmt.Printf("Connection to server failed %s\n", err)
fmt.Printf("Connection to server failed %s", err)
return nil
}
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
+1 -5
View File
@@ -43,10 +43,6 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
}
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}
configPath, _ := cmd.Flags().GetString("config")
config, err := loadMCPClientConfig(configPath)
if err != nil {
@@ -59,7 +55,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
fmt.Printf("Connection to server failed %s\n", err)
return nil
}
defer transport.CloseGRPCConnection(ln)
defer ln.Close()
con.Rpc = rpc
go handleConnectionLost(ln)
-42
View File
@@ -1,42 +0,0 @@
package cli
import (
"fmt"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
const (
enableWGFlag = "enable-wg"
disableWGFlag = "disable-wg"
)
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if cmd == nil {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
return nil
}
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
if err != nil {
return err
}
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
if err != nil {
return err
}
if cmd.Flags().Changed(enableWGFlag) && cmd.Flags().Changed(disableWGFlag) {
return fmt.Errorf("--%s and --%s cannot be used together", enableWGFlag, disableWGFlag)
}
mode := transport.MultiplayerConnectAuto
switch {
case enableWG:
mode = transport.MultiplayerConnectEnableWG
case disableWG:
mode = transport.MultiplayerConnectDisableWG
}
transport.SetMultiplayerConnectMode(mode)
return nil
}
-136
View File
@@ -1,136 +0,0 @@
package cli
import (
"strings"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
func TestWireGuardOverrideFlagsDefaultToFalse(t *testing.T) {
for _, name := range []string{enableWGFlag, disableWGFlag} {
flag := rootCmd.PersistentFlags().Lookup(name)
if flag == nil {
t.Fatalf("expected --%s persistent flag", name)
}
if flag.DefValue != "false" {
t.Fatalf("expected --%s to default to false, got %q", name, flag.DefValue)
}
}
}
func TestApplyMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if !transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
t.Fatal("expected automatic mode to honor an explicitly enabled WireGuard config")
}
if transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
t.Fatal("expected automatic mode to ignore a legacy WireGuard config without the opt-in marker")
}
}
func TestApplyMultiplayerConnectModeEnableWGForcesLegacyConfig(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if !transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
t.Fatal("expected --enable-wg to force a complete legacy config through WireGuard")
}
}
func TestApplyMultiplayerConnectModeDisableWGOverridesEnabledConfig(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
if err := applyMultiplayerConnectMode(cmd); err != nil {
t.Fatalf("apply multiplayer mode: %v", err)
}
if transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
t.Fatal("expected --disable-wg to force direct mTLS")
}
}
func TestApplyMultiplayerConnectModeRejectsConflictingOverrides(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
err := applyMultiplayerConnectMode(cmd)
if err == nil {
t.Fatal("expected conflicting WireGuard overrides to fail")
}
if !strings.Contains(err.Error(), "cannot be used together") {
t.Fatalf("unexpected conflict error: %v", err)
}
}
func TestApplyMultiplayerConnectModeRejectsBothOverridesWhenExplicitlyFalse(t *testing.T) {
resetMultiplayerConnectModeForTest(t)
cmd := newTransportModeTestCommand()
if err := cmd.Flags().Set(enableWGFlag, "false"); err != nil {
t.Fatalf("set --%s: %v", enableWGFlag, err)
}
if err := cmd.Flags().Set(disableWGFlag, "false"); err != nil {
t.Fatalf("set --%s: %v", disableWGFlag, err)
}
err := applyMultiplayerConnectMode(cmd)
if err == nil {
t.Fatal("expected explicitly combining both WireGuard override flags to fail")
}
if !strings.Contains(err.Error(), "cannot be used together") {
t.Fatalf("unexpected conflict error: %v", err)
}
}
func newTransportModeTestCommand() *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().Bool(enableWGFlag, false, "")
cmd.Flags().Bool(disableWGFlag, false, "")
return cmd
}
func resetMultiplayerConnectModeForTest(t *testing.T) {
t.Helper()
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
t.Cleanup(func() {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
})
}
func enabledWGClientConfig() *assets.ClientConfig {
config := legacyWGClientConfig()
config.WG.Enabled = true
return config
}
func legacyWGClientConfig() *assets.ClientConfig {
return &assets.ClientConfig{
WG: &assets.ClientWGConfig{
ServerPubKey: "server-pub",
ClientPrivateKey: "client-priv",
ClientIP: "100.64.0.2",
},
}
}
-1
View File
@@ -12,7 +12,6 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
## Sub-packages
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
- `alias/` – Implements the 'alias' command group for the Sliver client console.
- `armory/` – Implements the 'armory' command group for the Sliver client console.
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
-5
View File
@@ -1,5 +0,0 @@
# client/command/ai
Implements the `ai` command for the Sliver client console.
The TUI loads server-backed AI conversation threads over gRPC, submits prompts to the server, and refreshes when `AIConversationEvent` updates arrive with assistant replies or failure messages.
-242
View File
@@ -1,242 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"fmt"
"os"
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/termio"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var openAIProgramTTY = tea.OpenTTY
// AICmd launches the AI conversation TUI.
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
if con == nil || con.Rpc == nil {
if con != nil {
con.PrintErrorf("Connect to a server before using `ai`.\n")
}
return
}
listenerID, listener := con.CreateEventListener()
defer con.RemoveEventListener(listenerID)
restoreEventNotifications := con.SuppressEventNotifications()
defer restoreEventNotifications()
model := newAIModel(con, buildAIContext(con), listener)
model.showExperimentalWarningModal()
width, height := 100, 30
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
opts := []tea.ProgramOption{
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
}
ttyOpts, cleanup := configureAIProgramTTY()
if cleanup != nil {
defer cleanup()
}
opts = append(opts, ttyOpts...)
program := tea.NewProgram(model, opts...)
if _, err := program.Run(); err != nil {
con.PrintErrorf("AI TUI error: %s\n", err)
}
}
// Console logging can swap stdout/stderr to pipes so session output can be tee'd
// to disk. Bubble Tea needs a real TTY output to enable terminal features like
// mouse reporting, so bind the AI TUI back to the interactive terminal when
// stdout is no longer the controlling TTY.
func configureAIProgramTTY() ([]tea.ProgramOption, func()) {
stdinTTY := isAITTY(os.Stdin)
stdoutTTY := isAITTY(os.Stdout)
switch {
case !stdinTTY || stdoutTTY:
return nil, nil
case isAITTY(termio.InteractiveInput()) && isAITTY(termio.InteractiveOutput()):
return []tea.ProgramOption{
tea.WithInput(termio.InteractiveInput()),
tea.WithOutput(termio.InteractiveOutput()),
}, nil
default:
inTTY, outTTY, err := openAIProgramTTY()
if err != nil {
return nil, nil
}
return []tea.ProgramOption{
tea.WithInput(inTTY),
tea.WithOutput(outTTY),
}, func() {
_ = inTTY.Close()
if outTTY != inTTY {
_ = outTTY.Close()
}
}
}
}
func isAITTY(file *os.File) bool {
return file != nil && term.IsTerminal(int(file.Fd()))
}
type aiContext struct {
target aiTargetSummary
connection aiConnectionSummary
status string
}
type aiTargetSummary struct {
SessionID string
BeaconID string
Label string
Host string
OS string
Arch string
C2 string
Mode string
Details []string
}
type aiConnectionSummary struct {
Profile string
Server string
Operator string
State string
}
func defaultAITargetSummary() aiTargetSummary {
return aiTargetSummary{
Label: "No active target",
Host: "Select a session or beacon with `use` or ctrl+s",
Mode: "offline preview",
C2: "n/a",
OS: "unknown",
Arch: "unknown",
}
}
func aiSessionTargetSummary(session *clientpb.Session) aiTargetSummary {
if session == nil {
return defaultAITargetSummary()
}
return aiTargetSummary{
SessionID: session.ID,
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
Host: fallback(session.Hostname, "<unknown host>"),
OS: fallback(session.OS, "unknown"),
Arch: fallback(session.Arch, "unknown"),
C2: fallback(session.ActiveC2, "unknown"),
Mode: "interactive session",
Details: []string{
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
fmt.Sprintf("PID: %d", session.PID),
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
},
}
}
func aiBeaconTargetSummary(beacon *clientpb.Beacon) aiTargetSummary {
if beacon == nil {
return defaultAITargetSummary()
}
return aiTargetSummary{
BeaconID: beacon.ID,
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
Host: fallback(beacon.Hostname, "<unknown host>"),
OS: fallback(beacon.OS, "unknown"),
Arch: fallback(beacon.Arch, "unknown"),
C2: fallback(beacon.ActiveC2, "unknown"),
Mode: "asynchronous beacon",
Details: []string{
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
fmt.Sprintf("PID: %d", beacon.PID),
fmt.Sprintf("Remote: %s", fallback(beacon.RemoteAddress, "<unknown>")),
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
},
}
}
func buildAIContext(con *console.SliverClient) aiContext {
ctx := aiContext{
target: defaultAITargetSummary(),
connection: aiConnectionSummary{
Profile: "<disconnected>",
Server: "<unknown>",
Operator: "<unknown>",
State: "idle",
},
status: "Loading AI conversations from the server...",
}
if con != nil {
if details, state, ok := con.CurrentConnection(); ok {
ctx.connection.State = strings.ToLower(state.String())
if details != nil && details.Config != nil {
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
}
}
session, beacon := con.ActiveTarget.Get()
switch {
case session != nil:
ctx.target = aiSessionTargetSummary(session)
case beacon != nil:
ctx.target = aiBeaconTargetSummary(beacon)
}
}
return ctx
}
func fallback(value, def string) string {
value = strings.TrimSpace(value)
if value == "" {
return def
}
return value
}
func formatUnix(ts int64) string {
if ts <= 0 {
return "<unknown>"
}
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
}
-49
View File
@@ -1,49 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
// Commands returns the ai command.
func Commands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
}
// ServerCommands returns the ai command for the top-level client REPL.
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
}
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
return &cobra.Command{
Use: consts.AIStr,
Short: "Open the Sliver AI conversation TUI",
Long: help.GetHelpFor([]string{consts.AIStr}),
Args: cobra.NoArgs,
GroupID: groupID,
Run: func(cmd *cobra.Command, args []string) {
AICmd(cmd, con, args)
},
}
}
-591
View File
@@ -1,591 +0,0 @@
package ai
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"context"
"net"
"sync"
"testing"
"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/rpcpb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
"google.golang.org/protobuf/proto"
)
func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
server := &aiRPCServer{
providersResp: &clientpb.AIProviderConfigs{
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
Config: &clientpb.AIConfigSummary{
Provider: "openai",
Model: "gpt-test",
Valid: true,
},
},
conversationsResp: &clientpb.AIConversations{
Conversations: []*clientpb.AIConversation{
{ID: "conv-1", Provider: "openai", Model: "gpt-test", Title: "First"},
{ID: "conv-2", Provider: "openai", Model: "gpt-test", Title: "Second"},
},
},
conversationByID: map[string]*clientpb.AIConversation{
"conv-1": {
ID: "conv-1",
Provider: "openai",
Model: "gpt-test",
Title: "First",
Messages: []*clientpb.AIConversationMessage{
{Role: "user", Content: "Hello"},
},
},
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")
msg := cmd()
loaded, ok := msg.(aiLoadedMsg)
if !ok {
t.Fatalf("expected aiLoadedMsg, got %T", msg)
}
if loaded.selectedID != "conv-1" {
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
}
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
}
if len(loaded.conversations) != 2 {
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
}
server.mu.Lock()
defer server.mu.Unlock()
if server.getProvidersCalls != 1 {
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
}
if server.getConversationsCalls != 1 {
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
}
if len(server.getConversationReqs) != 1 {
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
}
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
}
}
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
server := &aiRPCServer{
providersResp: &clientpb.AIProviderConfigs{
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
Config: &clientpb.AIConfigSummary{
Provider: "openai",
Model: "gpt-test",
SystemPrompt: "Stay concise.",
Valid: true,
},
},
conversationsResp: &clientpb.AIConversations{},
saveConversationResp: &clientpb.AIConversation{
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "New conversation",
SystemPrompt: "Stay concise.",
},
conversationByID: map[string]*clientpb.AIConversation{
"conv-created": {
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "New conversation",
SystemPrompt: "Stay concise.",
},
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")()
loaded, ok := msg.(aiLoadedMsg)
if !ok {
t.Fatalf("expected aiLoadedMsg, got %T", msg)
}
if loaded.status != "Created a new AI conversation." {
t.Fatalf("unexpected status: %q", loaded.status)
}
if loaded.selectedID != "conv-created" {
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 1 {
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
}
request := server.saveConversationReqs[0]
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
}
if request.GetSystemPrompt() != "Stay concise." {
t.Fatalf("unexpected SaveAIConversation system prompt: %q", request.GetSystemPrompt())
}
}
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
server := &aiRPCServer{
saveConversationResp: &clientpb.AIConversation{
ID: "conv-created",
Provider: "openai",
Model: "gpt-test",
Title: "Explain the workflow.",
},
saveMessageResp: &clientpb.AIConversationMessage{
ID: "msg-1",
ConversationID: "conv-created",
Role: "user",
Content: "Explain the workflow.\n\nWith details.",
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := submitPromptCmd(
&console.SliverClient{Rpc: rpcClient},
aiTargetSummary{SessionID: "session-1"},
nil,
"openai",
"gpt-test",
"Stay concise.",
"Explain the workflow.\n\nWith details.",
)()
submitted, ok := msg.(aiPromptSubmittedMsg)
if !ok {
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
}
if submitted.conversationID != "conv-created" {
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 1 {
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
}
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
}
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
}
if server.saveConversationReqs[0].GetSystemPrompt() != "Stay concise." {
t.Fatalf("unexpected conversation system prompt: %q", server.saveConversationReqs[0].GetSystemPrompt())
}
if len(server.saveMessageReqs) != 1 {
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
}
request := server.saveMessageReqs[0]
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
}
if request.GetContent() != "Explain the workflow.\n\nWith details." {
t.Fatalf("unexpected prompt content: %q", request.GetContent())
}
}
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
server := &aiRPCServer{
saveMessageResp: &clientpb.AIConversationMessage{
ID: "msg-1",
ConversationID: "conv-1",
Role: "user",
Content: "hello",
},
}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := submitPromptCmd(
&console.SliverClient{Rpc: rpcClient},
aiTargetSummary{},
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
"openai",
"gpt-test",
"Stay concise.",
"hello",
)()
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.saveConversationReqs) != 0 {
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
}
if len(server.saveMessageReqs) != 1 {
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
}
request := server.saveMessageReqs[0]
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
t.Fatalf("unexpected conversation settings in message request: %+v", request)
}
}
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
server := &aiRPCServer{}
rpcClient, cleanup := newAITestRPCClient(t, server)
defer cleanup()
msg := deleteConversationCmd(
&console.SliverClient{Rpc: rpcClient},
"conv-1",
"conv-2",
`Deleted "Thread".`,
)()
deleted, ok := msg.(aiConversationDeletedMsg)
if !ok {
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
}
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
t.Fatalf("unexpected delete message: %+v", deleted)
}
server.mu.Lock()
defer server.mu.Unlock()
if len(server.deleteConversationReqs) != 1 {
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
}
if server.deleteConversationReqs[0].GetID() != "conv-1" {
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
}
}
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
eventPayload := &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
Conversation: &clientpb.AIConversation{
ID: "conv-1",
OperatorName: "alice",
},
}
data, err := proto.Marshal(eventPayload)
if err != nil {
t.Fatalf("marshal conversation event: %v", err)
}
listener := make(chan *clientpb.Event, 2)
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
msg := waitForAIConversationEventCmd(listener)()
aiEvent, ok := msg.(aiConversationEventMsg)
if !ok {
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
}
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
}
}
func TestWaitForAIConversationEventCmdReturnsToastMessages(t *testing.T) {
listener := make(chan *clientpb.Event, 1)
listener <- &clientpb.Event{
EventType: consts.ClientToastEvent,
Data: []byte("Session alpha connected"),
Err: "info",
}
msg := waitForAIConversationEventCmd(listener)()
toast, ok := msg.(aiToastMsg)
if !ok {
t.Fatalf("expected aiToastMsg, got %T", msg)
}
if toast.level != "info" || toast.message != "Session alpha connected" {
t.Fatalf("unexpected toast payload: %+v", toast)
}
}
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
msg := waitForAIConversationEventCmd(listener)()
if _, ok := msg.(aiListenerClosedMsg); !ok {
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
}
}
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
model := newAIModel(nil, aiContext{
connection: aiConnectionSummary{Operator: "alice"},
}, listener)
model.loading = false
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
Conversation: &clientpb.AIConversation{
ID: "conv-2",
OperatorName: "bob",
Title: "Shared thread",
},
}})
nextModel := updated.(*aiModel)
if nextModel.loading {
t.Fatal("did not expect shared conversation event to trigger a reload")
}
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
}
if cmd == nil {
t.Fatal("expected shared conversation event to continue listening for events")
}
}
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
listener := make(chan *clientpb.Event)
close(listener)
model := newAIModel(nil, aiContext{
connection: aiConnectionSummary{Operator: "alice"},
}, listener)
model.loading = false
model.awaitingResponse = true
model.currentConversation = &clientpb.AIConversation{
ID: "conv-2",
OperatorName: "alice",
UpdatedAt: 100,
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
Messages: []*clientpb.AIConversationMessage{
{Role: "user", Content: "still waiting"},
},
}
model.conversations = []*clientpb.AIConversation{
{ID: "conv-2", Title: "Current"},
{ID: "conv-1", Title: "Fallback"},
}
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
ConversationID: "conv-2",
Conversation: &clientpb.AIConversation{
ID: "conv-2",
},
}})
nextModel := updated.(*aiModel)
if nextModel.loading {
t.Fatal("did not expect delete tombstone to trigger a reload")
}
if nextModel.currentConversation != nil {
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
}
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
}
if cmd == nil {
t.Fatal("expected delete tombstone to continue listening for events")
}
}
type aiRPCServer struct {
rpcpb.UnimplementedSliverRPCServer
mu sync.Mutex
providersResp *clientpb.AIProviderConfigs
conversationsResp *clientpb.AIConversations
conversationByID map[string]*clientpb.AIConversation
saveConversationResp *clientpb.AIConversation
saveMessageResp *clientpb.AIConversationMessage
sessionsResp *clientpb.Sessions
beaconsResp *clientpb.Beacons
getProvidersCalls int
getConversationsCalls int
getSessionsCalls int
getBeaconsCalls int
getConversationReqs []*clientpb.AIConversationReq
deleteConversationReqs []*clientpb.AIConversationReq
saveConversationReqs []*clientpb.AIConversation
saveMessageReqs []*clientpb.AIConversationMessage
getConversationStart chan struct{}
getConversationWait <-chan struct{}
saveMessageStarted chan struct{}
saveMessageRelease <-chan struct{}
}
func (s *aiRPCServer) GetAIProviders(context.Context, *commonpb.Empty) (*clientpb.AIProviderConfigs, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getProvidersCalls++
if s.providersResp == nil {
return &clientpb.AIProviderConfigs{}, nil
}
return proto.Clone(s.providersResp).(*clientpb.AIProviderConfigs), nil
}
func (s *aiRPCServer) GetAIConversations(context.Context, *commonpb.Empty) (*clientpb.AIConversations, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getConversationsCalls++
if s.conversationsResp == nil {
return &clientpb.AIConversations{}, nil
}
return proto.Clone(s.conversationsResp).(*clientpb.AIConversations), nil
}
func (s *aiRPCServer) GetSessions(context.Context, *commonpb.Empty) (*clientpb.Sessions, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getSessionsCalls++
if s.sessionsResp == nil {
return &clientpb.Sessions{}, nil
}
return proto.Clone(s.sessionsResp).(*clientpb.Sessions), nil
}
func (s *aiRPCServer) GetBeacons(context.Context, *commonpb.Empty) (*clientpb.Beacons, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getBeaconsCalls++
if s.beaconsResp == nil {
return &clientpb.Beacons{}, nil
}
return proto.Clone(s.beaconsResp).(*clientpb.Beacons), nil
}
func (s *aiRPCServer) GetAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*clientpb.AIConversation, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.getConversationReqs = append(s.getConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
if s.getConversationStart != nil {
select {
case s.getConversationStart <- struct{}{}:
default:
}
}
if s.getConversationWait != nil {
s.mu.Unlock()
<-s.getConversationWait
s.mu.Lock()
}
if s.conversationByID == nil {
return &clientpb.AIConversation{}, nil
}
conversation := s.conversationByID[req.GetID()]
if conversation == nil {
return &clientpb.AIConversation{}, nil
}
return proto.Clone(conversation).(*clientpb.AIConversation), nil
}
func (s *aiRPCServer) SaveAIConversation(_ context.Context, req *clientpb.AIConversation) (*clientpb.AIConversation, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.saveConversationReqs = append(s.saveConversationReqs, proto.Clone(req).(*clientpb.AIConversation))
if s.saveConversationResp == nil {
return proto.Clone(req).(*clientpb.AIConversation), nil
}
return proto.Clone(s.saveConversationResp).(*clientpb.AIConversation), nil
}
func (s *aiRPCServer) DeleteAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*commonpb.Empty, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.deleteConversationReqs = append(s.deleteConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
return &commonpb.Empty{}, nil
}
func (s *aiRPCServer) SaveAIConversationMessage(_ context.Context, req *clientpb.AIConversationMessage) (*clientpb.AIConversationMessage, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.saveMessageReqs = append(s.saveMessageReqs, proto.Clone(req).(*clientpb.AIConversationMessage))
if s.saveMessageStarted != nil {
select {
case s.saveMessageStarted <- struct{}{}:
default:
}
}
if s.saveMessageRelease != nil {
s.mu.Unlock()
<-s.saveMessageRelease
s.mu.Lock()
}
if s.saveMessageResp == nil {
return proto.Clone(req).(*clientpb.AIConversationMessage), nil
}
return proto.Clone(s.saveMessageResp).(*clientpb.AIConversationMessage), nil
}
func newAITestRPCClient(t *testing.T, srv rpcpb.SliverRPCServer) (rpcpb.SliverRPCClient, func()) {
t.Helper()
listener := bufconn.Listen(1024 * 1024)
grpcServer := grpc.NewServer()
rpcpb.RegisterSliverRPCServer(grpcServer, srv)
go func() {
_ = grpcServer.Serve(listener)
}()
dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() }
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(dialer),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
grpcServer.Stop()
_ = listener.Close()
t.Fatalf("dial bufconn: %v", err)
}
cleanup := func() {
_ = conn.Close()
grpcServer.Stop()
_ = listener.Close()
}
return rpcpb.NewSliverRPCClient(conn), cleanup
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-251
View File
@@ -1,251 +0,0 @@
package alias
/*
Sliver Implant Framework
Copyright (C) 2021 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 (
"fmt"
"strconv"
"strings"
"github.com/spf13/cobra"
)
// Alias commands run with cobra's DisableFlagParsing enabled (see LoadAlias),
// the same treatment extension commands received for their arguments. Before
// that, an unknown flag such as the payload's own `-pid` was rejected outright
// by pflag ("unknown flag"), and operators had to inject cobra's "--"
// separator (documented on the wiki) to pass arguments through at all (#2264).
//
// With flag parsing disabled cobra hands Run the raw, unstripped slice, and we
// extract the Sliver-owned flags ourselves here. Everything we do not own is
// passed to the payload untouched, so all of these now work:
//
// amsi-bypass -pid 6969
// fakealias --process notepad.exe arg1 arg2
// fakealias -- --anything still-here (existing workaround, unchanged)
type ownedFlagKind int
const (
ownedBool ownedFlagKind = iota
ownedString
ownedUint32
ownedInt64
)
// ownedFlagSpec describes one Sliver-owned flag that splitAliasArgs extracts
// from the raw argument slice before the rest is handed to the alias payload.
type ownedFlagSpec struct {
long string // canonical pflag name, e.g. "process-arguments"
short string // shorthand letter, e.g. "A" ("" if the flag has none)
kind ownedFlagKind // how the value is consumed and validated
}
// baseAliasOwnedFlags are registered for every alias command.
var baseAliasOwnedFlags = []ownedFlagSpec{
{long: "process", short: "p", kind: ownedString},
{long: "process-arguments", short: "A", kind: ownedString},
{long: "ppid", short: "P", kind: ownedUint32},
{long: "save", short: "s", kind: ownedBool},
{long: "timeout", short: "t", kind: ownedInt64},
}
// assemblyAliasOwnedFlags are only owned when the alias wraps a .NET assembly;
// for every other alias they are unknown tokens and pass through to the payload.
var assemblyAliasOwnedFlags = []ownedFlagSpec{
{long: "method", short: "m", kind: ownedString},
{long: "class", short: "c", kind: ownedString},
{long: "app-domain", short: "d", kind: ownedString},
{long: "arch", short: "a", kind: ownedString},
{long: "in-process", short: "i", kind: ownedBool},
{long: "runtime", short: "r", kind: ownedString},
{long: "amsi-bypass", short: "M", kind: ownedBool},
{long: "etw-bypass", short: "E", kind: ownedBool},
}
// aliasOwnedFlagSpecs returns the flag set owned by a particular alias.
func aliasOwnedFlagSpecs(isAssembly bool) []ownedFlagSpec {
if !isAssembly {
return baseAliasOwnedFlags
}
specs := make([]ownedFlagSpec, 0, len(baseAliasOwnedFlags)+len(assemblyAliasOwnedFlags))
specs = append(specs, baseAliasOwnedFlags...)
return append(specs, assemblyAliasOwnedFlags...)
}
// ownedFlagMatch is a raw token that spelled one of the Sliver-owned flags.
type ownedFlagMatch struct {
spec ownedFlagSpec
value string // value from a "--flag=value" / "-f=value" spelling
inline bool // whether the token carried its value inline
}
// splitAliasArgs separates the Sliver-owned flags from the alias payload's own
// arguments in a raw (DisableFlagParsing) argument slice. It accepts the long
// forms --flag, --flag=value, --flag value and the shorthand forms -f, -f=value,
// -f value, mirroring pflag. Two deliberate differences from pflag:
//
// - a glued shorthand value (-pnotepad) does NOT match --process: tokens
// like the payload's own "-pid" must survive intact (#2264);
// - after a literal "--" every token is copied through verbatim, preserving
// the long-standing separator workaround.
//
// The returned map holds the raw string value of each flag that was provided;
// callers re-publish the values with applyOwnedAliasFlags.
func splitAliasArgs(raw []string, specs []ownedFlagSpec) (map[string]string, bool, []string, error) {
values := make(map[string]string)
extArgs := make([]string, 0, len(raw))
byLong, byShort := ownedFlagTables(specs)
passthrough := false
for i := 0; i < len(raw); i++ {
tok := raw[i]
// Once we have seen "--", copy everything through untouched.
if passthrough {
extArgs = append(extArgs, tok)
continue
}
if tok == "--" {
passthrough = true
continue
}
if tok == "--help" || tok == "-h" {
return values, true, extArgs, nil
}
match, ok := matchOwnedFlag(tok, byLong, byShort)
if !ok {
// Not one of ours (including glued shorthands and the payload's
// own flags): the alias owns the token, pass it through untouched.
extArgs = append(extArgs, tok)
continue
}
i = takeOwnedValue(match, raw, i, values)
}
if err := validateOwnedValues(specs, values); err != nil {
return nil, false, nil, err
}
return values, false, extArgs, nil
}
// ownedFlagTables indexes the specs by long name and shorthand.
func ownedFlagTables(specs []ownedFlagSpec) (byLong, byShort map[string]ownedFlagSpec) {
byLong = make(map[string]ownedFlagSpec, len(specs))
byShort = make(map[string]ownedFlagSpec, len(specs))
for _, spec := range specs {
byLong[spec.long] = spec
if spec.short != "" {
byShort[spec.short] = spec
}
}
return byLong, byShort
}
// matchOwnedFlag checks whether tok spells one of the Sliver-owned flags, in
// the long form (--name, --name=value) or shorthand form (-f, -f=value). A
// glued shorthand value (-pnotepad) deliberately does not match: pflag would
// read "-pid" as -p with value "id", which is exactly the mangling that broke
// #2264, so such tokens must fall through to the payload.
func matchOwnedFlag(tok string, byLong, byShort map[string]ownedFlagSpec) (ownedFlagMatch, bool) {
var match ownedFlagMatch
var name, value string
var hasValue, isShort bool
switch {
case strings.HasPrefix(tok, "--"):
name, value, hasValue = strings.Cut(tok[2:], "=")
case strings.HasPrefix(tok, "-") && len(tok) > 1:
name, value, hasValue = strings.Cut(tok[1:], "=")
isShort = len(name) == 1
default:
return match, false
}
spec, ok := byLong[name]
if isShort {
spec, ok = byShort[name]
}
if !ok {
return match, false
}
match.spec, match.value, match.inline = spec, value, hasValue
return match, true
}
// takeOwnedValue records the flag's value in values and returns the index of
// the last raw token it consumed. The "--flag value" / "-f value" spellings
// consume the following token; a bool without an inline value defaults to
// true; a value flag as the very last token is dropped and keeps its default,
// mirroring the extension behaviour.
func takeOwnedValue(match ownedFlagMatch, raw []string, i int, values map[string]string) int {
switch {
case match.spec.kind == ownedBool:
if !match.inline {
match.value = "true"
}
values[match.spec.long] = match.value
case match.inline:
values[match.spec.long] = match.value
case i+1 < len(raw):
values[match.spec.long] = raw[i+1]
return i + 1
}
return i
}
// validateOwnedValues rejects non-numeric values for the numeric flag kinds,
// the way pflag's typed values would have.
func validateOwnedValues(specs []ownedFlagSpec, values map[string]string) error {
for _, spec := range specs {
value, provided := values[spec.long]
if !provided {
continue
}
var err error
switch spec.kind {
case ownedUint32:
_, err = strconv.ParseUint(value, 10, 32)
case ownedInt64:
_, err = strconv.ParseInt(value, 10, 64)
}
if err != nil {
return fmt.Errorf("invalid value %q for --%s", value, spec.long)
}
}
return nil
}
// applyOwnedAliasFlags republishes the manually parsed Sliver-owned flag values
// onto the command's flag set. DisableFlagParsing prevents pflag from
// populating them, so readers such as runAliasCommand's Get* calls and
// ActiveTarget.Request (which derives the gRPC timeout from --timeout) would
// otherwise observe only the defaults.
func applyOwnedAliasFlags(cmd *cobra.Command, values map[string]string) error {
for name, value := range values {
flag := cmd.Flags().Lookup(name)
if flag == nil {
continue // e.g. an assembly flag on a non-assembly alias
}
if err := flag.Value.Set(value); err != nil {
return fmt.Errorf("invalid value %q for --%s: %w", value, name, err)
}
}
return nil
}
-259
View File
@@ -1,259 +0,0 @@
package alias
/*
Sliver Implant Framework
Copyright (C) 2021 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 (
"reflect"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// TestSplitAliasArgs covers the argument passthrough regression from #2264:
// the alias payload's own flags must reach the payload intact, while the
// Sliver-owned flags are extracted and validated.
func TestSplitAliasArgs(t *testing.T) {
base := aliasOwnedFlagSpecs(false)
assembly := aliasOwnedFlagSpecs(true)
cases := []struct {
name string
specs []ownedFlagSpec
in []string
values map[string]string
help bool
extArgs []string
wantErr bool
}{
{
name: "payload flag without -- separator (#2264 regression)",
specs: base,
in: []string{"-pid", "6969"},
values: map[string]string{},
extArgs: []string{"-pid", "6969"},
},
{
name: "glued shorthand is not ours and passes through",
specs: base,
in: []string{"-pid"},
values: map[string]string{},
extArgs: []string{"-pid"},
},
{
name: "payload flag with -- separator (existing workaround)",
specs: base,
in: []string{"--", "--pid", "6969"},
values: map[string]string{},
extArgs: []string{"--pid", "6969"},
},
{
name: "owned flag after -- belongs to the payload",
specs: base,
in: []string{"--", "--save"},
values: map[string]string{},
extArgs: []string{"--save"},
},
{
name: "long flag with separate value, then payload args",
specs: base,
in: []string{"--process", "notepad.exe", "arg1", "arg2"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1", "arg2"},
},
{
name: "long flag with equals value",
specs: base,
in: []string{"--process=notepad.exe", "arg1"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1"},
},
{
name: "shorthand with separate value",
specs: base,
in: []string{"-p", "notepad.exe", "arg1"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{"arg1"},
},
{
name: "shorthand with equals value",
specs: base,
in: []string{"-p=notepad.exe"},
values: map[string]string{"process": "notepad.exe"},
extArgs: []string{},
},
{
name: "bool shorthand and long false form",
specs: base,
in: []string{"-s", "--save=false", "arg1"},
values: map[string]string{"save": "false"},
extArgs: []string{"arg1"},
},
{
name: "quoted value containing spaces stays a single argument",
specs: base,
in: []string{"-A", "-kalc -kdisable", "arg1"},
values: map[string]string{"process-arguments": "-kalc -kdisable"},
extArgs: []string{"arg1"},
},
{
name: "valid uint32 and int64 values",
specs: base,
in: []string{"-P", "1234", "--timeout=90"},
values: map[string]string{"ppid": "1234", "timeout": "90"},
extArgs: []string{},
},
{
name: "invalid uint32 value errors",
specs: base,
in: []string{"--ppid", "notanumber"},
wantErr: true,
},
{
name: "invalid int64 value errors",
specs: base,
in: []string{"--timeout=x"},
wantErr: true,
},
{
name: "assembly flags are owned for assembly aliases",
specs: assembly,
in: []string{"--method", "Run", "--class", "Prog", "-i", "-M", "--arch", "x64", "payloadArg"},
values: map[string]string{
"method": "Run", "class": "Prog", "in-process": "true",
"amsi-bypass": "true", "arch": "x64",
},
extArgs: []string{"payloadArg"},
},
{
name: "assembly flags pass through for non-assembly aliases",
specs: base,
in: []string{"--method", "Run"},
values: map[string]string{},
extArgs: []string{"--method", "Run"},
},
{
name: "help flags are detected",
specs: base,
in: []string{"--help"},
help: true,
values: map[string]string{},
extArgs: []string{},
},
{
name: "short help is detected",
specs: base,
in: []string{"-h"},
help: true,
values: map[string]string{},
extArgs: []string{},
},
{
name: "value flag as last token keeps its default",
specs: base,
in: []string{"arg1", "--process"},
values: map[string]string{},
extArgs: []string{"arg1"},
},
{
name: "empty input",
specs: base,
in: nil,
values: map[string]string{},
extArgs: []string{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
values, help, extArgs, err := splitAliasArgs(tc.in, tc.specs)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got values=%#v help=%v extArgs=%#v", values, help, extArgs)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if help != tc.help {
t.Errorf("help: got %v, want %v", help, tc.help)
}
if !reflect.DeepEqual(values, tc.values) {
t.Errorf("values: got %#v, want %#v", values, tc.values)
}
if !reflect.DeepEqual(extArgs, tc.extArgs) {
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
}
})
}
}
// TestApplyOwnedAliasFlags checks that manually parsed values are re-published
// on the command's flag set for every value type an alias can own.
func TestApplyOwnedAliasFlags(t *testing.T) {
cmd := &cobra.Command{}
f := pflag.NewFlagSet("alias-test", pflag.ContinueOnError)
f.StringP("process", "p", "", "host process")
f.StringP("process-arguments", "A", "", "host process arguments")
f.Uint32P("ppid", "P", 0, "parent pid")
f.BoolP("save", "s", false, "save output")
f.Int64P("timeout", "t", 60, "timeout")
f.BoolP("amsi-bypass", "M", false, "amsi bypass")
cmd.Flags().AddFlagSet(f)
err := applyOwnedAliasFlags(cmd, map[string]string{
"process": "notepad.exe",
"process-arguments": "-k foo",
"ppid": "1234",
"save": "true",
"timeout": "90",
"amsi-bypass": "true",
})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
for name, want := range map[string]interface{}{
"process": "notepad.exe", "process-arguments": "-k foo",
} {
got, _ := cmd.Flags().GetString(name)
if got != want.(string) {
t.Errorf("%s: got %q, want %q", name, got, want)
}
}
if ppid, _ := cmd.Flags().GetUint32("ppid"); ppid != 1234 {
t.Errorf("ppid: got %d, want 1234", ppid)
}
if save, _ := cmd.Flags().GetBool("save"); !save {
t.Error("save: got false, want true")
}
if timeout, _ := cmd.Flags().GetInt64("timeout"); timeout != 90 {
t.Errorf("timeout: got %d, want 90", timeout)
}
if amsi, _ := cmd.Flags().GetBool("amsi-bypass"); !amsi {
t.Error("amsi-bypass: got false, want true")
}
// Unset flags keep their defaults.
if inProc := cmd.Flags().Lookup("in-process"); inProc != nil {
t.Error("in-process should not be registered on this command")
}
}
+7 -37
View File
@@ -215,6 +215,8 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
}
}
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
// for each alias command, add a new app command
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
@@ -225,14 +227,9 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
Run: func(cmd *cobra.Command, args []string) {
runAliasCommand(cmd, con, args)
},
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
// DisableFlagParsing keeps the alias payload's own flags (which are
// unknown to pflag) from being rejected or swallowed; we split the
// Sliver-owned flags out of the raw slice ourselves in
// runAliasCommand (#2264, same treatment as extensions).
DisableFlagParsing: true,
GroupID: consts.AliasHelpGroup,
Annotations: makeAliasPlatformFilters(aliasManifest),
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
GroupID: consts.AliasHelpGroup,
Annotations: makeAliasPlatformFilters(aliasManifest),
}
if aliasManifest.IsAssembly {
@@ -253,7 +250,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
f.BoolP("save", "s", false, "Save output to disk")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
@@ -336,27 +333,6 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
return
}
aliasManifest := loadedAlias.Manifest
// The command runs with DisableFlagParsing, so args is the raw, unstripped
// slice. Separate the Sliver-owned flags from the payload's own arguments
// and re-publish them on the flag set (so the Get* readers below and
// Request()'s --timeout still work), then hand only the payload arguments
// to the execution paths (#2264, same treatment as extensions in #2323).
ownedValues, helpRequested, payloadArgs, err := splitAliasArgs(args, aliasOwnedFlagSpecs(aliasManifest.IsAssembly))
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if helpRequested {
_ = cmd.Help()
return
}
if err := applyOwnedAliasFlags(cmd, ownedValues); err != nil {
con.PrintErrorf("%s\n", err)
return
}
args = payloadArgs
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
if err != nil {
con.PrintErrorf("Fail to find alias file: %s\n", err)
@@ -424,13 +400,7 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
var outFilePath *os.File
if save, _ := cmd.Flags().GetBool("save"); save {
hostname := ""
if session != nil {
hostname = session.GetHostname()
} else if beacon != nil {
hostname = beacon.GetHostname()
}
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(hostname)))
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
outFilePath, err = os.CreateTemp("", outFile)
if err != nil {
con.PrintErrorf("%s\n", err)
+1 -1
View File
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Something is wrong with this entry
pkgCache.Delete(key)
pkgCache.Delete(value)
return true
}
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
-18
View File
@@ -1,23 +1,5 @@
package armory
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
+24 -34
View File
@@ -301,44 +301,32 @@ func getCommandsInCache(armoryPK string) []string {
return commandNames
}
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
if cacheEntry.Pkg.IsAlias {
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
}
if cacheEntry.Extension == nil {
return false
}
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
return true
}
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
return true
}
}
return false
}
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
packages := []*pkgCacheEntry{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.LastErr != nil || !packageMatchesInstallName(cacheEntry, name) {
return true
}
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
return true
}
packageVersion := ""
if cacheEntry.Pkg.IsAlias {
packageVersion = cacheEntry.Alias.Version
} else {
packageVersion = cacheEntry.Extension.Version
}
if minimumVersion == "" || packageVersion >= minimumVersion {
packages = append(packages, &cacheEntry)
if cacheEntry.LastErr == nil {
if cacheEntry.Pkg.IsAlias {
if cacheEntry.Alias.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
}
}
} else {
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
break
}
}
}
}
}
return true
})
@@ -744,7 +732,9 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
con.Printf(console.Clearln + "\r") // Clear download message
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
return nil
}
func writeArmoryTempFile(data []byte) (string, error) {
-218
View File
@@ -1,218 +0,0 @@
package armory
import (
"errors"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/extensions"
)
func resetPackageCache(t *testing.T) {
t.Helper()
pkgCache.Range(func(key, _ any) bool {
pkgCache.Delete(key)
return true
})
t.Cleanup(func() {
pkgCache.Range(func(key, _ any) bool {
pkgCache.Delete(key)
return true
})
})
}
func testExtensionCacheEntry() pkgCacheEntry {
manifest := &extensions.ExtensionManifest{
Name: "ADCS Request (Remote)",
PackageName: "remote-adcs-package",
Version: "v0.1.5",
ArmoryName: "Default Armory",
ArmoryPK: "armory-public-key",
ExtCommand: []*extensions.ExtCommand{
{CommandName: "remote-adcs-request"},
{CommandName: "remote-adcs-request-on-behalf"},
},
}
return pkgCacheEntry{
ArmoryConfig: &assets.ArmoryConfig{PublicKey: "armory-public-key"},
Pkg: ArmoryPackage{
Name: "Remote ADCS package",
CommandName: "remote-adcs-index",
},
Extension: manifest,
ID: "remote-adcs-request-id",
}
}
func TestGetPackageForCommandAcceptsStableExtensionIdentities(t *testing.T) {
resetPackageCache(t)
entry := testExtensionCacheEntry()
pkgCache.Store(entry.ID, entry)
for _, name := range []string{
"remote-adcs-index",
"remote-adcs-request",
"remote-adcs-request-on-behalf",
"remote-adcs-package",
} {
t.Run(name, func(t *testing.T) {
found, err := getPackageForCommand(name, "armory-public-key", "v0.1.5")
if err != nil {
t.Fatalf("lookup %q failed: %v", name, err)
}
if found.ID != entry.ID {
t.Fatalf("lookup %q returned %q, want %q", name, found.ID, entry.ID)
}
})
}
if _, err := getPackageForCommand("remote-adcs-package", "other-armory", "v0.1.5"); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("wrong-armory lookup error = %v, want %v", err, ErrPackageNotFound)
}
if _, err := getPackageForCommand("remote-adcs-package", "armory-public-key", "v0.1.6"); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("newer-version lookup error = %v, want %v", err, ErrPackageNotFound)
}
for _, name := range []string{"ADCS Request (Remote)", "Remote ADCS package", "unknown-package"} {
if _, err := getPackageForCommand(name, "", ""); !errors.Is(err, ErrPackageNotFound) {
t.Fatalf("unstable identity %q lookup error = %v, want %v", name, err, ErrPackageNotFound)
}
}
}
func TestPackageMatchesInstallNameLeavesAliasesCommandOnly(t *testing.T) {
entry := pkgCacheEntry{
Pkg: ArmoryPackage{
Name: "Human Alias Name",
CommandName: "alias-command",
IsAlias: true,
},
Alias: &alias.AliasManifest{CommandName: "alias-command"},
}
if packageMatchesInstallName(entry, "Human Alias Name") {
t.Fatal("alias unexpectedly matched its display name")
}
if !packageMatchesInstallName(entry, "alias-command") {
t.Fatal("alias did not match its command name")
}
}
func TestExtensionManifestsMatch(t *testing.T) {
manifest := func(name, packageName string, commands ...string) *extensions.ExtensionManifest {
extCommands := make([]*extensions.ExtCommand, 0, len(commands))
for _, command := range commands {
extCommands = append(extCommands, &extensions.ExtCommand{CommandName: command})
}
return &extensions.ExtensionManifest{Name: name, PackageName: packageName, ExtCommand: extCommands}
}
tests := []struct {
name string
local *extensions.ExtensionManifest
latest *extensions.ExtensionManifest
want bool
}{
{
name: "v2 package identity",
local: manifest("Old Display Name", "stable-package", "old-command"),
latest: manifest("New Display Name", "stable-package", "new-command"),
want: true,
},
{
name: "different v2 packages",
local: manifest("Shared Display Name", "package-one", "shared-command"),
latest: manifest("Shared Display Name", "package-two", "shared-command"),
want: false,
},
{
name: "v2 does not downgrade to legacy identity",
local: manifest("Human Display Name", "stable-package", "shared-command"),
latest: manifest("legacy-command", "", "shared-command"),
want: false,
},
{
name: "legacy command identity",
local: manifest("legacy-command", "", "legacy-command"),
latest: manifest("Human Display Name", "stable-package", "other-command", "legacy-command"),
want: true,
},
{
name: "display names are not identity",
local: manifest("Shared Name", "", "old-command"),
latest: manifest("Shared Name", "stable-package", "new-command"),
want: false,
},
{
name: "unrelated legacy package",
local: manifest("Old Name", "", "old-command"),
latest: manifest("New Name", "stable-package", "new-command"),
want: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := extensionManifestsMatch(test.local, test.latest); got != test.want {
t.Fatalf("extensionManifestsMatch() = %t, want %t", got, test.want)
}
})
}
}
func TestExtensionUpdateDisplayNameResolvesPackage(t *testing.T) {
resetPackageCache(t)
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
installedManifest := []byte(`{
"name":"ADCS Request (Remote)",
"package_name":"remote-adcs-package",
"version":"v0.1.3",
"commands":[{
"command_name":"remote-adcs-request",
"help":"Request a certificate",
"files":[{"os":"windows","arch":"amd64","path":"adcs_request.x64.o"}]
}]
}`)
installDir := filepath.Join(assets.GetExtensionsDir(), "remote-adcs-request")
if err := os.MkdirAll(installDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(installDir, extensions.ManifestFileName), installedManifest, 0o600); err != nil {
t.Fatal(err)
}
entry := testExtensionCacheEntry()
pkgCache.Store(entry.ID, entry)
otherEntry := testExtensionCacheEntry()
otherEntry.ID = "other-armory-package-id"
otherEntry.ArmoryConfig = &assets.ArmoryConfig{PublicKey: "other-armory"}
otherManifest := *otherEntry.Extension
otherManifest.Version = "v9.0.0"
otherManifest.ArmoryName = "Other Armory"
otherManifest.ArmoryPK = "other-armory"
otherEntry.Extension = &otherManifest
pkgCache.Store(otherEntry.ID, otherEntry)
updates := checkForExtensionUpdates("armory-public-key")
versionInfo, ok := updates["ADCS Request (Remote)"]
if !ok {
t.Fatal("display-name update was not detected")
}
if versionInfo.OldVersion != "v0.1.3" || versionInfo.NewVersion != "v0.1.5" {
t.Fatalf("unexpected versions: %#v", versionInfo)
}
if versionInfo.PackageID != entry.ID {
t.Fatalf("update package ID = %q, want %q", versionInfo.PackageID, entry.ID)
}
found := packageCacheLookupByID(versionInfo.PackageID)
if found == nil {
t.Fatal("detected update package is missing from the cache")
}
if found.ID != entry.ID {
t.Fatalf("resolved package %q, want %q", found.ID, entry.ID)
}
}
-18
View File
@@ -1,23 +1,5 @@
package armory
/*
Sliver Implant Framework
Copyright (C) 2026 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 (
"fmt"
"os"
+11 -36
View File
@@ -40,7 +40,6 @@ type VersionInformation struct {
OldVersion string
NewVersion string
ArmoryName string
PackageID string
}
type PackageType uint
@@ -125,9 +124,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if !ok {
continue
}
updatedPackage := packageCacheLookupByID(extVersionInfo.PackageID)
if updatedPackage == nil {
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, ErrPackageNotFound)
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
if err != nil {
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
continue
}
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
@@ -174,21 +173,8 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
return results
}
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
if localManifest.PackageName != "" {
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
}
for _, localCommand := range localManifest.ExtCommand {
for _, latestCommand := range latestManifest.ExtCommand {
if localCommand.CommandName == latestCommand.CommandName {
return true
}
}
}
return false
}
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
_, cachedExtensions := packageManifestsInCache()
// Return a map of extension name to minimum version to install
results := make(map[string]VersionInformation)
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
@@ -200,12 +186,7 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
if err != nil {
continue
}
pkgCache.Range(func(_, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
return true
}
latestExt := cacheEntry.Extension
for _, latestExt := range cachedExtensions {
/*
We used to check if the version identifiers were different between the installed version and the
version in the armory. This worked when we had one armory, but with multiple potential armories,
@@ -213,22 +194,16 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
so we will rely on the author of the package incrementing the version identifier somehow to determine
if a package is newer.
*/
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
current, found := results[localManifest.Name]
if !found || latestExt.Version > current.NewVersion ||
(latestExt.Version == current.NewVersion && cacheEntry.ID < current.PackageID) {
results[localManifest.Name] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestExt.Version,
ArmoryName: latestExt.ArmoryName,
PackageID: cacheEntry.ID,
}
results[localManifest.Name] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestExt.Version,
ArmoryName: latestExt.ArmoryName,
}
}
}
return true
})
}
}
return results
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
-31
View File
@@ -1,31 +0,0 @@
package docs
import (
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
// Commands returns the docs command.
func Commands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newDocsCommand(consts.SliverCoreHelpGroup, con)}
}
// ServerCommands returns the docs command for the top-level client REPL.
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{newDocsCommand(consts.GenericHelpGroup, con)}
}
func newDocsCommand(groupID string, con *console.SliverClient) *cobra.Command {
return &cobra.Command{
Use: consts.DocsStr,
Short: "Browse the embedded Sliver docs in a TUI",
Long: help.GetHelpFor([]string{consts.DocsStr}),
Args: cobra.NoArgs,
GroupID: groupID,
Run: func(cmd *cobra.Command, args []string) {
DocsCmd(cmd, con, args)
},
}
}
-36
View File
@@ -1,36 +0,0 @@
package docs
import (
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
)
// DocsCmd launches the embedded docs browser TUI.
func DocsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
entries, err := loadDocEntries()
if err != nil {
if con != nil {
con.PrintErrorf("Failed to load embedded docs: %s\n", err)
}
return
}
model := newDocsModel(entries)
width, height := 100, 32
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
if _, err := program.Run(); err != nil && con != nil {
con.PrintErrorf("Docs TUI error: %s\n", err)
}
}
File diff suppressed because it is too large. Load diff
-199
View File
@@ -1,199 +0,0 @@
package docs
import (
"strings"
"testing"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
)
func sampleDocs() []docEntry {
return []docEntry{
{
Name: "Getting Started",
Content: "# Getting Started\n\n- first step\n- second step",
Description: "Getting Started",
},
{
Name: "Operators",
Content: "# Operators\n\nTeam workflows",
Description: "Team workflows",
},
{
Name: "Networking",
Content: "# Networking\n\nTransport details",
Description: "Transport details",
},
}
}
func TestRenderMarkdownWithGlowRendersBulletLists(t *testing.T) {
rendered, err := renderMarkdownWithGlow(48, "# Title\n\n- first item")
if err != nil {
t.Fatalf("renderMarkdownWithGlow(): %v", err)
}
plain := ansi.Strip(rendered)
if !strings.Contains(plain, "Title") {
t.Fatalf("expected heading text, got %q", plain)
}
if !strings.Contains(plain, "• first item") {
t.Fatalf("expected glow-style bullet rendering, got %q", plain)
}
}
func TestSummarizeMarkdownPrefersFirstMeaningfulLine(t *testing.T) {
summary := summarizeMarkdown("# Title\n\n- Bullet\n\nParagraph")
if summary != "Title" {
t.Fatalf("expected first heading summary, got %q", summary)
}
}
func TestSummarizeMarkdownStripsMarkdownAndUnicodeSymbols(t *testing.T) {
summary := summarizeMarkdown("⚠️ **IMPORTANT:** This content was NOT created by the Sliver authors.")
if summary != "IMPORTANT: This content was NOT created by the Sliver authors." {
t.Fatalf("expected sanitized browser summary, got %q", summary)
}
}
func TestDocsModelDefaultsToGettingStarted(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 32)
if model.currentDocName != "Getting Started" {
t.Fatalf("expected Getting Started to be selected, got %q", model.currentDocName)
}
plain := ansi.Strip(model.viewer.View())
if !strings.Contains(plain, "first step") {
t.Fatalf("expected initial viewer content to render selected doc, got %q", plain)
}
}
func TestDocsModelSlashFocusesBrowserAndStartsFiltering(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 32)
model.focus = docsFocusViewer
updated, cmd := model.Update(tea.KeyPressMsg{Text: "/", Code: '/'})
if cmd == nil {
t.Fatal("expected slash to focus the search input")
}
updatedModel := updated.(*docsModel)
if updatedModel.focus != docsFocusBrowser {
t.Fatalf("expected slash to move focus to browser, got %v", updatedModel.focus)
}
if updatedModel.filterState != docsFilterStateFiltering {
t.Fatalf("expected slash to enter filtering state, got %v", updatedModel.filterState)
}
}
func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 32)
model.startFiltering()
model.browserFilter.SetValue("oper")
model.updateFilteredEntries(false)
visible := model.visibleEntries()
if len(visible) != 1 || visible[0].Name != "Operators" {
t.Fatalf("expected Operators to be the only filtered result, got %+v", visible)
}
if model.currentDocName != "Operators" {
t.Fatalf("expected filtering to sync selection to Operators, got %q", model.currentDocName)
}
}
func TestDocsModelEnterMovesFocusToViewer(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 32)
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
updatedModel := updated.(*docsModel)
if updatedModel.focus != docsFocusViewer {
t.Fatalf("expected enter to focus viewer, got %v", updatedModel.focus)
}
}
func TestDocsModelEscClearsAppliedFilter(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 32)
model.startFiltering()
model.browserFilter.SetValue("net")
model.finishFiltering(false)
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
updatedModel := updated.(*docsModel)
if updatedModel.filterState != docsFilterStateUnfiltered {
t.Fatalf("expected esc to clear the filter, got %v", updatedModel.filterState)
}
if len(updatedModel.visibleEntries()) != len(sampleDocs()) {
t.Fatalf("expected filter clear to restore all docs, got %d visible", len(updatedModel.visibleEntries()))
}
}
func TestDocsModelBrowserBodyPinsHelpToBottom(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(120, 28)
body := ansi.Strip(model.renderBrowserBody(model.browserWidth, model.browserHeight))
lines := strings.Split(body, "\n")
if len(lines) != model.browserHeight {
t.Fatalf("expected browser body height %d, got %d lines", model.browserHeight, len(lines))
}
if !strings.Contains(lines[len(lines)-1], "q quit") {
t.Fatalf("expected help controls on the last browser line, got %q", lines[len(lines)-1])
}
}
func TestDocsWindowPollSchedulesResizeAndKeepsPolling(t *testing.T) {
model := newDocsModel(sampleDocs())
model.applyWindowSize(100, 28)
updated, cmd := model.Update(docsWindowPollMsg{width: 120, height: 34})
if cmd == nil {
t.Fatal("expected window poll to schedule resize handling")
}
updatedModel := updated.(*docsModel)
if updatedModel.width != 100 || updatedModel.height != 28 {
t.Fatalf("expected poll tick to leave dimensions unchanged until WindowSizeMsg, got %dx%d", updatedModel.width, updatedModel.height)
}
msg := cmd()
batch, ok := msg.(tea.BatchMsg)
if !ok {
t.Fatalf("expected poll tick to return tea.BatchMsg, got %T", msg)
}
if len(batch) != 2 {
t.Fatalf("expected poll tick batch size 2, got %d", len(batch))
}
var sawWindowSize bool
var sawPollTick bool
for _, subcmd := range batch {
if subcmd == nil {
continue
}
submsg := subcmd()
switch msg := submsg.(type) {
case tea.WindowSizeMsg:
sawWindowSize = true
if msg.Width != 120 || msg.Height != 34 {
t.Fatalf("expected resize 120x34, got %dx%d", msg.Width, msg.Height)
}
case docsWindowPollMsg:
sawPollTick = true
}
}
if !sawWindowSize {
t.Fatal("expected poll tick to emit WindowSizeMsg")
}
if !sawPollTick {
t.Fatal("expected poll tick to schedule another poll")
}
}
-8
View File
@@ -114,14 +114,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
+1 -219
View File
@@ -20,44 +20,27 @@ package exec
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
const (
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
imageFileDLLMask = 0x2000
)
// ExecuteShellcodeCmd - Execute shellcode in-memory.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
interactive, _ := cmd.Flags().GetBool("interactive")
@@ -77,27 +60,6 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
return
}
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
if shouldConvertPE {
if targetOS != "windows" {
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
return
}
con.PrintInfof("Converting PE input to shellcode ...\n")
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
if err != nil {
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
return
}
}
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
if shikataGaNai {
if !rwxPages {
@@ -134,7 +96,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return
}
ctrl := make(chan bool)
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
con.SpinUntil(msg, ctrl)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
Data: shellcodeBin,
@@ -173,186 +135,6 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
}
}
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
cmd.Flags().Changed("shellcode-compress") ||
cmd.Flags().Changed("shellcode-exitopt") ||
cmd.Flags().Changed("shellcode-bypass") ||
cmd.Flags().Changed("shellcode-headers") ||
cmd.Flags().Changed("shellcode-thread") ||
cmd.Flags().Changed("shellcode-unicode") ||
cmd.Flags().Changed("shellcode-oep")
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
}
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
}
if shellcodeBypass < 1 || shellcodeBypass > 3 {
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
}
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
}
shellcodeCompress := uint32(1)
if shellcodeCompressEnabled {
shellcodeCompress = 2
}
return &clientpb.ShellcodeConfig{
Entropy: shellcodeEntropy,
Compress: shellcodeCompress,
ExitOpt: shellcodeExitOpt,
Bypass: shellcodeBypass,
Headers: shellcodeHeaders,
Thread: shellcodeThread,
Unicode: shellcodeUnicode,
OEP: shellcodeOEP,
}, anyChanged, nil
}
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
ext := strings.ToLower(filepath.Ext(shellcodePath))
isPEByExt := ext == ".exe" || ext == ".dll"
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
}
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetName()
}
if beacon != nil {
return beacon.GetName()
}
return "target"
}
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return strings.ToLower(session.GetOS())
}
if beacon != nil {
return strings.ToLower(beacon.GetOS())
}
return ""
}
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetArch()
}
if beacon != nil {
return beacon.GetArch()
}
return ""
}
type executeShellcodeDonutOptions struct {
entropy int
compress int
exitOpt int
bypass int
headers int
thread bool
unicode bool
oep uint32
}
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
opts := executeShellcodeDonutOptions{
entropy: defaultExecuteShellcodeDonutEntropy,
compress: defaultExecuteShellcodeDonutCompress,
exitOpt: defaultExecuteShellcodeDonutExitOpt,
bypass: defaultExecuteShellcodeDonutBypass,
headers: defaultExecuteShellcodeDonutHeaders,
}
if config == nil {
return opts
}
if config.Entropy >= 1 && config.Entropy <= 3 {
opts.entropy = int(config.Entropy)
}
if config.Compress >= 1 && config.Compress <= 2 {
opts.compress = int(config.Compress)
}
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
opts.exitOpt = int(config.ExitOpt)
}
if config.Bypass >= 1 && config.Bypass <= 3 {
opts.bypass = int(config.Bypass)
}
if config.Headers >= 1 && config.Headers <= 2 {
opts.headers = int(config.Headers)
}
opts.thread = config.Thread
opts.unicode = config.Unicode
if config.OEP > 0 {
opts.oep = config.OEP
}
return opts
}
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return nil, err
}
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
ext := ".exe"
if isDLL {
ext = ".dll"
}
opts := normalizeExecuteShellcodeDonutConfig(config)
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
Ext: ext,
Arch: getExecuteShellcodeDonutArch(arch),
Bypass: opts.bypass,
Headers: opts.headers,
Entropy: opts.entropy,
Compress: opts.compress,
ExitOpt: opts.exitOpt,
Thread: opts.thread,
Unicode: opts.unicode,
OEP: opts.oep,
})
if err != nil {
return nil, err
}
return addExecuteShellcodeStackCheck(result.Loader), nil
}
func getExecuteShellcodeDonutArch(arch string) int {
donutArch := wasmdonut.DonutArchX84
switch strings.ToLower(arch) {
case "x32", "x86", "386":
donutArch = wasmdonut.DonutArchX86
case "x64", "amd64":
donutArch = wasmdonut.DonutArchX64
case "x84":
donutArch = wasmdonut.DonutArchX84
}
return donutArch
}
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
stackCheckPrologue := []byte{
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
}
return append(stackCheckPrologue, shellcode...)
}
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
// Check active session
session := con.ActiveTarget.GetSessionInteractive()
@@ -1,100 +0,0 @@
package exec
import (
"testing"
"github.com/spf13/cobra"
)
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if changed {
t.Fatalf("expected changed=false with defaults")
}
if cfg == nil {
t.Fatalf("expected non-nil config")
}
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
t.Fatalf("set shellcode-entropy: %v", err)
}
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
t.Fatalf("set shellcode-compress: %v", err)
}
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
t.Fatalf("set shellcode-thread: %v", err)
}
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if !changed {
t.Fatalf("expected changed=true")
}
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
t.Fatalf("unexpected changed config: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
t.Fatalf("set shellcode-bypass: %v", err)
}
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
t.Fatalf("expected validation error for shellcode-bypass")
}
}
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
testCases := []struct {
name string
path string
flagsChanged bool
wantConvert bool
wantDLL bool
}{
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
}
})
}
}
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
}
-123
View File
@@ -1,123 +0,0 @@
package extensions
/*
Sliver Implant Framework
Copyright (C) 2021 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 (
"strconv"
"strings"
"github.com/spf13/cobra"
)
// splitExtensionArgs separates Sliver-owned flags (--save, --timeout, --help)
// from the extension's own arguments in a raw argument slice.
//
// Extension commands run with cobra's DisableFlagParsing enabled (see
// ExtensionRegisterCommand). Without it, pflag parses the command line first
// and, because the extension's own flags (e.g. --type, --fqdn) are unknown to
// pflag, ParseErrorsWhitelist.UnknownFlags causes pflag to silently drop each
// unknown flag together with its value; the BOF argument parser never saw them
// unless the operator injected cobra's "--" separator (#2309). Running with
// flag parsing disabled hands us the raw, unstripped slice, so we separate the
// Sliver-owned flags ourselves here.
//
// A literal "--" stops Sliver-flag scanning; every token after it is passed
// through verbatim, preserving the long-standing workaround syntax. Unknown
// tokens (the extension's named flags, their values and positional arguments)
// are never consumed or reordered, so both forms work:
//
// delegationbof --timeout 30 --type 6 --fqdn child.htb.local
// delegationbof -- --type 6 --fqdn child.htb.local
func splitExtensionArgs(raw []string) (save bool, timeout int64, help bool, extArgs []string) {
timeout = defaultTimeout
extArgs = make([]string, 0, len(raw))
passthrough := false
for i := 0; i < len(raw); i++ {
tok := raw[i]
// Once we have seen "--", copy everything through untouched so the
// existing `ext -- --foo bar` syntax keeps working identically.
if passthrough {
extArgs = append(extArgs, tok)
continue
}
switch {
case tok == "--":
passthrough = true
case tok == "--help" || tok == "-h":
help = true
case tok == "--save" || tok == "-s":
save = true
case strings.HasPrefix(tok, "--save="):
save = parseBoolValue(tok[len("--save="):], true)
case tok == "--timeout" || tok == "-t":
// Consume the following token as the value, mirroring pflag. If no
// token follows, leave the default in place rather than erroring.
if i+1 < len(raw) {
if n, err := strconv.ParseInt(raw[i+1], 10, 64); err == nil {
timeout = n
}
i++
}
case strings.HasPrefix(tok, "--timeout="):
if n, err := strconv.ParseInt(tok[len("--timeout="):], 10, 64); err == nil {
timeout = n
}
default:
// Not a Sliver-owned flag: hand it to the extension parser untouched.
extArgs = append(extArgs, tok)
}
}
return save, timeout, help, extArgs
}
// applyOwnedFlags republishes the manually parsed Sliver-owned flag values back
// onto the command's flag set. DisableFlagParsing prevents pflag from populating
// them, so helpers that read them via cmd.Flags() (notably ActiveTarget.Request,
// which derives the gRPC timeout from --timeout) would otherwise observe only the
// defaults.
func applyOwnedFlags(cmd *cobra.Command, save bool, timeout int64) {
if flag := cmd.Flags().Lookup("timeout"); flag != nil {
_ = flag.Value.Set(strconv.FormatInt(timeout, 10))
}
if flag := cmd.Flags().Lookup("save"); flag != nil {
_ = flag.Value.Set(strconv.FormatBool(save))
}
}
// parseBoolValue parses a --flag=value bool, defaulting to defVal when the value
// is empty (the bare-flag form). It accepts the same spellings pflag does.
func parseBoolValue(v string, defVal bool) bool {
switch strings.ToLower(v) {
case "", "true", "t", "1":
return true
case "false", "f", "0":
return false
}
return defVal
}
-149
View File
@@ -1,149 +0,0 @@
package extensions
/*
Sliver Implant Framework
Copyright (C) 2021 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 (
"reflect"
"testing"
)
// TestSplitExtensionArgs covers the argument-passthrough regression from #2309:
// the extension's own named flags must reach the BOF parser intact regardless of
// the cobra "--" separator, while Sliver-owned flags are still honoured.
func TestSplitExtensionArgs(t *testing.T) {
cases := []struct {
name string
in []string
save bool
timeout int64
help bool
extArgs []string
}{
{
name: "named extension flags without -- separator (#2309 regression)",
in: []string{"--type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
},
{
name: "named extension flags with -- separator (existing workaround)",
in: []string{"--", "--type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
},
{
name: "sliver-owned flags precede extension arguments",
in: []string{"--timeout", "30", "--save", "--type", "6"},
save: true,
timeout: 30,
extArgs: []string{"--type", "6"},
},
{
name: "quoted value containing spaces stays a single argument",
in: []string{"--name", "hello world", "--type", "6"},
timeout: defaultTimeout,
extArgs: []string{"--name", "hello world", "--type", "6"},
},
{
name: "positional extension arguments are preserved",
in: []string{"foo", "bar", "baz"},
timeout: defaultTimeout,
extArgs: []string{"foo", "bar", "baz"},
},
{
name: "single-dash and double-dash forms are preserved",
in: []string{"-type", "6", "--fqdn", "child.htb.local"},
timeout: defaultTimeout,
extArgs: []string{"-type", "6", "--fqdn", "child.htb.local"},
},
{
name: "equals form for timeout",
in: []string{"--timeout=120", "--type", "6"},
timeout: 120,
extArgs: []string{"--type", "6"},
},
{
name: "short -s save flag",
in: []string{"-s", "--type", "6"},
save: true,
timeout: defaultTimeout,
extArgs: []string{"--type", "6"},
},
{
name: "help flag is detected",
in: []string{"--help"},
help: true,
timeout: defaultTimeout,
extArgs: []string{},
},
{
name: "empty input",
in: nil,
timeout: defaultTimeout,
extArgs: []string{},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
save, timeout, help, extArgs := splitExtensionArgs(tc.in)
if save != tc.save {
t.Errorf("save: got %v, want %v", save, tc.save)
}
if timeout != tc.timeout {
t.Errorf("timeout: got %d, want %d", timeout, tc.timeout)
}
if help != tc.help {
t.Errorf("help: got %v, want %v", help, tc.help)
}
if !reflect.DeepEqual(extArgs, tc.extArgs) {
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
}
})
}
}
// TestParseBoolValue checks the --flag=value helper used for --save=value.
func TestParseBoolValue(t *testing.T) {
cases := []struct {
in string
want bool
}{
{"", true},
{"true", true},
{"TRUE", true},
{"1", true},
{"t", true},
{"false", false},
{"0", false},
{"no", false}, // unsupported spellings keep the default
}
for _, tc := range cases {
got := parseBoolValue(tc.in, true)
// "no" is not a pflag spelling, so it falls through to the default (true).
want := tc.want
if tc.in == "no" {
want = true
}
if got != want {
t.Errorf("parseBoolValue(%q): got %v, want %v", tc.in, got, want)
}
}
}
+8 -621
View File
@@ -1,28 +1,8 @@
package extensions
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"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/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
/*
@@ -133,104 +113,7 @@ const (
}`
)
type registerExtensionTestRPC struct {
rpcpb.SliverRPCClient
register func(context.Context, *sliverpb.RegisterExtensionReq, ...grpc.CallOption) (*sliverpb.RegisterExtension, error)
}
func (f *registerExtensionTestRPC) RegisterExtension(ctx context.Context, req *sliverpb.RegisterExtensionReq, opts ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return f.register(ctx, req, opts...)
}
func newExtensionTestConsole(t *testing.T, rpc rpcpb.SliverRPCClient, session *clientpb.Session, beacon *clientpb.Beacon) *console.SliverClient {
t.Helper()
con := console.NewConsole(false)
con.IsCLI = true
con.Rpc = rpc
con.ActiveTarget.Set(session, beacon)
return con
}
func TestRemoveExtensionByManifestNameOnlyDeletesManagedExtensions(t *testing.T) {
clientRoot := t.TempDir()
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
originalManifests := loadedManifests
originalExtensions := loadedExtensions
t.Cleanup(func() {
loadedManifests = originalManifests
loadedExtensions = originalExtensions
})
tests := []struct {
name string
extPath string
wantDeleted bool
}{
{
name: "installed extension",
extPath: filepath.Join(clientRoot, "extensions", "installed"),
wantDeleted: true,
},
{
name: "temporarily loaded extension",
extPath: t.TempDir(),
wantDeleted: false,
},
{
name: "extensions directory sibling",
extPath: filepath.Join(clientRoot, "extensions-backup"),
wantDeleted: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if err := os.MkdirAll(test.extPath, 0o700); err != nil {
t.Fatalf("create extension directory: %v", err)
}
markerPath := filepath.Join(test.extPath, "source-code.txt")
if err := os.WriteFile(markerPath, []byte("keep me"), 0o600); err != nil {
t.Fatalf("create marker file: %v", err)
}
manifestName := "test-extension"
commandName := "test-command"
command := &ExtCommand{CommandName: commandName}
loadedManifests = map[string]*ExtensionManifest{
manifestName: {RootPath: test.extPath, ExtCommand: []*ExtCommand{command}},
}
loadedExtensions = map[string]*ExtCommand{commandName: command}
found, err := RemoveExtensionByManifestName(manifestName, nil)
if err != nil {
t.Fatalf("remove extension: %v", err)
}
if !found {
t.Fatal("expected extension manifest to be found")
}
if _, ok := loadedManifests[manifestName]; ok {
t.Error("manifest was not unloaded")
}
if _, ok := loadedExtensions[commandName]; ok {
t.Error("command was not unloaded")
}
_, err = os.Stat(markerPath)
if test.wantDeleted && !os.IsNotExist(err) {
t.Errorf("managed extension directory was not deleted: %v", err)
}
if !test.wantDeleted && err != nil {
t.Errorf("external extension directory was modified: %v", err)
}
})
}
}
func TestParseExtensionManifest(t *testing.T) {
expectedPath := util.ResolvePath("foo/test1.dll")
extManifest, err := ParseExtensionManifest([]byte(sample1))
if err != nil {
t.Fatalf("Error parsing extension manifest: %s", err)
@@ -267,8 +150,8 @@ func TestParseExtensionManifest(t *testing.T) {
if extCmd.Files[0].Arch != "amd64" {
t.Errorf("Expected Arch 'amd64', got '%s'", extCmd.Files[0].Arch)
}
if extCmd.Files[0].Path != expectedPath {
t.Errorf("Expected path '%s', got '%s'", expectedPath, extCmd.Files[0].Path)
if extCmd.Files[0].Path != "/foo/test1.dll" {
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extCmd.Files[0].Path)
}
}
@@ -295,228 +178,14 @@ func TestParseExtensionManifest(t *testing.T) {
if extManifest2.Files[0].Arch != "amd64" {
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
}
if extManifest2.Files[0].Path != expectedPath {
t.Errorf("Expected path '%s', got '%s'", expectedPath, extManifest2.Files[0].Path)
if extManifest2.Files[0].Path != "/foo/test1.dll" {
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest2.Files[0].Path)
}
}
}
func TestConvertOldManifestPreservesInit(t *testing.T) {
const initExport = "InitializeExtension"
manifest, err := ParseExtensionManifest([]byte(`{
"name": "legacy-test",
"command_name": "legacy-test",
"help": "some help",
"init": "InitializeExtension",
"files": [
{
"os": "windows",
"arch": "amd64",
"path": "foo/test.dll"
}
]
}`))
if err != nil {
t.Fatalf("Error parsing legacy extension manifest: %s", err)
}
if len(manifest.ExtCommand) != 1 {
t.Fatalf("Expected 1 extension command, got %d", len(manifest.ExtCommand))
}
if manifest.ExtCommand[0].Init != initExport {
t.Errorf("Expected init export %q, got %q", initExport, manifest.ExtCommand[0].Init)
}
}
func TestRegisterExtensionPropagatesInit(t *testing.T) {
const initExport = "InitializeExtension"
tests := []struct {
name string
session *clientpb.Session
beacon *clientpb.Beacon
wantAsync bool
wantID string
}{
{
name: "session",
session: &clientpb.Session{ID: "session-id"},
wantID: "session-id",
},
{
name: "beacon",
beacon: &clientpb.Beacon{ID: "beacon-id"},
wantAsync: true,
wantID: "beacon-id",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotReq *sliverpb.RegisterExtensionReq
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, req *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
gotReq = req
return &sliverpb.RegisterExtension{Response: &commonpb.Response{}}, nil
},
}
con := newExtensionTestConsole(t, rpc, test.session, test.beacon)
err := registerExtension("linux", &ExtCommand{CommandName: "test", Init: initExport}, []byte("extension"), nil, con)
if err != nil {
t.Fatalf("RegisterExtension returned an error: %s", err)
}
if gotReq == nil {
t.Fatal("RegisterExtension RPC was not called")
}
if gotReq.Init != initExport {
t.Errorf("Expected init export %q, got %q", initExport, gotReq.Init)
}
if gotReq.Request.GetAsync() != test.wantAsync {
t.Errorf("Expected async=%t, got %t", test.wantAsync, gotReq.Request.GetAsync())
}
if test.wantAsync && gotReq.Request.GetBeaconID() != test.wantID {
t.Errorf("Expected beacon ID %q, got %q", test.wantID, gotReq.Request.GetBeaconID())
}
if !test.wantAsync && gotReq.Request.GetSessionID() != test.wantID {
t.Errorf("Expected session ID %q, got %q", test.wantID, gotReq.Request.GetSessionID())
}
})
}
}
func TestRegisterExtensionWaitsForBeaconRegistration(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
close(started)
<-release
return &sliverpb.RegisterExtension{Response: &commonpb.Response{Async: true}}, nil
},
}
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
done := make(chan error, 1)
go func() {
done <- registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
}()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("Timed out waiting for RegisterExtension RPC")
}
select {
case err := <-done:
t.Fatalf("RegisterExtension returned before the beacon registration was enqueued: %v", err)
case <-time.After(100 * time.Millisecond):
}
close(release)
select {
case err := <-done:
if err != nil {
t.Fatalf("RegisterExtension returned an error: %s", err)
}
case <-time.After(time.Second):
t.Fatal("Timed out waiting for RegisterExtension to return")
}
}
func TestRegisterExtensionReturnsBeaconRPCError(t *testing.T) {
wantErr := errors.New("register failed")
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return nil, wantErr
},
}
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
if !errors.Is(err, wantErr) {
t.Fatalf("Expected error %q, got %v", wantErr, err)
}
}
func TestRegisterExtensionRejectsNilResponse(t *testing.T) {
rpc := &registerExtensionTestRPC{
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
return nil, nil
},
}
con := newExtensionTestConsole(t, rpc, &clientpb.Session{ID: "session-id"}, nil)
if err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con); err == nil {
t.Fatal("RegisterExtension accepted a nil response")
}
}
func TestExtensionCommandVisibilityUsesExactTargetPairs(t *testing.T) {
extension := &ExtCommand{Files: []*extensionFile{
{OS: "windows", Arch: "amd64"},
{OS: "linux", Arch: "amd64"},
{OS: "darwin", Arch: "arm64"},
}}
tests := []struct {
name string
session *clientpb.Session
beacon *clientpb.Beacon
wantVisible bool
}{
{
name: "cross-platform Linux session",
session: &clientpb.Session{OS: "linux", Arch: "amd64"},
wantVisible: true,
},
{
name: "cross-platform macOS beacon",
beacon: &clientpb.Beacon{OS: "darwin", Arch: "arm64"},
wantVisible: true,
},
{
name: "cross-platform Windows session",
session: &clientpb.Session{OS: "windows", Arch: "amd64"},
wantVisible: true,
},
{
name: "Linux architecture mismatch",
session: &clientpb.Session{OS: "linux", Arch: "arm64"},
wantVisible: false,
},
{
name: "macOS architecture mismatch",
beacon: &clientpb.Beacon{OS: "darwin", Arch: "amd64"},
wantVisible: false,
},
{
name: "Windows architecture mismatch",
session: &clientpb.Session{OS: "windows", Arch: "386"},
wantVisible: false,
},
{
name: "unsupported native extension target",
beacon: &clientpb.Beacon{OS: "linux", Arch: "riscv64"},
wantVisible: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
con := newExtensionTestConsole(t, nil, test.session, test.beacon)
command := &cobra.Command{Annotations: makeCommandPlatformFilters(extension)}
activeFilters := con.App.Menu(consts.ImplantMenu).ActiveFiltersFor(command)
visible := len(activeFilters) == 0
if visible != test.wantVisible {
t.Fatalf("command visibility = %t, want %t (active filters: %v)", visible, test.wantVisible, activeFilters)
}
})
}
}
func TestParseMultipleCmdManifest(t *testing.T) {
expectedPath := util.ResolvePath("ex.dll")
mextManifest, err := ParseExtensionManifest([]byte(multicmd))
if err != nil {
t.Errorf("error parsing manifest: %s", err)
@@ -537,11 +206,11 @@ func TestParseMultipleCmdManifest(t *testing.T) {
if mextManifest.ExtCommand[1].Entrypoint != "Test2" {
t.Errorf("expected entrypoint Test2, got %s", mextManifest.ExtCommand[1].Entrypoint)
}
if mextManifest.ExtCommand[0].Files[0].Path != expectedPath { // path normalization is platform-specific
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[0].Files[0].Path)
if mextManifest.ExtCommand[0].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
}
if mextManifest.ExtCommand[1].Files[0].Path != expectedPath { // path normalization is platform-specific
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[1].Files[0].Path)
if mextManifest.ExtCommand[1].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
}
//maybe some more? args?
}
@@ -648,285 +317,3 @@ func TestParseExtensionManifestErrors(t *testing.T) {
}
}
}
func TestParseExtensionManifestBOFExecutor(t *testing.T) {
tests := []struct {
name string
data string
want string
}{
{
name: "multi-command manifest",
data: `{
"name":"modern-bof",
"commands":[{
"command_name":"modern-bof",
"help":"test",
"bof_executor":"reflektor",
"files":[{"os":"windows","arch":"amd64","path":"modern.o"}]
}]
}`,
want: BOFExecutorReflektor,
},
{
name: "legacy manifest",
data: `{
"name":"legacy-bof",
"command_name":"legacy-bof",
"help":"test",
"bof_executor":"coff-loader",
"depends_on":"custom-loader",
"files":[{"os":"windows","arch":"amd64","path":"legacy.o"}]
}`,
want: BOFExecutorCOFFLoader,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest, err := ParseExtensionManifest([]byte(test.data))
if err != nil {
t.Fatalf("ParseExtensionManifest returned an error: %s", err)
}
if got := manifest.ExtCommand[0].BOFExecutor; got != test.want {
t.Fatalf("BOFExecutor = %q, want %q", got, test.want)
}
})
}
}
func TestParseExtensionManifestRejectsInvalidBOFExecutor(t *testing.T) {
tests := []struct {
name string
executor string
dependsOn string
wantErrPart string
}{
{
name: "executor must be exact lowercase",
executor: "Reflektor",
wantErrPart: "invalid `bof_executor`",
},
{
name: "unknown executor",
executor: "other-loader",
wantErrPart: "invalid `bof_executor`",
},
{
name: "coff loader requires dependency",
executor: BOFExecutorCOFFLoader,
wantErrPart: "no `depends_on` fallback",
},
{
name: "coff loader rejects whitespace dependency",
executor: BOFExecutorCOFFLoader,
dependsOn: " \t",
wantErrPart: "no `depends_on` fallback",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest := &ExtensionManifest{
Name: "test",
ExtCommand: []*ExtCommand{{
CommandName: "test",
Help: "test",
BOFExecutor: test.executor,
DependsOn: test.dependsOn,
Files: []*extensionFile{{
OS: "windows", Arch: "amd64", Path: "test.o",
}},
}},
}
err := validManifest(manifest)
if err == nil || !strings.Contains(err.Error(), test.wantErrPart) {
t.Fatalf("validManifest error = %v, want error containing %q", err, test.wantErrPart)
}
})
}
}
func TestPlanExtensionExecution(t *testing.T) {
tests := []struct {
name string
ext *ExtCommand
path string
capabilities uint64
want extensionExecutionMode
wantErr bool
}{
{
name: "native artifact remains native",
ext: &ExtCommand{CommandName: "native"},
path: "native.dll",
want: extensionExecutionNative,
},
{
name: "legacy manifest keeps dependency route",
ext: &ExtCommand{CommandName: "legacy", DependsOn: "coff-loader"},
path: "legacy.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionCOFFLoader,
},
{
name: "explicit coff loader is authoritative",
ext: &ExtCommand{CommandName: "coff", BOFExecutor: BOFExecutorCOFFLoader, DependsOn: "custom-loader"},
path: "coff.bin",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionCOFFLoader,
},
{
name: "explicit Reflektor uses capable target",
ext: &ExtCommand{CommandName: "builtin", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
path: "builtin.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "explicit Reflektor falls back to dependency",
ext: &ExtCommand{CommandName: "fallback", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
path: "fallback.o",
want: extensionExecutionCOFFLoader,
},
{
name: "dependency-free BOF defaults to Reflektor",
ext: &ExtCommand{CommandName: "default"},
path: "default.o",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "uppercase object extension is a BOF",
ext: &ExtCommand{CommandName: "uppercase"},
path: "uppercase.O",
capabilities: sliverpb.CapabilityBOFV1,
want: extensionExecutionReflektor,
},
{
name: "unsupported dependency-free BOF fails closed",
ext: &ExtCommand{CommandName: "unsupported"},
path: "unsupported.o",
wantErr: true,
},
{
name: "coff loader without dependency fails closed",
ext: &ExtCommand{CommandName: "missing", BOFExecutor: BOFExecutorCOFFLoader},
path: "missing.o",
wantErr: true,
},
{
name: "invalid executor fails closed",
ext: &ExtCommand{CommandName: "invalid", BOFExecutor: "other"},
path: "invalid.o",
capabilities: sliverpb.CapabilityBOFV1,
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := planExtensionExecution(test.ext, test.path, test.capabilities)
if test.wantErr {
if err == nil {
t.Fatalf("planExtensionExecution returned mode %d, want error", got)
}
return
}
if err != nil {
t.Fatalf("planExtensionExecution returned an error: %s", err)
}
if got != test.want {
t.Fatalf("planExtensionExecution mode = %d, want %d", got, test.want)
}
})
}
}
func TestBuildCallExtensionRequestPreservesLegacyWireFields(t *testing.T) {
root := t.TempDir()
objectData := []byte("object-data")
loaderData := []byte("loader-data")
objectPath := filepath.Join(root, "object.o")
loaderPath := filepath.Join(root, "loader.dll")
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
t.Fatalf("write object fixture: %s", err)
}
if err := os.WriteFile(loaderPath, loaderData, 0o600); err != nil {
t.Fatalf("write loader fixture: %s", err)
}
const loaderName = "custom-loader"
previousLoader, hadPreviousLoader := loadedExtensions[loaderName]
defer func() {
if hadPreviousLoader {
loadedExtensions[loaderName] = previousLoader
} else {
delete(loadedExtensions, loaderName)
}
}()
loaderManifest := &ExtensionManifest{Name: loaderName, RootPath: root}
loadedExtensions[loaderName] = &ExtCommand{
CommandName: loaderName,
Entrypoint: "RunCOFF",
Files: []*extensionFile{{
OS: "windows", Arch: "amd64", Path: filepath.Base(loaderPath),
}},
Manifest: loaderManifest,
}
legacyArgs := []byte{0x01, 0x02, 0x03, 0x04}
ext := &ExtCommand{CommandName: "test-bof", DependsOn: loaderName}
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionCOFFLoader, ext, objectPath, "RunCOFF", legacyArgs)
if err != nil {
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
}
loaderHash := sha256.Sum256(loaderData)
want := &sliverpb.CallExtensionReq{
Name: hex.EncodeToString(loaderHash[:]),
Export: "RunCOFF",
Args: legacyArgs,
}
gotWire, err := proto.Marshal(got)
if err != nil {
t.Fatalf("marshal actual request: %s", err)
}
wantWire, err := proto.Marshal(want)
if err != nil {
t.Fatalf("marshal expected request: %s", err)
}
if !bytes.Equal(gotWire, wantWire) {
t.Fatalf("legacy request wire bytes changed:\n got: %x\nwant: %x", gotWire, wantWire)
}
}
func TestBuildCallExtensionRequestForReflektor(t *testing.T) {
root := t.TempDir()
objectData := []byte("object-data")
objectPath := filepath.Join(root, "object.o")
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
t.Fatalf("write object fixture: %s", err)
}
typedArgs := []byte{0x05, 0x06}
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionReflektor, &ExtCommand{CommandName: "test-bof"}, objectPath, "go", typedArgs)
if err != nil {
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
}
objectHash := sha256.Sum256(objectData)
if got.Name != hex.EncodeToString(objectHash[:]) {
t.Errorf("Name = %q, want object hash %q", got.Name, hex.EncodeToString(objectHash[:]))
}
if got.Export != "go" {
t.Errorf("Export = %q, want %q", got.Export, "go")
}
if !bytes.Equal(got.Args, typedArgs) {
t.Errorf("Args = %x, want %x", got.Args, typedArgs)
}
if !bytes.Equal(got.BOFData, objectData) {
t.Errorf("BOFData = %q, want %q", got.BOFData, objectData)
}
if !got.IsBOF {
t.Error("IsBOF = false, want true")
}
}
+23 -29
View File
@@ -40,21 +40,7 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
con.PrintErrorf("Extension path '%s' does not exist", extLocalPath)
return
}
_ = InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
}
func readExtensionManifest(extLocalPath string, isGz bool) ([]byte, error) {
if isGz {
return util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
}
return os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
}
func extensionPackageID(manifest *ExtensionManifest) string {
if manifest.PackageName != "" {
return manifest.PackageName
}
return manifest.Name
InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
}
// InstallFromDir installs a Sliver extension from either a local directory or gzipped archive.
@@ -68,28 +54,37 @@ func extensionPackageID(manifest *ExtensionManifest) string {
// - con: Sliver console client for displaying status and error messages
// - isGz: Whether the source is a gzipped archive (true) or directory (false)
//
// The function returns errors after printing them to the console if:
// The function will return early with error messages printed to console if:
// - The manifest cannot be read or parsed
// - Required directories cannot be created
// - File copy operations fail
//
// Declining an overwrite prompt cancels the installation without an error.
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) error {
manifestData, err := readExtensionManifest(extLocalPath, isGz)
// - User declines overwrite when prompted
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) {
var manifestData []byte
var err error
if isGz {
manifestData, err = util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
} else {
manifestData, err = os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
}
if err != nil {
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
return fmt.Errorf("read %s: %w", ManifestFileName, err)
return
}
manifestF, err := ParseExtensionManifest(manifestData)
if err != nil {
con.PrintErrorf("Error parsing %s: %s", ManifestFileName, err)
return fmt.Errorf("parse %s: %w", ManifestFileName, err)
return
}
// Use package name if available, otherwise use extension name
// (Note, for v1 manifests this will actually be command_name)
packageID := extensionPackageID(manifestF)
packageID := manifestF.Name
if manifestF.PackageName != "" {
packageID = manifestF.PackageName
}
//create repo path
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(packageID))
@@ -99,7 +94,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
confirm := false
_ = forms.Confirm("Overwrite current install?", &confirm)
if !confirm {
return nil
return
}
}
forceRemoveAll(minstallPath)
@@ -108,13 +103,13 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
err = os.MkdirAll(minstallPath, 0o700)
if err != nil {
con.PrintErrorf("Error creating extension directory: %s\n", err)
return fmt.Errorf("create extension directory: %w", err)
return
}
err = os.WriteFile(filepath.Join(minstallPath, ManifestFileName), manifestData, 0o600)
if err != nil {
con.PrintErrorf("Failed to write %s: %s\n", ManifestFileName, err)
forceRemoveAll(minstallPath)
return fmt.Errorf("write %s: %w", ManifestFileName, err)
return
}
for _, manifest := range manifestF.ExtCommand {
@@ -130,7 +125,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
if err != nil {
con.PrintErrorf("\nError creating extension directory: %s\n", err)
forceRemoveAll(installPath)
return fmt.Errorf("create extension artifact directory: %w", err)
return
}
err = util.CopyFile(src, dst)
if err != nil {
@@ -140,12 +135,11 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
if err != nil {
con.PrintErrorf("Error installing command: %s\n", err)
forceRemoveAll(installPath)
return fmt.Errorf("install extension artifact: %w", err)
return
}
}
}
}
return nil
}
func installArtifact(extGzFilePath string, installPath string, artifactPath string) error {
-135
View File
@@ -1,135 +0,0 @@
package extensions
import (
"archive/tar"
"errors"
"io/fs"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/klauspost/compress/gzip"
"github.com/spf13/cobra"
)
type installArchiveMember struct {
name string
data []byte
}
const installTestManifest = `{
"name": "Test Extension",
"package_name": "test-extension",
"version": "v1.0.0",
"commands": [{
"command_name": "test-command",
"help": "test command",
"files": [{"os": "windows", "arch": "amd64", "path": "foo/test.dll"}]
}]
}`
func writeInstallArchive(t *testing.T, prefix string, members ...installArchiveMember) string {
t.Helper()
archivePath := filepath.Join(t.TempDir(), "extension.tar.gz")
archive, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
gzipWriter := gzip.NewWriter(archive)
tarWriter := tar.NewWriter(gzipWriter)
for _, member := range members {
header := &tar.Header{
Name: prefix + member.name,
Mode: 0o600,
Size: int64(len(member.data)),
Typeflag: tar.TypeReg,
}
if err := tarWriter.WriteHeader(header); err != nil {
t.Fatal(err)
}
if _, err := tarWriter.Write(member.data); err != nil {
t.Fatal(err)
}
}
if err := tarWriter.Close(); err != nil {
t.Fatal(err)
}
if err := gzipWriter.Close(); err != nil {
t.Fatal(err)
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
return archivePath
}
func testInstallConsole(t *testing.T) *console.SliverClient {
t.Helper()
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(
con, nil, nil, nil, emptyCommands, emptyCommands, false, "",
); err != nil {
t.Fatal(err)
}
return con
}
func TestInstallFromDirAcceptsPrefixedAndUnprefixedArchives(t *testing.T) {
for _, prefix := range []string{"./", ""} {
name := "unprefixed"
if prefix != "" {
name = "prefixed"
}
t.Run(name, func(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, prefix,
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
)
if err := InstallFromDir(archivePath, false, testInstallConsole(t), true); err != nil {
t.Fatal(err)
}
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
if _, err := os.Stat(filepath.Join(installRoot, ManifestFileName)); err != nil {
t.Fatalf("installed manifest: %v", err)
}
artifact, err := os.ReadFile(filepath.Join(installRoot, "foo", "test.dll"))
if err != nil {
t.Fatalf("installed artifact: %v", err)
}
if string(artifact) != "extension data" {
t.Fatalf("artifact data = %q", artifact)
}
})
}
}
func TestInstallFromDirReturnsMissingManifest(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, "./",
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
)
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("error = %v, want fs.ErrNotExist", err)
}
}
func TestInstallFromDirReturnsMissingArtifactAndCleansUp(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
archivePath := writeInstallArchive(t, "./",
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
)
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("error = %v, want fs.ErrNotExist", err)
}
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
if _, err := os.Stat(installRoot); !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("install directory was not cleaned up: %v", err)
}
}
+104 -183
View File
@@ -54,20 +54,6 @@ const (
// ManifestFileName - Extension manifest file name.
ManifestFileName = "extension.json"
// BOFExecutorReflektor selects the built-in Reflektor BOF executor.
BOFExecutorReflektor = "reflektor"
// BOFExecutorCOFFLoader selects the manifest's depends_on COFF loader.
BOFExecutorCOFFLoader = "coff-loader"
)
type extensionExecutionMode uint8
const (
extensionExecutionNative extensionExecutionMode = iota
extensionExecutionCOFFLoader
extensionExecutionReflektor
)
var loadedExtensions = map[string]*ExtCommand{}
@@ -86,7 +72,6 @@ type ExtensionManifest_ struct {
Arguments []*extensionArgument `json:"arguments"`
Entrypoint string `json:"entrypoint"`
DependsOn string `json:"depends_on"`
BOFExecutor string `json:"bof_executor"`
Init string `json:"init"`
RootPath string `json:"-"`
@@ -115,7 +100,6 @@ type ExtCommand struct {
Arguments []*extensionArgument `json:"arguments"`
Entrypoint string `json:"entrypoint"`
DependsOn string `json:"depends_on"`
BOFExecutor string `json:"bof_executor"`
Init string `json:"init"`
Schema *packages.OutputSchema `json:"schema"`
@@ -234,11 +218,9 @@ func convertOldManifest(old *ExtensionManifest_) *ExtensionManifest {
{
CommandName: old.CommandName,
DependsOn: old.DependsOn,
BOFExecutor: old.BOFExecutor,
Help: old.Help,
LongHelp: old.LongHelp,
Entrypoint: old.Entrypoint,
Init: old.Init,
Files: old.Files,
Arguments: old.Arguments,
Schema: nil,
@@ -288,15 +270,6 @@ func validManifest(manifest *ExtensionManifest) error {
if extManifest.CommandName == "" {
return errors.New("missing `command_name` field in extension manifest")
}
switch extManifest.BOFExecutor {
case "", BOFExecutorReflektor:
case BOFExecutorCOFFLoader:
if strings.TrimSpace(extManifest.DependsOn) == "" {
return fmt.Errorf("command %q uses bof_executor %q but has no `depends_on` fallback", extManifest.CommandName, BOFExecutorCOFFLoader)
}
default:
return fmt.Errorf("command %q has invalid `bof_executor` %q (must be %q or %q)", extManifest.CommandName, extManifest.BOFExecutor, BOFExecutorReflektor, BOFExecutorCOFFLoader)
}
if len(extManifest.Files) == 0 {
return errors.New("missing `files` field in extension manifest")
}
@@ -397,19 +370,16 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
Run: func(cmd *cobra.Command, args []string) {
runExtensionCmd(cmd, con, args)
},
GroupID: consts.ExtensionHelpGroup,
// DisableFlagParsing keeps the extension's own flags (which are unknown to
// pflag) from being silently stripped together with their values. We split
// Sliver-owned flags out of the raw slice ourselves in runExtensionCmd (#2309).
DisableFlagParsing: true,
Annotations: makeCommandPlatformFilters(extCmd),
GroupID: consts.ExtensionHelpGroup,
Annotations: makeCommandPlatformFilters(extCmd),
}
// Flags
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
f.BoolP("save", "s", false, "Save output to disk")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
extensionCmd.Flags().AddFlagSet(f)
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
// Completions
comps := carapace.Gen(extensionCmd)
@@ -498,30 +468,44 @@ func registerExtension(goos string, ext *ExtCommand, binData []byte, cmd *cobra.
//set extension name to a hash of the data to avoid loading more than one instance
bd := sha256.Sum256(binData)
name := hex.EncodeToString(bd[:])
sess, _ := con.ActiveTarget.GetInteractive()
sess, beac := con.ActiveTarget.GetInteractive()
ctrl := make(chan bool)
//first time run of an extension will require some waiting depending on the size
if sess != nil {
msg := fmt.Sprintf("Sending %s to implant ...", ext.CommandName)
con.SpinUntil(msg, ctrl)
}
//don't block if we are in beacon mode
if beac != nil && sess == nil {
go func() {
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
Name: name,
Data: binData,
OS: goos,
Init: ext.Init,
Request: con.ActiveTarget.Request(cmd),
})
if err != nil {
con.PrintErrorf("Error registering extension: %s\n", err)
}
if registerResp.Response != nil && registerResp.Response.Err != "" {
con.PrintErrorf("Error registering extension: %s\n", errors.New(registerResp.Response.Err))
}
}()
return nil
}
//session mode (hopefully)
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
Name: name,
Data: binData,
OS: goos,
Init: ext.Init,
Request: con.ActiveTarget.Request(cmd),
})
if sess != nil {
ctrl <- true
<-ctrl
}
ctrl <- true
<-ctrl
if err != nil {
return err
}
if registerResp == nil {
return errors.New("received empty register extension response")
}
if registerResp.Response != nil && registerResp.Response.Err != "" {
return errors.New(registerResp.Response.Err)
}
@@ -544,102 +528,7 @@ func loadDep(goos string, goarch string, depName string, cmd *cobra.Command, con
return fmt.Errorf("missing dependency %s", depName)
}
func planExtensionExecution(ext *ExtCommand, binPath string, capabilities uint64) (extensionExecutionMode, error) {
isBOF := ext.BOFExecutor != "" || strings.EqualFold(filepath.Ext(binPath), ".o")
if !isBOF {
return extensionExecutionNative, nil
}
dependsOn := strings.TrimSpace(ext.DependsOn)
switch ext.BOFExecutor {
case BOFExecutorCOFFLoader:
if dependsOn == "" {
return extensionExecutionNative, fmt.Errorf("BOF command %q selects %q but has no depends_on loader", ext.CommandName, BOFExecutorCOFFLoader)
}
return extensionExecutionCOFFLoader, nil
case "":
// Existing manifests infer the loader from depends_on. Keep that route
// even when the target supports the built-in executor.
if dependsOn != "" {
return extensionExecutionCOFFLoader, nil
}
case BOFExecutorReflektor:
default:
return extensionExecutionNative, fmt.Errorf("BOF command %q has invalid bof_executor %q", ext.CommandName, ext.BOFExecutor)
}
if capabilities&sliverpb.CapabilityBOFV1 != 0 {
return extensionExecutionReflektor, nil
}
if dependsOn != "" {
return extensionExecutionCOFFLoader, nil
}
return extensionExecutionNative, fmt.Errorf("target does not support built-in Reflektor BOF execution and command %q has no depends_on fallback", ext.CommandName)
}
func activeTargetCapabilities(session *clientpb.Session, beacon *clientpb.Beacon) uint64 {
if session != nil {
return session.Capabilities
}
if beacon != nil {
return beacon.Capabilities
}
return 0
}
func buildCallExtensionRequest(goos string, goarch string, mode extensionExecutionMode, ext *ExtCommand, binPath string, entryPoint string, extensionArgs []byte) (*sliverpb.CallExtensionReq, error) {
extData, err := os.ReadFile(binPath)
if err != nil {
return nil, err
}
bd := sha256.Sum256(extData)
name := hex.EncodeToString(bd[:])
if mode == extensionExecutionCOFFLoader {
loader, found := loadedExtensions[ext.DependsOn]
if !found {
return nil, fmt.Errorf("attempted to load non-existing extension: %s", ext.DependsOn)
}
loaderPath, err := loader.getFileForTarget(goos, goarch)
if err != nil {
return nil, fmt.Errorf("could not get file for extension %s: %w", ext.DependsOn, err)
}
loaderData, err := os.ReadFile(loaderPath)
if err != nil {
return nil, fmt.Errorf("dep read file error: %w", err)
}
if len(loaderData) == 0 {
return nil, fmt.Errorf("read empty file: %s", loaderPath)
}
bd = sha256.Sum256(loaderData)
name = hex.EncodeToString(bd[:])
}
req := &sliverpb.CallExtensionReq{
Name: name,
Export: entryPoint,
Args: extensionArgs,
}
if mode == extensionExecutionReflektor {
req.BOFData = extData
req.IsBOF = true
}
return req, nil
}
func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// The command runs with DisableFlagParsing, so args is the raw, unstripped
// slice. Separate Sliver-owned flags from the extension's own arguments and
// re-publish them on the flag set (so Request()'s --timeout still works),
// then feed only the extension arguments to the BOF/DLL parsers (#2309).
save, timeout, helpRequested, extArgs := splitExtensionArgs(args)
if helpRequested {
_ = cmd.Help()
return
}
applyOwnedFlags(cmd, save, timeout)
args = extArgs
var (
err error
extensionArgs []byte
@@ -666,32 +555,27 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
return
}
checkCache := session != nil
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
con.PrintErrorf("Could not load extension: %s\n", err)
return
}
binPath, err := ext.getFileForTarget(goos, goarch)
if err != nil {
con.PrintErrorf("Failed to read extension file: %s\n", err)
return
}
executionMode, err := planExtensionExecution(ext, binPath, activeTargetCapabilities(session, beacon))
if err != nil {
con.PrintErrorf("Could not execute extension: %s\n", err)
return
}
if executionMode != extensionExecutionReflektor {
checkCache := session != nil
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
con.PrintErrorf("Could not load extension: %s\n", err)
return
}
}
isBOF := filepath.Ext(binPath) == ".o"
// BOFs (Beacon Object Files) are a specific kind of extensions
// that either use the built-in Reflektor executor or another extension.
// that require another extension (a COFF loader) to be present.
// BOFs also have strongly typed arguments that need to be parsed in the proper way.
switch executionMode {
case extensionExecutionCOFFLoader:
// Legacy COFF loaders expect the entrypoint, object, and typed argument
// buffer packed into the existing outer argument envelope.
// This block will pack both the BOF data and its arguments into a single buffer that
// the loader will extract and load.
if isBOF {
// Beacon Object File -- requires a COFF loader
extensionArgs, err = getBOFArgs(cmd, args, binPath, ext)
if err != nil {
con.PrintErrorf("BOF args error: %s\n", err)
@@ -699,15 +583,7 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
}
extName = ext.DependsOn
entryPoint = loadedExtensions[extName].Entrypoint // should exist at this point
case extensionExecutionReflektor:
extensionArgs, err = ParseFlagArgumentsToBuffer(cmd, args, binPath, ext)
if err != nil {
con.PrintErrorf("BOF args error: %s\n", err)
return
}
extName = ext.CommandName
entryPoint = ext.Entrypoint
default:
} else {
// Regular DLL - Just join the arguments with spaces
if len(args) > 0 {
extensionArgs = []byte(strings.Join(args, " "))
@@ -721,15 +597,45 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
ctrl := make(chan bool)
msg := fmt.Sprintf("Executing %s ...", cmd.Name())
con.SpinUntil(msg, ctrl)
callExtReq, err := buildCallExtensionRequest(goos, goarch, executionMode, ext, binPath, entryPoint, extensionArgs)
extdata, err := os.ReadFile(binPath)
if err != nil {
ctrl <- true
<-ctrl
con.PrintErrorf("Could not prepare extension call: %s\n", err)
return
con.PrintErrorf("ext read file error: %s\n", err)
}
callExtReq.Request = con.ActiveTarget.Request(cmd)
callExtResp, err := con.Rpc.CallExtension(context.Background(), callExtReq)
bd := sha256.Sum256(extdata)
name := hex.EncodeToString(bd[:])
if isBOF {
//if we are using a bof, we are actually calling the coffloader extension - so get the file from dep ref and use that shasum
if extension, found := loadedExtensions[ext.DependsOn]; found {
dep, err := extension.getFileForTarget(goos, goarch)
if err != nil {
con.PrintErrorf("could not get file for extension %s", ext.DependsOn)
return
}
depdata, err := os.ReadFile(dep)
if err != nil {
con.PrintErrorf("dep read file error: %s\n", err)
return
}
if len(depdata) == 0 {
con.PrintErrorf("read empty file: %s\n", dep)
return
}
bd = sha256.Sum256(depdata)
name = hex.EncodeToString(bd[:])
} else {
// handle error
con.PrintErrorf("attempted to load non-existing extension: %s", ext.DependsOn)
return
}
}
callExtResp, err := con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
Name: name,
Export: entryPoint,
Args: extensionArgs,
Request: con.ActiveTarget.Request(cmd),
})
ctrl <- true
<-ctrl
if err != nil {
@@ -944,22 +850,37 @@ func makeExtensionFlagNameCompletion(extCmd *ExtCommand) carapace.Action {
}
func makeCommandPlatformFilters(extCmd *ExtCommand) map[string]string {
supportedTargets := make(map[string]struct{}, len(extCmd.Files))
for _, file := range extCmd.Files {
supportedTargets[file.OS+"/"+file.Arch] = struct{}{}
}
filtersOS := make(map[string]bool)
filtersArch := make(map[string]bool)
// Console command filters are hide-on-match, so annotate the command with
// every exact target pair it does not support. A separate fallback hides all
// native extensions on target pairs outside the supported loader matrix.
filters := []string{consts.NativeExtensionUnsupportedTargetFilter}
for _, target := range consts.NativeExtensionTargets() {
if _, supported := supportedTargets[target.GOOS+"/"+target.GOARCH]; !supported {
filters = append(filters, target.Filter)
var all []string
// Only add filters for architectures when there OS matters.
for _, file := range extCmd.Files {
filtersOS[file.OS] = true
if filtersOS[file.OS] {
filtersArch[file.Arch] = true
}
}
for os, enabled := range filtersOS {
if enabled {
all = append(all, os)
}
}
for arch, enabled := range filtersArch {
if enabled {
all = append(all, arch)
}
}
if len(all) == 0 {
return map[string]string{}
}
return map[string]string{
appConsole.CommandFilterKey: strings.Join(filters, ","),
appConsole.CommandFilterKey: strings.Join(all, ","),
}
}
+15 -12
View File
@@ -23,6 +23,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
@@ -99,11 +100,21 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
return true, nil
}
// Only installed extensions are owned by Sliver and may be deleted.
// Temporarily loaded extensions can live in arbitrary source directories.
if isWithinExtensionsDir(extPath) {
forceRemoveAll(extPath)
// If path is outside extensions directory, prompt for confirmation
if !strings.HasPrefix(extPath, assets.GetExtensionsDir()) {
confirm := false
_ = forms.Confirm(fmt.Sprintf("Remove '%s' extension directory from filesystem?", manifestName), &confirm)
if !confirm {
// Skip the forceRemoveAll but continue with the rest
delete(loadedManifests, manifestName)
for _, cmd := range man.ExtCommand {
delete(loadedExtensions, cmd.CommandName)
}
return true, nil
}
}
forceRemoveAll(extPath)
delete(loadedManifests, manifestName)
for _, cmd := range man.ExtCommand {
delete(loadedExtensions, cmd.CommandName)
@@ -113,14 +124,6 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
return false, nil
}
func isWithinExtensionsDir(extPath string) bool {
relPath, err := filepath.Rel(assets.GetExtensionsDir(), extPath)
if err != nil {
return false
}
return relPath != "." && filepath.IsLocal(relPath)
}
func forceRemoveAll(rootPath string) {
util.ChmodR(rootPath, 0o600, 0o700)
os.RemoveAll(rootPath)
+3 -10
View File
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
// Session flags and completions.
@@ -159,7 +159,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("save", "s", "", "directory/file to the binary to")
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
f.StringP("name", "n", "", "Implant name")
f.BoolP("external-builder", "E", false, "use an external builder")
})
flags.BindFlagCompletions(profilesGenerateCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
@@ -340,13 +340,6 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.Uint32P("key-exchange", "X", DefaultWGKeyExPort, "wg key-exchange port")
f.Uint32P("tcp-comms", "T", DefaultWGNPort, "wg c2 comms port")
f.Bool("include-mtls", false, "force include mtls transport")
f.Bool("include-http", false, "force include http transport")
f.Bool("include-wg", false, "force include wireguard transport")
f.Bool("include-dns", false, "force include dns transport")
f.Bool("include-named-pipe", false, "force include named-pipe transport")
f.Bool("include-tcp-pivot", false, "force include tcp-pivot transport")
f.Bool("all-protocols", false, "force include all transport protocols")
f.BoolP("run-at-load", "R", false, "run the implant entrypoint from DllMain/Constructor (shared library only)")
f.BoolP("netgo", "q", false, "force the use of netgo")
@@ -367,7 +360,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
bindSpoofMetadataFlag(f)
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'archive' (for Go c-archives), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
// Shellcode generation options:
// - Windows: Donut
-24
View File
@@ -24,30 +24,6 @@ func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
}
func TestProfilesGenerateExternalBuilderFlagRegistered(t *testing.T) {
cmds := Commands(&console.SliverClient{})
profilesCmd := commandByUse(cmds, consts.ProfilesStr)
if profilesCmd == nil {
t.Fatalf("missing %q command", consts.ProfilesStr)
}
profilesGenerateCmd := commandByUse(profilesCmd.Commands(), consts.GenerateStr)
if profilesGenerateCmd == nil {
t.Fatalf("missing %q subcommand under %q", consts.GenerateStr, consts.ProfilesStr)
}
flag := profilesGenerateCmd.Flags().Lookup("external-builder")
if flag == nil {
t.Fatalf("%q missing %q flag", profilesGenerateCmd.Use, "external-builder")
}
if flag.Shorthand != "E" {
t.Fatalf("%q %q flag shorthand mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.Shorthand, "E")
}
if flag.DefValue != "false" {
t.Fatalf("%q %q flag default mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.DefValue, "false")
}
}
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
t.Helper()
-30
View File
@@ -87,7 +87,6 @@ var (
"linux/arm64": true,
"windows/386": true,
"windows/amd64": true,
"windows/arm64": true,
}
ErrNoExternalBuilder = errors.New("no external builders are available")
@@ -255,8 +254,6 @@ func nameOfOutputFormat(value clientpb.OutputFormat) string {
return "Service"
case clientpb.OutputFormat_SHARED_LIB:
return "Shared Library"
case clientpb.OutputFormat_GO_ARCHIVE:
return "Go Archive"
case clientpb.OutputFormat_SHELLCODE:
return "Shellcode"
default:
@@ -392,9 +389,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
configFormat = clientpb.OutputFormat_SHARED_LIB
isSharedLib = true
runAtLoad, _ = cmd.Flags().GetBool("run-at-load")
case "archive":
configFormat = clientpb.OutputFormat_GO_ARCHIVE
isSharedLib = true
case "shellcode":
configFormat = clientpb.OutputFormat_SHELLCODE
isShellcode = true
@@ -489,23 +483,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
c2Profile = consts.DefaultC2Profile
}
allProtocols, _ := cmd.Flags().GetBool("all-protocols")
includeMTLS, _ := cmd.Flags().GetBool("include-mtls")
includeHTTP, _ := cmd.Flags().GetBool("include-http")
includeWG, _ := cmd.Flags().GetBool("include-wg")
includeDNS, _ := cmd.Flags().GetBool("include-dns")
includeNamedPipe, _ := cmd.Flags().GetBool("include-named-pipe")
includeTCP, _ := cmd.Flags().GetBool("include-tcp-pivot")
if allProtocols {
includeMTLS = true
includeHTTP = true
includeWG = true
includeDNS = true
includeNamedPipe = true
includeTCP = true
}
// exports if its a shared library
config := &clientpb.ImplantConfig{
@@ -550,13 +527,6 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
DebugFile: debugFile,
HTTPC2ConfigName: c2Profile,
ShellcodeConfig: shellcodeConfig,
IncludeMTLS: includeMTLS,
IncludeHTTP: includeHTTP,
IncludeWG: includeWG,
IncludeDNS: includeDNS,
IncludeNamePipe: includeNamedPipe,
IncludeTCP: includeTCP,
}
return name, config
+1 -1
View File
@@ -197,7 +197,7 @@ func OSCompleter(con *console.SliverClient) carapace.Action {
func FormatCompleter() carapace.Action {
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
return carapace.ActionValues([]string{
"exe", "shared", "archive", "service", "shellcode",
"exe", "shared", "service", "shellcode",
}...).Tag("implant format")
})
}
@@ -1,13 +0,0 @@
package generate
import (
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
func TestNameOfOutputFormatGoArchive(t *testing.T) {
if got := nameOfOutputFormat(clientpb.OutputFormat_GO_ARCHIVE); got != "Go Archive" {
t.Fatalf("nameOfOutputFormat(GO_ARCHIVE) = %q, want %q", got, "Go Archive")
}
}
+3 -22
View File
@@ -19,7 +19,6 @@ package generate
*/
import (
"fmt"
"os"
"strings"
@@ -82,27 +81,9 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
}
}
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
return
}
} else {
_, err := externalBuild(implantName, profile.Config, nil, save, con)
if err != nil {
switch err {
case ErrNoExternalBuilder:
con.PrintErrorf("There are no external builders currently connected to the server\n")
con.PrintErrorf("See 'builders' command for more information\n")
case ErrNoValidBuilders:
con.PrintErrorf("There are external builders connected to the server, but none can build the target you specified\n")
con.PrintErrorf("Invalid target %s\n", fmt.Sprintf("%s:%s/%s", profile.Config.Format, profile.Config.GOOS, profile.Config.GOARCH))
con.PrintErrorf("See 'builders' command for more information\n")
default:
con.PrintErrorf("%s\n", err)
}
return
}
_, err := compile(implantName, profile.Config, nil, save, con)
if err != nil {
return
}
} else {
con.PrintErrorf("No profile with name '%s'", name)
-2
View File
@@ -263,8 +263,6 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
properties["format"] = "Executable"
case clientpb.OutputFormat_SHARED_LIB:
properties["format"] = "Shared Library"
case clientpb.OutputFormat_GO_ARCHIVE:
properties["format"] = "Go Archive"
case clientpb.OutputFormat_SERVICE:
properties["format"] = "Service"
case clientpb.OutputFormat_SHELLCODE:
+2 -56
View File
@@ -56,10 +56,6 @@ var (
consts.LsStr: lsHelp,
consts.CdStr: cdHelp,
consts.PwdStr: pwdHelp,
consts.AIStr: aiHelp,
consts.AIConfigStr: aiConfigHelp,
consts.DocsStr: docsHelp,
consts.OpforStr: opforHelp,
consts.CatStr: catHelp,
consts.EditStr: editHelp,
consts.HexEditStr: hexEditHelp,
@@ -171,55 +167,6 @@ _=/usr/bin/env
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
[[.Bold]]About:[[.Normal]] Get information about a Sliver by name, or for the active Sliver.`
aiHelp = `[[.Bold]]Command:[[.Normal]] ai
[[.Bold]]About:[[.Normal]] Launch the Sliver AI Bubble Tea TUI. The AI workspace now loads server-side conversation threads over gRPC, keeps them synced through the shared event stream, and renders conversation transcripts as markdown in the main pane.
[[.Bold]]Example:[[.Normal]]
use my-session
ai
`
opforHelp = `[[.Bold]]Command:[[.Normal]] opfor [script.cna|subcommand]
[[.Bold]]About:[[.Normal]] Check, run, or persistently load OPFOR-compatible CNA scripts in the Sliver client. Loaded Beacon aliases are exposed beneath the opfor namespace and use the active session or beacon. This integration intentionally focuses on BOF-oriented scripts and a small set of terminal UI and session APIs; it is not a complete Aggressor Script client implementation.
[[.Bold]]Modes:[[.Normal]]
check compiles a CNA without executing any top-level code.
run executes top-level code once with trailing arguments in @ARGV, then discards aliases, hooks, and callbacks.
load executes top-level initialization and retains aliases, hooks, and callbacks for the lifetime of this client process. A bare CNA path is shorthand for load.
help displays beacon_command_register metadata for a loaded alias without invoking it.
Most BOF CNAs, including FirefoxDump, define an alias at top level. Running such a file only creates and immediately retires that alias; load it, select a target, and invoke the alias to execute its BOF.
The default operation timeout is 600 seconds. Put --timeout before the alias name so the host consumes it rather than passing it to the CNA command.
[[.Bold]]Examples:[[.Normal]]
opfor check ./script.cna
opfor run ./script.cna argument
opfor load ./firefoxdump.cna
opfor help firefoxdump
use <session-or-beacon>
opfor firefoxdump /all
opfor --timeout 900 firefoxdump /all
opfor list
opfor unload firefoxdump.cna
`
aiConfigHelp = `[[.Bold]]Command:[[.Normal]] ai-config
[[.Bold]]About:[[.Normal]] Open a server-side form to update the AI block in server.yaml. The form stores the default provider, model, thinking level, and provider-specific credentials such as the API key and optional base URL.
[[.Bold]]Example:[[.Normal]]
ai-config
`
docsHelp = `[[.Bold]]Command:[[.Normal]] docs
[[.Bold]]About:[[.Normal]] Browse the embedded Sliver documentation in a Bubble Tea TUI. The left pane is a searchable docs browser, and the right pane renders markdown with Glow styling.
[[.Bold]]Keys:[[.Normal]] Use / to filter docs, enter to focus the selected document, tab to switch panes, and q to quit.
[[.Bold]]Example:[[.Normal]]
docs
`
useHelp = `[[.Bold]]Command:[[.Normal]] use [sliver name/session]
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
@@ -490,9 +437,8 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
+2 -2
View File
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.NetworkHelpGroup,
}
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
f.Int32P("kill", "k", -1, "kill a background job")
f.BoolP("kill-all", "K", false, "kill all jobs")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = JobsIDCompleter(con)
+1 -1
View File
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
f.StringP("filter", "f", "", "filter based on loot type")
-2
View File
@@ -38,7 +38,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.String("listen", defaultConfig.ListenAddress, "listen address for the MCP server")
f.String("name", defaultConfig.ServerName, "server name for MCP initialize")
f.String("version", defaultConfig.ServerVersion, "server version for MCP initialize")
f.BoolP("yes", "y", false, "skip the prompt-injection confirmation (broken or non-interactive terminals)")
})
flags.BindFlagCompletions(startCmd, func(comp *carapace.ActionMap) {
(*comp)["transport"] = carapace.ActionValues("http", "sse")
@@ -67,7 +66,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, consoleCmd, func(f *pflag.FlagSet) {
f.String("transport", string(defaultConfig.Transport), "mcp transport (http or sse)")
f.String("url", "", "mcp server URL (defaults to local server settings)")
f.String("auth-token", "", "MCP auth token (defaults to the local mcp.yaml token)")
})
flags.BindFlagCompletions(consoleCmd, func(comp *carapace.ActionMap) {
(*comp)["transport"] = carapace.ActionValues("http", "sse")
+4 -29
View File
@@ -6,14 +6,12 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/bishopfox/sliver/client/console"
clientmcp "github.com/bishopfox/sliver/client/mcp"
"github.com/bishopfox/sliver/client/version"
mcpclient "github.com/mark3labs/mcp-go/client"
mcptransport "github.com/mark3labs/mcp-go/client/transport"
mcpapi "github.com/mark3labs/mcp-go/mcp"
"github.com/spf13/cobra"
)
@@ -38,23 +36,7 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
}
}
authToken, _ := cmd.Flags().GetString("auth-token")
if authToken == "" {
status := clientmcp.GetStatus()
if status.AuthToken != "" {
authToken = status.AuthToken
} else {
authInfo, loadErr := clientmcp.LoadAuthInfo()
if loadErr == nil {
authToken = authInfo.Token
} else if !errors.Is(loadErr, os.ErrNotExist) {
con.PrintErrorf("%s\n", loadErr)
return
}
}
}
client, err := newConsoleClient(transport, targetURL, authToken)
client, err := newConsoleClient(transport, targetURL)
if err != nil {
con.PrintErrorf("%s\n", err)
return
@@ -86,19 +68,12 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
runConsoleREPL(ctx, client, cmd.InOrStdin(), cmd.OutOrStdout())
}
func newConsoleClient(transport clientmcp.Transport, targetURL string, authToken string) (*mcpclient.Client, error) {
if strings.TrimSpace(authToken) == "" {
return nil, fmt.Errorf("mcp auth token is required (use --auth-token or start the local MCP server first)")
}
headers := map[string]string{
clientmcp.AuthHeaderName(): authToken,
}
func newConsoleClient(transport clientmcp.Transport, targetURL string) (*mcpclient.Client, error) {
switch transport {
case clientmcp.TransportHTTP:
return mcpclient.NewStreamableHttpClient(targetURL, mcptransport.WithHTTPHeaders(headers))
return mcpclient.NewStreamableHttpClient(targetURL)
case clientmcp.TransportSSE:
return mcpclient.NewSSEMCPClient(targetURL, mcpclient.WithHeaders(headers))
return mcpclient.NewSSEMCPClient(targetURL)
default:
return nil, fmt.Errorf("unsupported transport %q", transport)
}
-9
View File
@@ -26,15 +26,6 @@ func McpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
} else {
con.PrintErrorf("Endpoint: %s\n", err)
}
if status.AuthHeader != "" {
con.Printf("Auth Header: %s\n", status.AuthHeader)
}
if status.AuthToken != "" {
con.Printf("Auth Token: %s\n", status.AuthToken)
}
if status.AuthConfigPath != "" {
con.Printf("Auth Config: %s\n", status.AuthConfigPath)
}
if status.Running && !status.StartedAt.IsZero() {
uptime := time.Since(status.StartedAt).Truncate(time.Second)
+1 -13
View File
@@ -28,10 +28,8 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}.WithDefaults()
msg := `Do you know what prompt injection is and are you an adult?`
skipConfirm, _ := cmd.Flags().GetBool("yes")
if !skipConfirm && !settings.IsUserAnAdultWithPrompt(con, msg) {
if !settings.IsUserAnAdultWithPrompt(con, msg) {
con.PrintErrorf("Failed to start MCP server, the user is not qualified to use feature\n")
con.PrintInfof("Use --yes to skip the confirmation prompt, or toggle it with 'settings autoadult'\n")
return
}
@@ -45,14 +43,4 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if err == nil {
con.PrintInfof("Endpoint: %s\n", endpoint)
}
status := slivermcp.GetStatus()
if status.AuthHeader != "" {
con.PrintInfof("Auth Header: %s\n", status.AuthHeader)
}
if status.AuthToken != "" {
con.PrintInfof("Auth Token: %s\n", status.AuthToken)
}
if status.AuthConfigPath != "" {
con.PrintInfof("Auth Config: %s\n", status.AuthConfigPath)
}
}
-516
View File
@@ -1,516 +0,0 @@
//go:build client
// Package opfor integrates OPFOR-compatible CNA scripting with the Sliver client.
package opfor
import (
"context"
"fmt"
"math"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
opforengine "github.com/sliverarmory/opfor"
"github.com/spf13/cobra"
)
const aliasInvocationTimeout = 10 * time.Minute
// Commands returns the OPFOR script manager for the implant menu.
func Commands(client *console.SliverClient) []*cobra.Command {
return []*cobra.Command{managementCommand(client, consts.SliverCoreHelpGroup)}
}
// ServerCommands returns the same persistent script manager in the server
// menu. CNA Beacon aliases themselves are registered only on implant roots.
func ServerCommands(client *console.SliverClient) []*cobra.Command {
return []*cobra.Command{managementCommand(client, consts.GenericHelpGroup)}
}
func managementCommand(client *console.SliverClient, groupID string) *cobra.Command {
command := &cobra.Command{
Use: consts.OpforStr + " [script.cna]",
Short: "Load and manage OPFOR CNA scripts",
Long: help.GetHelpFor([]string{consts.OpforStr}),
GroupID: groupID,
Args: cobra.ArbitraryArgs,
RunE: func(command *cobra.Command, args []string) error {
return runManagementCommand(command, client, args)
},
}
command.PersistentFlags().Int64P(
"timeout",
"t",
int64(aliasInvocationTimeout/time.Second),
"OPFOR operation timeout in seconds (place before a dynamic alias name)",
)
// The first non-flag token is a CNA alias (or convenience script path).
// Everything after it belongs to the script, including flag-like tokens.
command.Flags().SetInterspersed(false)
load := &cobra.Command{
Use: "load <script.cna>",
Short: "Execute and retain a CNA's aliases, hooks, and callbacks",
Args: cobra.ExactArgs(1),
RunE: func(command *cobra.Command, args []string) error {
return loadScriptCommand(command, client, args[0])
},
}
run := &cobra.Command{
Use: "run <script.cna> [arguments...]",
Short: "Execute a CNA once and discard its registrations",
Args: cobra.MinimumNArgs(1),
RunE: func(command *cobra.Command, args []string) error {
return runScriptCommand(command, client, args[0], args[1:])
},
}
// Tokens after the script path belong to @ARGV, including flag-like ones.
run.Flags().SetInterspersed(false)
check := &cobra.Command{
Use: "check <script.cna>",
Short: "Compile a CNA without executing it",
Args: cobra.ExactArgs(1),
RunE: func(command *cobra.Command, args []string) error {
return checkScriptCommand(command, client, args[0])
},
}
aliasHelp := &cobra.Command{
Use: "help <alias>",
Short: "Show help registered for a loaded CNA alias",
Args: cobra.ExactArgs(1),
RunE: func(command *cobra.Command, args []string) error {
return helpAliasCommand(command, client, args[0])
},
}
unload := &cobra.Command{
Use: "unload <script.cna>",
Short: "Unload a CNA script and retire its aliases",
Args: cobra.ExactArgs(1),
RunE: func(command *cobra.Command, args []string) error {
return unloadScriptCommand(command, client, args[0])
},
}
list := &cobra.Command{
Use: "list",
Short: "List loaded CNA scripts by absolute path",
Args: cobra.NoArgs,
RunE: func(command *cobra.Command, _ []string) error {
return listScriptsCommand(command, client)
},
}
command.AddCommand(check, aliasHelp, load, unload, list, run)
return command
}
func runManagementCommand(command *cobra.Command, client *console.SliverClient, args []string) error {
if _, err := configuredCommandTimeout(command); err != nil {
return err
}
if len(args) == 0 {
return command.Help()
}
if len(args) == 1 && strings.EqualFold(filepath.Ext(args[0]), ".cna") {
return loadScriptCommand(command, client, args[0])
}
manager, err := managerFor(client)
if err != nil {
return err
}
if manager.hasAlias(args[0]) {
return manager.runAlias(command, args[0], args[1:])
}
return fmt.Errorf("opfor: unknown CNA alias %q", args[0])
}
func loadScriptCommand(command *cobra.Command, client *console.SliverClient, path string) error {
manager, err := managerFor(client)
if err != nil {
return err
}
previousAliases := manager.aliasBindingCounts()
ctx, cancel, err := commandTimeoutContext(command)
if err != nil {
return err
}
defer cancel()
absolute, err := manager.Load(ctx, path)
if err != nil {
return err
}
manager.output.PrintSuccessf("Loaded CNA script %s\n", absolute)
currentAliases := manager.aliasBindingCounts()
aliases := make([]string, 0)
for _, alias := range manager.aliasNames() {
if currentAliases[alias] > previousAliases[alias] && !reservedManagementAlias(alias) {
aliases = append(aliases, alias)
}
}
if len(aliases) == 0 {
manager.output.PrintInfof("The script is retained for this client process; it registered no invokable Beacon aliases\n")
return nil
}
manager.output.PrintInfof("Registered CNA aliases:\n")
for _, alias := range aliases {
manager.output.Printf(" opfor %s\n", alias)
}
manager.output.PrintInfof("View CNA alias help:\n")
for _, alias := range aliases {
manager.output.Printf(" opfor help %s\n", alias)
}
return nil
}
func reservedManagementAlias(name string) bool {
switch name {
case "check", "help", "list", "load", "run", "unload":
return true
default:
return false
}
}
func runScriptCommand(command *cobra.Command, client *console.SliverClient, path string, arguments []string) error {
manager, err := managerFor(client)
if err != nil {
return err
}
ctx, cancel, err := commandTimeoutContext(command)
if err != nil {
return err
}
defer cancel()
_, err = manager.Run(ctx, path, arguments)
return err
}
func checkScriptCommand(command *cobra.Command, client *console.SliverClient, path string) error {
if _, err := configuredCommandTimeout(command); err != nil {
return err
}
manager, err := managerFor(client)
if err != nil {
return err
}
if _, err := manager.Check(path); err != nil {
return err
}
manager.output.Printf("%s: ok\n", path)
return nil
}
func helpAliasCommand(command *cobra.Command, client *console.SliverClient, name string) error {
if _, err := configuredCommandTimeout(command); err != nil {
return err
}
manager, err := managerFor(client)
if err != nil {
return err
}
metadata, err := manager.aliasMetadata(name)
if err != nil {
return err
}
description := strings.TrimSpace(metadata.Description)
if description == "" {
description = "CNA-defined Beacon alias"
}
manager.output.Printf("Command: opfor %s [arguments...]\n\n%s\n", name, description)
if detail := strings.TrimSpace(metadata.Detail); detail != "" {
manager.output.Printf("\n%s\n", detail)
}
return nil
}
func unloadScriptCommand(command *cobra.Command, client *console.SliverClient, path string) error {
manager, err := managerFor(client)
if err != nil {
return err
}
ctx, cancel, err := commandTimeoutContext(command)
if err != nil {
return err
}
defer cancel()
absolute, err := manager.Unload(ctx, path)
if err != nil {
return err
}
manager.output.PrintSuccessf("Unloaded CNA script %s\n", absolute)
return nil
}
func listScriptsCommand(command *cobra.Command, client *console.SliverClient) error {
if _, err := configuredCommandTimeout(command); err != nil {
return err
}
manager, err := managerFor(client)
if err != nil {
return err
}
paths := manager.Paths()
if len(paths) == 0 {
manager.output.PrintInfof("No CNA scripts are loaded\n")
return nil
}
manager.output.PrintInfof("Loaded CNA scripts:\n")
for _, path := range paths {
manager.output.Printf(" %s\n", path)
}
return nil
}
// RegisterCommands rehydrates the active CNA Beacon aliases beneath the
// implant menu's opfor namespace. Keeping script declarations namespaced avoids
// collisions with built-ins such as Sliver's own cat command.
func RegisterCommands(root *cobra.Command, client *console.SliverClient) {
if root == nil || client == nil {
return
}
manager, err := managerFor(client)
if err != nil {
client.PrintErrorf("%s\n", err)
return
}
namespace := directChild(root, consts.OpforStr)
if namespace == nil {
return
}
manager.commandMu.Lock()
manager.mu.Lock()
for previous := range manager.roots {
if previous == namespace {
continue
}
// A replaced menu root may still be executing asynchronously. Stop
// retaining it, but leave its Cobra tree intact for that execution.
delete(manager.roots, previous)
}
if manager.roots[namespace] == nil {
manager.roots[namespace] = make(map[string]*cobra.Command)
}
manager.mu.Unlock()
manager.syncRootLocked(namespace)
manager.commandMu.Unlock()
}
func (manager *Manager) syncAllRoots() {
manager.commandMu.Lock()
defer manager.commandMu.Unlock()
manager.mu.RLock()
roots := make([]*cobra.Command, 0, len(manager.roots))
for root := range manager.roots {
roots = append(roots, root)
}
manager.mu.RUnlock()
for _, root := range roots {
manager.syncRootLocked(root)
}
}
func (manager *Manager) syncRootLocked(root *cobra.Command) {
names := manager.aliasNames()
wanted := make(map[string]struct{}, len(names))
for _, name := range names {
wanted[name] = struct{}{}
}
catalog, _ := manager.runtime.SnapshotAggressorCommandCatalog(opforengine.AggressorCommandBeacon)
help := make(map[string]opforengine.AggressorCommandMetadata, len(catalog.Commands))
for _, metadata := range catalog.Commands {
help[metadata.Name] = metadata
}
manager.mu.Lock()
attached := manager.roots[root]
if attached == nil {
attached = map[string]*cobra.Command{}
manager.roots[root] = attached
}
for name, command := range attached {
if _, keep := wanted[name]; keep {
continue
}
root.RemoveCommand(command)
delete(attached, name)
}
manager.mu.Unlock()
for _, name := range names {
metadata := help[name]
manager.mu.RLock()
existing := attached[name]
manager.mu.RUnlock()
if existing != nil {
applyAliasHelp(existing, metadata)
continue
}
if directChild(root, name) != nil {
// A built-in/Armory command wins over an untrusted script declaration.
continue
}
aliasName := name
command := &cobra.Command{
Use: aliasName + " [arguments...]",
DisableFlagParsing: true,
RunE: func(command *cobra.Command, args []string) error {
return manager.runAlias(command, aliasName, args)
},
}
applyAliasHelp(command, metadata)
root.AddCommand(command)
manager.mu.Lock()
attached[aliasName] = command
manager.mu.Unlock()
}
}
func directChild(root *cobra.Command, name string) *cobra.Command {
for _, child := range root.Commands() {
if child.Name() == name {
return child
}
}
return nil
}
func applyAliasHelp(command *cobra.Command, metadata opforengine.AggressorCommandMetadata) {
command.Short = metadata.Description
if command.Short == "" {
command.Short = "CNA-defined Beacon alias"
}
command.Long = strings.TrimSpace(strings.Join([]string{metadata.Description, metadata.Detail}, "\n\n"))
if command.Long == "" {
command.Long = command.Short
}
}
func (manager *Manager) runAlias(command *cobra.Command, name string, args []string) error {
args, timeoutOverride, err := consumeAliasHostArguments(args)
if err != nil {
return err
}
ctx, cancel, err := commandTimeoutContextWithOverride(command, timeoutOverride)
if err != nil {
return err
}
defer cancel()
session, beacon := manager.client.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return errorsNewTargetRequired(name)
}
targetID := ""
if session != nil {
targetID = session.ID
} else {
targetID = beacon.ID
}
rawInput := name
if len(args) != 0 {
rawInput += " " + strings.Join(args, " ")
}
if err := manager.invokeAlias(ctx, name, rawInput, args, targetID); err != nil {
return fmt.Errorf("OPFOR alias %s failed: %w", name, err)
}
return nil
}
func consumeAliasHostArguments(args []string) ([]string, time.Duration, error) {
remaining := args
var timeout time.Duration
for len(remaining) != 0 {
var value string
switch argument := remaining[0]; {
case argument == "--":
return remaining[1:], timeout, nil
case argument == "--timeout" || argument == "-t":
if len(remaining) < 2 {
return nil, 0, fmt.Errorf("opfor: %s requires a timeout in seconds", argument)
}
value = remaining[1]
remaining = remaining[2:]
case strings.HasPrefix(argument, "--timeout="):
value = strings.TrimPrefix(argument, "--timeout=")
remaining = remaining[1:]
case strings.HasPrefix(argument, "-t="):
value = strings.TrimPrefix(argument, "-t=")
remaining = remaining[1:]
default:
return remaining, timeout, nil
}
parsed, err := parseAliasTimeout(value)
if err != nil {
return nil, 0, err
}
timeout = parsed
}
return remaining, timeout, nil
}
func parseAliasTimeout(value string) (time.Duration, error) {
seconds, err := strconv.ParseInt(value, 10, 64)
if err != nil || seconds <= 0 || seconds > math.MaxInt64/int64(time.Second) {
return 0, fmt.Errorf("opfor: invalid timeout %q; expected positive seconds", value)
}
return time.Duration(seconds) * time.Second, nil
}
func (manager *Manager) invokeAlias(ctx context.Context, name, rawInput string, arguments []string, targetID string) error {
if targetID == "" {
return errorsNewTargetRequired(name)
}
parsedArguments := make([]string, len(arguments))
copy(parsedArguments, arguments)
_, err := manager.runtime.InvokeConsole(ctx, opforengine.ConsoleInvocation{
Kind: opforengine.BindingAlias,
Name: name,
RawInput: rawInput,
ParsedArguments: parsedArguments,
SessionID: opforengine.String(targetID),
})
return err
}
func errorsNewTargetRequired(name string) error {
return fmt.Errorf("opfor: alias %q requires an active target", name)
}
func commandTimeoutContext(command *cobra.Command) (context.Context, context.CancelFunc, error) {
return commandTimeoutContextWithOverride(command, 0)
}
func commandTimeoutContextWithOverride(command *cobra.Command, override time.Duration) (context.Context, context.CancelFunc, error) {
parent := context.Background()
if command != nil && command.Context() != nil {
parent = command.Context()
}
timeout, err := configuredCommandTimeout(command)
if err != nil {
return nil, nil, err
}
if override > 0 {
timeout = override
}
ctx, cancel := context.WithTimeout(parent, timeout)
return ctx, cancel, nil
}
func configuredCommandTimeout(command *cobra.Command) (time.Duration, error) {
if command == nil {
return aliasInvocationTimeout, nil
}
flag := command.Flag("timeout")
if flag == nil {
return aliasInvocationTimeout, nil
}
return parseAliasTimeout(flag.Value.String())
}
-17
View File
@@ -1,17 +0,0 @@
//go:build !client
package opfor
import (
"github.com/bishopfox/sliver/client/console"
"github.com/spf13/cobra"
)
// Commands is unavailable in builds which do not include the client tag.
func Commands(_ *console.SliverClient) []*cobra.Command { return nil }
// ServerCommands is unavailable in builds which do not include the client tag.
func ServerCommands(_ *console.SliverClient) []*cobra.Command { return nil }
// RegisterCommands is a no-op outside client builds.
func RegisterCommands(_ *cobra.Command, _ *console.SliverClient) {}
-260
View File
@@ -1,260 +0,0 @@
//go:build client
package opfor
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/bishopfox/sliver/client/console"
"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"
"google.golang.org/protobuf/proto"
)
type corpusRPC struct {
rpcpb.SliverRPCClient
mu sync.Mutex
sessions []*clientpb.Session
calls []*sliverpb.CallExtensionReq
}
func (rpc *corpusRPC) GetSessions(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.Sessions, error) {
return &clientpb.Sessions{Sessions: rpc.sessions}, nil
}
func (*corpusRPC) GetBeacons(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.Beacons, error) {
return &clientpb.Beacons{}, nil
}
func (*corpusRPC) GetBeaconTaskContent(context.Context, *clientpb.BeaconTask, ...grpc.CallOption) (*clientpb.BeaconTask, error) {
return nil, fmt.Errorf("unexpected beacon task poll in session corpus test")
}
func (rpc *corpusRPC) CallExtension(_ context.Context, request *sliverpb.CallExtensionReq, _ ...grpc.CallOption) (*sliverpb.CallExtension, error) {
rpc.mu.Lock()
rpc.calls = append(rpc.calls, proto.Clone(request).(*sliverpb.CallExtensionReq))
rpc.mu.Unlock()
return &sliverpb.CallExtension{Response: &commonpb.Response{}}, nil
}
func (rpc *corpusRPC) snapshotCalls() []*sliverpb.CallExtensionReq {
rpc.mu.Lock()
defer rpc.mu.Unlock()
return append([]*sliverpb.CallExtensionReq(nil), rpc.calls...)
}
type corpusPromptUI struct{}
func (corpusPromptUI) Confirm(_ string, defaultValue bool) (bool, error) {
return defaultValue, nil
}
func (corpusPromptUI) Input(_ string, defaultValue string) (string, error) {
return defaultValue, nil
}
type corpusOutput struct{}
func (corpusOutput) Printf(string, ...any) {}
func (corpusOutput) PrintInfof(string, ...any) {}
func (corpusOutput) PrintSuccessf(string, ...any) {}
func (corpusOutput) PrintErrorf(string, ...any) {}
func newCorpusManager(t *testing.T, rpc rpcpb.SliverRPCClient) *Manager {
t.Helper()
client := console.NewConsole(false)
client.Rpc = rpc
manager, err := newManagerWithOutput(client, corpusPromptUI{}, corpusOutput{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = manager.runtime.Close(context.Background())
})
return manager
}
func materializeCorpusScript(t *testing.T, fixture, scriptName, objectPath string, object []byte) string {
t.Helper()
source, err := os.ReadFile(filepath.Join("testdata", "corpus", filepath.FromSlash(fixture)))
if err != nil {
t.Fatal(err)
}
root := t.TempDir()
scriptPath := filepath.Join(root, scriptName)
if err := os.WriteFile(scriptPath, source, 0o600); err != nil {
t.Fatal(err)
}
resourcePath := filepath.Join(root, filepath.FromSlash(objectPath))
if err := os.MkdirAll(filepath.Dir(resourcePath), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(resourcePath, object, 0o600); err != nil {
t.Fatal(err)
}
return scriptPath
}
func expectedStringArguments(value string) []byte {
payload := make([]byte, 4+len(value)+1)
payload[0] = byte(len(value) + 1)
payload[1] = byte((len(value) + 1) >> 8)
payload[2] = byte((len(value) + 1) >> 16)
payload[3] = byte((len(value) + 1) >> 24)
copy(payload[4:], value)
arguments := make([]byte, 4+len(payload))
arguments[0] = byte(len(payload))
arguments[1] = byte(len(payload) >> 8)
arguments[2] = byte(len(payload) >> 16)
arguments[3] = byte(len(payload) >> 24)
copy(arguments[4:], payload)
return arguments
}
type corpusDispatchTest struct {
name string
fixture string
scriptName string
objectPath string
alias string
arguments []string
packedString *string
objectContent []byte
}
func TestSliverArmoryCNACorpusDispatchesBOFs(t *testing.T) {
tests := []corpusDispatchTest{
{
name: "FirefoxDump all",
fixture: "firefoxdump/firefoxdump.cna",
scriptName: "firefoxdump.cna",
objectPath: "bin/firefoxdump.x64.o",
alias: "firefoxdump",
arguments: []string{"/all"},
packedString: stringPointer("/all"),
objectContent: []byte("firefoxdump-x64-object"),
},
{
name: "bof_collection cat path with spaces",
fixture: "bof_collection/cat.cna",
scriptName: "cat.cna",
objectPath: "dist/cat.x64.o",
alias: "cat",
arguments: []string{`C:\Program`, `Files\notes.txt`},
packedString: stringPointer(`C:\Program Files\notes.txt`),
objectContent: []byte("cat-x64-object"),
},
{
name: "OperatorsKit FindDotnet no arguments",
fixture: "operatorskit/finddotnet.cna",
scriptName: "finddotnet.cna",
objectPath: "finddotnet.o",
alias: "finddotnet",
objectContent: []byte("finddotnet-x64-object"),
},
{
name: "OperatorsKit FindSysmon registry",
fixture: "operatorskit/findsysmon.cna",
scriptName: "findsysmon.cna",
objectPath: "findsysmon.o",
alias: "findsysmon",
arguments: []string{"reg"},
packedString: stringPointer("reg"),
objectContent: []byte("findsysmon-x64-object"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) { runCorpusDispatchTest(t, test) })
}
}
func runCorpusDispatchTest(t *testing.T, test corpusDispatchTest) {
t.Helper()
session := &clientpb.Session{
ID: "session-corpus",
Name: "corpus",
OS: "windows",
Arch: "amd64",
Capabilities: sliverpb.CapabilityBOFV1,
}
rpc := &corpusRPC{sessions: []*clientpb.Session{session}}
manager := newCorpusManager(t, rpc)
scriptPath := materializeCorpusScript(
t, test.fixture, test.scriptName, test.objectPath, test.objectContent,
)
loadedPath, err := manager.Load(context.Background(), scriptPath)
if err != nil {
t.Fatalf("load pinned %s fixture: %v", test.fixture, err)
}
if loadedPath != scriptPath {
t.Fatalf("loaded path = %q, want %q", loadedPath, scriptPath)
}
assertCorpusAliases(t, manager.aliasNames(), test.alias)
rawInput := strings.Join(append([]string{test.alias}, test.arguments...), " ")
if err := manager.invokeAlias(context.Background(), test.alias, rawInput, test.arguments, session.ID); err != nil {
t.Fatalf("invoke %s: %v", test.alias, err)
}
calls := rpc.snapshotCalls()
if len(calls) != 1 {
t.Fatalf("CallExtension calls = %d, want 1", len(calls))
}
assertCorpusCall(t, calls[0], test, session.ID)
if _, err := manager.Unload(context.Background(), scriptPath); err != nil {
t.Fatalf("unload %s: %v", test.alias, err)
}
if names := manager.aliasNames(); len(names) != 0 {
t.Fatalf("aliases after unload = %v, want none", names)
}
}
func assertCorpusAliases(t *testing.T, names []string, want string) {
t.Helper()
if len(names) != 1 || names[0] != want {
t.Fatalf("registered aliases = %v, want [%s]", names, want)
}
}
func assertCorpusCall(t *testing.T, call *sliverpb.CallExtensionReq, test corpusDispatchTest, sessionID string) {
t.Helper()
if !call.IsBOF || call.Export != "go" {
t.Fatalf("CallExtension BOF/export = %v/%q, want true/go", call.IsBOF, call.Export)
}
if !bytes.Equal(call.BOFData, test.objectContent) {
t.Fatalf("CallExtension BOF bytes = %q, want %q", call.BOFData, test.objectContent)
}
wantArguments := []byte{0, 0, 0, 0}
if test.packedString != nil {
wantArguments = expectedStringArguments(*test.packedString)
}
if !bytes.Equal(call.Args, wantArguments) {
t.Fatalf("CallExtension args = %x, want Reflektor framing %x", call.Args, wantArguments)
}
if call.GetRequest().GetSessionID() != sessionID || call.GetRequest().GetAsync() {
t.Fatalf("CallExtension request = %#v, want synchronous session %s", call.GetRequest(), sessionID)
}
if len(call.Name) != 64 {
t.Fatalf("CallExtension name digest length = %d, want 64", len(call.Name))
}
}
func stringPointer(value string) *string {
return &value
}
-416
View File
@@ -1,416 +0,0 @@
//go:build client
package opfor
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/bishopfox/sliver/client/console"
opforengine "github.com/sliverarmory/opfor"
"github.com/spf13/cobra"
)
// Manager owns one long-lived OPFOR runtime for a Sliver client. Keeping the
// runtime alive is important: CNA aliases, hooks, and callbacks are scoped to
// the Script returned by Runtime.Load.
type Manager struct {
client *console.SliverClient
output clientOutput
runtime *opforengine.Runtime
ui promptUI
loadMu sync.Mutex
commandMu sync.Mutex
promptMu sync.Mutex
mu sync.RWMutex
scripts map[string]*opforengine.Script
aliases map[string][]opforengine.Binding
roots map[*cobra.Command]map[string]*cobra.Command
}
type managerEntry struct {
manager *Manager
err error
}
const oneShotRuntimeCloseTimeout = 5 * time.Second
var (
clientManagersMu sync.Mutex
clientManagers = map[*console.SliverClient]managerEntry{}
)
func managerFor(client *console.SliverClient) (*Manager, error) {
if client == nil {
return nil, errors.New("opfor: Sliver client is nil")
}
clientManagersMu.Lock()
defer clientManagersMu.Unlock()
if entry, found := clientManagers[client]; found {
return entry.manager, entry.err
}
manager, err := newManager(client, formsPromptUI{})
clientManagers[client] = managerEntry{manager: manager, err: err}
return manager, err
}
func newManager(client *console.SliverClient, ui promptUI) (*Manager, error) {
return newManagerWithOutput(client, ui, client)
}
func newManagerWithOutput(client *console.SliverClient, ui promptUI, output clientOutput) (*Manager, error) {
if client == nil {
return nil, errors.New("opfor: Sliver client is nil")
}
if ui == nil {
return nil, errors.New("opfor: prompt UI is nil")
}
if output == nil {
return nil, errors.New("opfor: output sink is nil")
}
manager := &Manager{
client: client,
output: output,
ui: ui,
scripts: map[string]*opforengine.Script{},
aliases: map[string][]opforengine.Binding{},
roots: map[*cobra.Command]map[string]*cobra.Command{},
}
runtime, err := manager.newRuntime(true)
if err != nil {
return nil, fmt.Errorf("opfor: create runtime: %w", err)
}
manager.runtime = runtime
return manager, nil
}
func (manager *Manager) newRuntime(observeBindings bool) (*opforengine.Runtime, error) {
stdout := clientWriter{output: manager.output}
stderr := clientWriter{output: manager.output, stderr: true}
options := []opforengine.Option{
opforengine.WithBOFPackByteOrder(opforengine.BOFPackLittleEndian),
opforengine.WithAggressorBeaconExecutionProvider(
opforengine.AggressorBeaconExecutionProviderFunc(manager.executeBeacon),
),
opforengine.WithAggressorSessionQueryProvider(
opforengine.AggressorSessionQueryProviderFunc(manager.querySession),
),
opforengine.WithAggressorBeaconTranscriptSink(
opforengine.AggressorBeaconTranscriptSinkFunc(manager.publishTranscript),
),
opforengine.WithAggressorPromptProvider(
opforengine.AggressorPromptProviderFunc(manager.presentPrompt),
),
opforengine.WithStdout(stdout),
opforengine.WithStderr(stderr),
}
if observeBindings {
options = append(options, opforengine.WithBindingObserver(manager))
}
return opforengine.New(options...)
}
// Registered observes declarations published by loaded scripts. Only Beacon
// aliases become Sliver commands; other binding families remain available to
// OPFOR internally for hooks, events, and callback composition.
func (manager *Manager) Registered(_ context.Context, binding opforengine.Binding) error {
if binding.Kind != opforengine.BindingAlias {
return nil
}
manager.mu.Lock()
manager.aliases[binding.Name] = append(manager.aliases[binding.Name], binding)
manager.mu.Unlock()
return nil
}
// Unregistered removes the exact binding generation which retired. An older
// alias with the same name remains visible when scripts were layered.
func (manager *Manager) Unregistered(_ context.Context, binding opforengine.Binding) error {
if binding.Kind != opforengine.BindingAlias {
return nil
}
manager.mu.Lock()
bindings := manager.aliases[binding.Name]
for index := len(bindings) - 1; index >= 0; index-- {
if bindings[index].ID == binding.ID && bindings[index].Script == binding.Script {
bindings = append(bindings[:index], bindings[index+1:]...)
break
}
}
if len(bindings) == 0 {
delete(manager.aliases, binding.Name)
} else {
manager.aliases[binding.Name] = bindings
}
manager.mu.Unlock()
return nil
}
// Load compiles and executes a CNA, retaining its registrations until unload.
func (manager *Manager) Load(ctx context.Context, path string) (string, error) {
absolute, content, err := readCNAFile(path)
if err != nil {
return "", err
}
manager.loadMu.Lock()
defer manager.loadMu.Unlock()
manager.mu.RLock()
_, exists := manager.scripts[absolute]
manager.mu.RUnlock()
if exists {
return "", fmt.Errorf("opfor: script is already loaded: %s", absolute)
}
program, err := manager.runtime.Compile(opforengine.NewSource(absolute, content))
if err != nil {
return "", fmt.Errorf("opfor: compile %s: %w", absolute, err)
}
script, err := manager.runtime.Load(ctx, program)
if err != nil {
return "", fmt.Errorf("opfor: load %s: %w", absolute, err)
}
manager.mu.Lock()
manager.scripts[absolute] = script
manager.mu.Unlock()
manager.syncAllRoots()
return absolute, nil
}
// Check compiles a CNA without creating a Script or executing top-level code.
// Standalone compilation intentionally mirrors the OPFOR CLI check command and
// avoids charging the long-lived Sliver runtime's source budget.
func (manager *Manager) Check(path string) (string, error) {
absolute, _, err := compileCNAFile(path)
return absolute, err
}
// Run executes a CNA once with arguments exposed through Sleep's @ARGV. Any
// aliases, hooks, callbacks, or events registered by the script are retired
// before this method returns, matching the OPFOR CLI run command.
func (manager *Manager) Run(ctx context.Context, path string, arguments []string) (string, error) {
absolute, program, err := compileCNAFile(path)
if err != nil {
return "", err
}
values := make([]opforengine.Value, len(arguments))
for index, argument := range arguments {
values[index] = opforengine.String(argument)
}
// A one-shot runtime matches the standalone OPFOR CLI and prevents
// temporary registrations from becoming visible to the persistent Sliver
// manager. It retains all Sliver providers but deliberately omits the
// binding observer used to publish persistent aliases.
runtime, err := manager.newRuntime(false)
if err != nil {
return "", fmt.Errorf("opfor: create runtime: %w", err)
}
_, executeErr := runtime.Execute(ctx, program, values...)
closeCtx, cancel := context.WithTimeout(context.Background(), oneShotRuntimeCloseTimeout)
closeErr := runtime.Close(closeCtx)
cancel()
if executeErr != nil || closeErr != nil {
var runErr error
if executeErr != nil {
runErr = fmt.Errorf("opfor: execute %s: %w", absolute, executeErr)
}
if closeErr != nil {
runErr = errors.Join(runErr, fmt.Errorf("opfor: close runtime: %w", closeErr))
}
return "", runErr
}
return absolute, nil
}
// Unload retires a loaded CNA and all registrations owned by that script.
func (manager *Manager) Unload(ctx context.Context, path string) (string, error) {
manager.loadMu.Lock()
defer manager.loadMu.Unlock()
absolute, script, err := manager.findScript(path)
if err != nil {
return "", err
}
if err := script.Unload(ctx); err != nil {
return "", fmt.Errorf("opfor: unload %s: %w", absolute, err)
}
manager.mu.Lock()
delete(manager.scripts, absolute)
manager.mu.Unlock()
manager.syncAllRoots()
return absolute, nil
}
func (manager *Manager) findScript(path string) (string, *opforengine.Script, error) {
absolute, absoluteErr := filepath.Abs(path)
if absoluteErr == nil {
absolute = filepath.Clean(absolute)
}
manager.mu.RLock()
defer manager.mu.RUnlock()
if absoluteErr == nil {
if script := manager.scripts[absolute]; script != nil {
return absolute, script, nil
}
}
base := filepath.Base(path)
var matchedPath string
var matchedScript *opforengine.Script
for candidate, script := range manager.scripts {
if filepath.Base(candidate) != base {
continue
}
if matchedScript != nil {
return "", nil, fmt.Errorf("opfor: script name %q is ambiguous; use an absolute path", path)
}
matchedPath, matchedScript = candidate, script
}
if matchedScript == nil {
return "", nil, fmt.Errorf("opfor: script is not loaded: %s", path)
}
return matchedPath, matchedScript, nil
}
// Paths returns the sorted absolute paths of all loaded CNA scripts.
func (manager *Manager) Paths() []string {
manager.mu.RLock()
paths := make([]string, 0, len(manager.scripts))
for path := range manager.scripts {
paths = append(paths, path)
}
manager.mu.RUnlock()
sort.Strings(paths)
return paths
}
func (manager *Manager) aliasNames() []string {
manager.mu.RLock()
names := make([]string, 0, len(manager.aliases))
for name, bindings := range manager.aliases {
if len(bindings) != 0 {
names = append(names, name)
}
}
manager.mu.RUnlock()
sort.Strings(names)
return names
}
func (manager *Manager) aliasBindingCounts() map[string]int {
manager.mu.RLock()
counts := make(map[string]int, len(manager.aliases))
for name, bindings := range manager.aliases {
if len(bindings) != 0 {
counts[name] = len(bindings)
}
}
manager.mu.RUnlock()
return counts
}
func (manager *Manager) hasAlias(name string) bool {
manager.mu.RLock()
defer manager.mu.RUnlock()
return len(manager.aliases[name]) != 0
}
func (manager *Manager) aliasMetadata(name string) (opforengine.AggressorCommandMetadata, error) {
if reservedManagementAlias(name) || !manager.hasAlias(name) {
return opforengine.AggressorCommandMetadata{}, fmt.Errorf("opfor: unknown CNA alias %q", name)
}
catalog, err := manager.runtime.SnapshotAggressorCommandCatalog(opforengine.AggressorCommandBeacon)
if err != nil {
return opforengine.AggressorCommandMetadata{}, fmt.Errorf("opfor: snapshot Beacon command help: %w", err)
}
for _, metadata := range catalog.Commands {
if metadata.Name == name {
return metadata, nil
}
}
return opforengine.AggressorCommandMetadata{Name: name}, nil
}
func absoluteCNAPath(path string) (string, error) {
if strings.TrimSpace(path) == "" {
return "", errors.New("opfor: CNA script path is empty")
}
absolute, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("opfor: resolve %s: %w", path, err)
}
absolute = filepath.Clean(absolute)
if !strings.EqualFold(filepath.Ext(absolute), ".cna") {
return "", fmt.Errorf("opfor: script must have a .cna extension: %s", absolute)
}
return absolute, nil
}
func readCNAFile(path string) (string, []byte, error) {
absolute, err := absoluteCNAPath(path)
if err != nil {
return "", nil, err
}
content, err := os.ReadFile(absolute)
if err != nil {
return "", nil, fmt.Errorf("opfor: read %s: %w", absolute, err)
}
return absolute, content, nil
}
func compileCNAFile(path string) (string, *opforengine.Program, error) {
absolute, content, err := readCNAFile(path)
if err != nil {
return "", nil, err
}
program, err := opforengine.Compile(opforengine.NewSource(absolute, content))
if err != nil {
return "", nil, fmt.Errorf("opfor: compile %s: %w", absolute, err)
}
return absolute, program, nil
}
type clientWriter struct {
output clientOutput
stderr bool
}
func (writer clientWriter) Write(data []byte) (int, error) {
if writer.output == nil || len(data) == 0 {
return len(data), nil
}
if writer.stderr {
writer.output.PrintErrorf("%s", data)
} else {
writer.output.Printf("%s", data)
}
return len(data), nil
}
type clientOutput interface {
Printf(string, ...any)
PrintInfof(string, ...any)
PrintSuccessf(string, ...any)
PrintErrorf(string, ...any)
}
var _ io.Writer = clientWriter{}
var _ opforengine.BindingObserver = (*Manager)(nil)
File diff suppressed because it is too large. Load diff
-498
View File
@@ -1,498 +0,0 @@
//go:build client
package opfor
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
opforengine "github.com/sliverarmory/opfor"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
const (
beaconTaskPollInterval = 250 * time.Millisecond
bofRequestFallback = 59 * time.Second
bofRequestDeadlineMargin = time.Second
bofOutputDefault = int32(0x00)
bofOutputError = int32(0x0d)
)
type opforRPC interface {
GetSessions(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.Sessions, error)
GetBeacons(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.Beacons, error)
GetBeaconTaskContent(context.Context, *clientpb.BeaconTask, ...grpc.CallOption) (*clientpb.BeaconTask, error)
CallExtension(context.Context, *sliverpb.CallExtensionReq, ...grpc.CallOption) (*sliverpb.CallExtension, error)
}
var _ opforRPC = (rpcpb.SliverRPCClient)(nil)
type resolvedTarget struct {
session *clientpb.Session
beacon *clientpb.Beacon
}
func (target resolvedTarget) id() string {
if target.session != nil {
return target.session.ID
}
if target.beacon != nil {
return target.beacon.ID
}
return ""
}
func (target resolvedTarget) capabilities() uint64 {
if target.session != nil {
return target.session.Capabilities
}
if target.beacon != nil {
return target.beacon.Capabilities
}
return 0
}
func (target resolvedTarget) arch() string {
if target.session != nil {
return target.session.Arch
}
if target.beacon != nil {
return target.beacon.Arch
}
return ""
}
func (manager *Manager) rpc() (opforRPC, error) {
if manager == nil || manager.client == nil || manager.client.Rpc == nil {
return nil, errors.New("opfor: Sliver RPC client is unavailable")
}
return manager.client.Rpc, nil
}
func (manager *Manager) resolveTarget(ctx context.Context, id string) (resolvedTarget, error) {
if strings.TrimSpace(id) == "" {
return resolvedTarget{}, errors.New("opfor: target ID is empty")
}
rpc, err := manager.rpc()
if err != nil {
return resolvedTarget{}, err
}
sessions, err := rpc.GetSessions(ctx, &commonpb.Empty{})
if err != nil {
return resolvedTarget{}, fmt.Errorf("opfor: list sessions: %w", err)
}
for _, session := range sessions.GetSessions() {
if session != nil && session.ID == id {
return resolvedTarget{session: session}, nil
}
}
beacons, err := rpc.GetBeacons(ctx, &commonpb.Empty{})
if err != nil {
return resolvedTarget{}, fmt.Errorf("opfor: list beacons: %w", err)
}
for _, beacon := range beacons.GetBeacons() {
if beacon != nil && beacon.ID == id {
return resolvedTarget{beacon: beacon}, nil
}
}
return resolvedTarget{}, fmt.Errorf("opfor: target %q was not found", id)
}
func (manager *Manager) executeBeacon(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
) (opforengine.Value, error) {
targetID, call, err := manager.prepareBeaconExecution(ctx, request)
if err != nil {
return opforengine.Null(), err
}
rpc, err := manager.rpc()
if err != nil {
return opforengine.Null(), err
}
response, err := rpc.CallExtension(ctx, call)
if err != nil {
executionErr := fmt.Errorf("opfor: execute BOF on %s: %w", targetID, err)
return opforengine.Null(), notifyBOFLifecycleError(ctx, request, "", executionErr)
}
taskID := response.GetResponse().GetTaskID()
response, err = manager.resolveCallExtensionResponse(ctx, rpc, response)
if err != nil {
return opforengine.Null(), notifyBOFLifecycleError(ctx, request, taskID, err)
}
if taskID == "" {
taskID = response.GetResponse().GetTaskID()
}
return manager.completeBeaconExecution(ctx, request, taskID, response)
}
func (manager *Manager) prepareBeaconExecution(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
) (string, *sliverpb.CallExtensionReq, error) {
if request.Kind != opforengine.AggressorBeaconInlineExecute {
return "", nil, fmt.Errorf("opfor: unsupported Beacon execution function %q", request.Name)
}
if err := ctx.Err(); err != nil {
return "", nil, err
}
targetID := request.BeaconID.String()
target, err := manager.resolveTarget(ctx, targetID)
if err != nil {
return "", nil, err
}
if _, err := beaconArchitecture(target.arch()); err != nil {
return "", nil, err
}
if target.capabilities()&sliverpb.CapabilityBOFV1 == 0 {
return "", nil, fmt.Errorf("opfor: target %q does not advertise the bof_v1 capability", targetID)
}
bofData, ok := request.Content.Bytes()
if !ok {
return "", nil, errors.New("opfor: beacon_inline_execute BOF content is not a byte string")
}
packed, ok := request.PackedArguments.Bytes()
if !ok {
packed = []byte(request.PackedArguments.String())
}
arguments, err := prefixBOFArguments(packed)
if err != nil {
return "", nil, err
}
digest := sha256.Sum256(bofData)
return targetID, &sliverpb.CallExtensionReq{
Name: hex.EncodeToString(digest[:]),
Args: arguments,
Export: request.EntryPoint.String(),
BOFData: bofData,
IsBOF: true,
// Older implants ignore this additive request bit and return legacy
// flattened Output, which bofOutputRecords continues to accept.
WantBOFOutputs: true,
Request: targetRequest(ctx, target),
}, nil
}
func (manager *Manager) completeBeaconExecution(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
taskID string,
response *sliverpb.CallExtension,
) (opforengine.Value, error) {
records := bofOutputRecords(response)
var executionErr error
if response.GetResponse().GetErr() != "" {
executionErr = fmt.Errorf("opfor: BOF execution failed: %s", response.GetResponse().GetErr())
}
if request.Callback == nil {
manager.renderBOFOutputs(records)
if executionErr != nil {
return opforengine.Null(), executionErr
}
return opforengine.Null(), nil
}
for index, record := range records {
chunkNumber := index + 1
isFinal := index == len(records)-1
if err := invokeBOFOutputCallback(ctx, request, record, chunkNumber, isFinal); err != nil {
callbackErr := fmt.Errorf("opfor: BOF result callback chunk %d: %w", chunkNumber, err)
if executionErr != nil {
return opforengine.Null(), errors.Join(executionErr, callbackErr)
}
return opforengine.Null(), callbackErr
}
}
if executionErr != nil {
return opforengine.Null(), notifyBOFLifecycleError(ctx, request, taskID, executionErr)
}
if taskID != "" {
if err := invokeBOFLifecycleCallback(ctx, request, "task_completed", taskID, nil); err != nil {
return opforengine.Null(), fmt.Errorf("opfor: BOF task_completed callback: %w", err)
}
} else if len(records) == 0 {
if err := invokeBOFLifecycleCallback(ctx, request, "success", "", nil); err != nil {
return opforengine.Null(), fmt.Errorf("opfor: BOF success callback: %w", err)
}
}
return opforengine.Null(), nil
}
// bofOutputRecords prefers the typed wire representation. Older servers and
// implants populate only Output, which remains one default-channel record.
func bofOutputRecords(response *sliverpb.CallExtension) []*sliverpb.BOFOutput {
if response == nil {
return nil
}
if records := response.GetBOFOutputs(); len(records) != 0 {
return records
}
if len(response.GetOutput()) == 0 {
return nil
}
return []*sliverpb.BOFOutput{{
Type: bofOutputDefault,
Data: response.GetOutput(),
}}
}
// The callback is deliberately multi-shot. Data records retain their exact
// byte strings and raw Cobalt output channel. chunk_num is one-based and
// is_final marks the final data record, independently of task lifecycle events.
func invokeBOFOutputCallback(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
record *sliverpb.BOFOutput,
chunkNumber int,
isFinal bool,
) error {
if request.Callback == nil {
return nil
}
if record == nil {
record = &sliverpb.BOFOutput{}
}
kind := "output"
if record.GetType() == bofOutputError {
kind = "error"
}
information := opforengine.NewOrderedHash()
information.Set("type", opforengine.String(kind))
information.Set("type_id", opforengine.Int(record.GetType()))
information.Set("chunk_num", opforengine.Int(int32(chunkNumber)))
information.Set("is_final", opforengine.Bool(isFinal))
_, err := request.Callback.Invoke(
ctx,
request.BeaconID,
opforengine.BinaryString(record.GetData()),
opforengine.HashValue(information),
)
return err
}
func invokeBOFLifecycleCallback(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
kind string,
taskID string,
result []byte,
) error {
if request.Callback == nil {
return nil
}
information := opforengine.NewOrderedHash()
information.Set("type", opforengine.String(kind))
if taskID != "" {
information.Set("taskid", opforengine.String(taskID))
}
_, err := request.Callback.Invoke(
ctx,
request.BeaconID,
opforengine.BinaryString(result),
opforengine.HashValue(information),
)
return err
}
func notifyBOFLifecycleError(
ctx context.Context,
request opforengine.AggressorBeaconExecutionRequest,
taskID string,
executionErr error,
) error {
if request.Callback == nil {
return executionErr
}
if ctx != nil && ctx.Err() != nil {
return executionErr
}
callbackErr := invokeBOFLifecycleCallback(
ctx, request, "error", taskID, []byte(executionErr.Error()),
)
if callbackErr != nil {
return errors.Join(executionErr, fmt.Errorf("opfor: BOF error callback: %w", callbackErr))
}
return executionErr
}
func (manager *Manager) renderBOFOutputs(records []*sliverpb.BOFOutput) {
wrote := false
endedWithNewline := true
for _, record := range records {
if record == nil || len(record.GetData()) == 0 {
continue
}
text := safeBOFText(record.GetData())
if text == "" {
continue
}
wrote = true
endedWithNewline = strings.HasSuffix(text, "\n")
if record.GetType() == bofOutputError {
manager.output.PrintErrorf("%s", text)
} else {
manager.output.Printf("%s", text)
}
}
if wrote && !endedWithNewline {
manager.output.Printf("\n")
}
}
// safeBOFText prevents untrusted native output from injecting terminal control
// sequences. Valid printable UTF-8 plus tab/newline/carriage return is retained;
// invalid bytes and other control bytes are rendered as hexadecimal escapes.
func safeBOFText(data []byte) string {
var result strings.Builder
result.Grow(len(data))
for len(data) != 0 {
r, size := utf8.DecodeRune(data)
if r == utf8.RuneError && size == 1 {
fmt.Fprintf(&result, `\x%02x`, data[0])
data = data[1:]
continue
}
if r == '\n' || r == '\r' || r == '\t' || unicode.IsPrint(r) {
result.WriteRune(r)
} else {
for _, value := range data[:size] {
fmt.Fprintf(&result, `\x%02x`, value)
}
}
data = data[size:]
}
return result.String()
}
func prefixBOFArguments(packed []byte) ([]byte, error) {
if uint64(len(packed)) > math.MaxUint32 {
return nil, errors.New("opfor: BOF argument buffer exceeds uint32 length")
}
arguments := make([]byte, 4+len(packed))
binary.LittleEndian.PutUint32(arguments, uint32(len(packed)))
copy(arguments[4:], packed)
return arguments, nil
}
func targetRequest(ctx context.Context, target resolvedTarget) *commonpb.Request {
request := &commonpb.Request{Timeout: int64(bofRequestTimeout(ctx))}
if target.session != nil {
request.SessionID = target.session.ID
request.Async = false
} else if target.beacon != nil {
request.BeaconID = target.beacon.ID
request.Async = true
}
return request
}
func bofRequestTimeout(ctx context.Context) time.Duration {
if ctx == nil {
return bofRequestFallback
}
deadline, ok := ctx.Deadline()
if !ok {
return bofRequestFallback
}
remaining := time.Until(deadline)
if remaining <= 0 {
return time.Nanosecond
}
if remaining > bofRequestDeadlineMargin {
remaining -= bofRequestDeadlineMargin
}
return remaining
}
func (manager *Manager) resolveCallExtensionResponse(
ctx context.Context,
rpc opforRPC,
response *sliverpb.CallExtension,
) (*sliverpb.CallExtension, error) {
if response == nil {
return nil, errors.New("opfor: BOF execution returned an empty response")
}
if response.GetResponse().GetErr() != "" || !response.GetResponse().GetAsync() {
return response, nil
}
taskID := response.GetResponse().GetTaskID()
if taskID == "" {
return nil, errors.New("opfor: asynchronous BOF execution returned no task ID")
}
ticker := time.NewTicker(beaconTaskPollInterval)
defer ticker.Stop()
for {
task, err := rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{ID: taskID})
if err != nil {
return nil, fmt.Errorf("opfor: fetch beacon task %s: %w", taskID, err)
}
state := strings.ToLower(task.GetState())
switch state {
case "completed":
if len(task.Response) == 0 {
return nil, fmt.Errorf("opfor: beacon task %s returned an empty response", taskID)
}
completed := &sliverpb.CallExtension{}
if err := proto.Unmarshal(task.Response, completed); err != nil {
return nil, fmt.Errorf("opfor: decode beacon task %s response: %w", taskID, err)
}
if completed.Response == nil {
completed.Response = &commonpb.Response{}
}
if completed.Response.TaskID == "" {
completed.Response.TaskID = taskID
}
return completed, nil
case "failed", "canceled", "cancelled":
return nil, fmt.Errorf("opfor: beacon task %s %s", taskID, state)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
}
}
}
func (manager *Manager) publishTranscript(
_ context.Context,
record opforengine.AggressorBeaconTranscriptRecord,
) error {
target := record.BeaconID.String()
text := record.Text.String()
prefix := ""
if target != "" {
prefix = "[" + target + "] "
}
switch record.Kind {
case opforengine.AggressorBeaconTranscriptError,
opforengine.AggressorBeaconTranscriptJobError:
manager.output.PrintErrorf("%s%s\n", prefix, text)
case opforengine.AggressorBeaconTranscriptTask,
opforengine.AggressorBeaconTranscriptTaskCompleted:
manager.output.PrintInfof("%s%s\n", prefix, text)
default:
manager.output.Printf("%s%s\n", prefix, text)
}
return nil
}
-218
View File
@@ -1,218 +0,0 @@
//go:build client
package opfor
import (
"context"
"fmt"
"strings"
"github.com/bishopfox/sliver/protobuf/commonpb"
opforengine "github.com/sliverarmory/opfor"
)
func (manager *Manager) querySession(
ctx context.Context,
query opforengine.AggressorSessionQuery,
) (opforengine.Value, error) {
if err := ctx.Err(); err != nil {
return opforengine.Null(), err
}
switch query.Kind {
case opforengine.AggressorSessionQueryBeacons,
opforengine.AggressorSessionQueryBeaconIDs:
return manager.queryAllSessions(ctx, query.Kind)
default:
return manager.querySingleSession(ctx, query)
}
}
func (manager *Manager) queryAllSessions(
ctx context.Context,
kind opforengine.AggressorSessionQueryKind,
) (opforengine.Value, error) {
targets, err := manager.allTargets(ctx)
if err != nil {
return opforengine.Null(), err
}
values := make([]opforengine.Value, 0, len(targets))
for _, target := range targets {
if kind == opforengine.AggressorSessionQueryBeaconIDs {
values = append(values, opforengine.String(target.id()))
} else {
values = append(values, opforengine.HashValue(targetMetadata(target)))
}
}
return opforengine.ArrayValue(opforengine.NewArray(values...)), nil
}
func (manager *Manager) querySingleSession(
ctx context.Context,
query opforengine.AggressorSessionQuery,
) (opforengine.Value, error) {
target, err := manager.resolveTarget(ctx, query.SessionID.String())
if err != nil {
return opforengine.Null(), err
}
metadata := targetMetadata(target)
switch query.Kind {
case opforengine.AggressorSessionQueryBeaconArchitecture,
opforengine.AggressorSessionQueryBeaconData,
opforengine.AggressorSessionQueryBeaconInfo,
opforengine.AggressorSessionQueryIs64:
return querySessionMetadata(target, metadata, query)
case opforengine.AggressorSessionQueryIsActive,
opforengine.AggressorSessionQueryIsAdmin,
opforengine.AggressorSessionQueryIsBeacon,
opforengine.AggressorSessionQueryIsSSH:
return querySessionStatus(target, query)
default:
return opforengine.Null(), fmt.Errorf("opfor: unsupported session query %q", query.Name)
}
}
func querySessionMetadata(
target resolvedTarget,
metadata *opforengine.Hash,
query opforengine.AggressorSessionQuery,
) (opforengine.Value, error) {
switch query.Kind {
case opforengine.AggressorSessionQueryBeaconArchitecture:
architecture, err := beaconArchitecture(target.arch())
if err != nil {
return opforengine.Null(), err
}
return opforengine.String(architecture), nil
case opforengine.AggressorSessionQueryBeaconData:
return opforengine.HashValue(metadata), nil
case opforengine.AggressorSessionQueryBeaconInfo:
value, found := metadata.Get(strings.ToLower(query.Key.String()))
if !found {
return opforengine.Null(), nil
}
return value, nil
case opforengine.AggressorSessionQueryIs64:
architecture, err := beaconArchitecture(target.arch())
if err != nil {
return opforengine.Null(), err
}
return opforengine.Bool(architecture == "x64"), nil
default:
return opforengine.Null(), fmt.Errorf("opfor: unsupported session metadata query %q", query.Name)
}
}
func querySessionStatus(
target resolvedTarget,
query opforengine.AggressorSessionQuery,
) (opforengine.Value, error) {
switch query.Kind {
case opforengine.AggressorSessionQueryIsActive:
if target.session != nil {
return opforengine.Bool(!target.session.IsDead), nil
}
return opforengine.Bool(target.beacon != nil && !target.beacon.IsDead), nil
case opforengine.AggressorSessionQueryIsAdmin:
integrity := strings.ToLower(targetIntegrity(target))
return opforengine.Bool(strings.Contains(integrity, "high") || strings.Contains(integrity, "system") || strings.Contains(integrity, "admin")), nil
case opforengine.AggressorSessionQueryIsBeacon:
return opforengine.Bool(target.beacon != nil), nil
case opforengine.AggressorSessionQueryIsSSH:
return opforengine.Bool(false), nil
default:
return opforengine.Null(), fmt.Errorf("opfor: unsupported session status query %q", query.Name)
}
}
func targetIntegrity(target resolvedTarget) string {
if target.session != nil {
return target.session.Integrity
}
if target.beacon != nil {
return target.beacon.Integrity
}
return ""
}
func (manager *Manager) allTargets(ctx context.Context) ([]resolvedTarget, error) {
rpc, err := manager.rpc()
if err != nil {
return nil, err
}
sessions, err := rpc.GetSessions(ctx, &commonpb.Empty{})
if err != nil {
return nil, fmt.Errorf("opfor: list sessions: %w", err)
}
beacons, err := rpc.GetBeacons(ctx, &commonpb.Empty{})
if err != nil {
return nil, fmt.Errorf("opfor: list beacons: %w", err)
}
targets := make([]resolvedTarget, 0, len(sessions.GetSessions())+len(beacons.GetBeacons()))
for _, session := range sessions.GetSessions() {
if session != nil {
targets = append(targets, resolvedTarget{session: session})
}
}
for _, beacon := range beacons.GetBeacons() {
if beacon != nil {
targets = append(targets, resolvedTarget{beacon: beacon})
}
}
return targets, nil
}
func beaconArchitecture(arch string) (string, error) {
switch strings.ToLower(arch) {
case "amd64", "x64", "x86_64":
return "x64", nil
case "386", "x86", "i386":
return "x86", nil
default:
return "", fmt.Errorf("opfor: unsupported BOF target architecture %q", arch)
}
}
func targetMetadata(target resolvedTarget) *opforengine.Hash {
metadata := opforengine.NewOrderedHash()
set := func(key string, value opforengine.Value) { metadata.Set(key, value) }
if target.session != nil {
appendTargetMetadata(set, target.session.ID, target.session.Name, target.session.Hostname,
target.session.Username, target.session.OS, target.session.Arch, target.session.Transport,
target.session.RemoteAddress, target.session.Filename, target.session.Version,
target.session.Integrity, target.session.PID, !target.session.IsDead, false)
} else if target.beacon != nil {
appendTargetMetadata(set, target.beacon.ID, target.beacon.Name, target.beacon.Hostname,
target.beacon.Username, target.beacon.OS, target.beacon.Arch, target.beacon.Transport,
target.beacon.RemoteAddress, target.beacon.Filename, target.beacon.Version,
target.beacon.Integrity, target.beacon.PID, !target.beacon.IsDead, true)
}
return metadata
}
func appendTargetMetadata(
set func(string, opforengine.Value),
id, name, hostname, username, osName, arch, transport, remoteAddress, filename, version, integrity string,
pid int32,
active, beacon bool,
) {
architecture, err := beaconArchitecture(arch)
if err != nil {
architecture = strings.ToLower(arch)
}
set("id", opforengine.String(id))
set("name", opforengine.String(name))
set("computer", opforengine.String(hostname))
set("host", opforengine.String(hostname))
set("user", opforengine.String(username))
set("os", opforengine.String(osName))
set("arch", opforengine.String(architecture))
set("transport", opforengine.String(transport))
set("external", opforengine.String(remoteAddress))
set("process", opforengine.String(filename))
set("version", opforengine.String(version))
set("integrity", opforengine.String(integrity))
set("pid", opforengine.Int(pid))
set("active", opforengine.Bool(active))
set("beacon", opforengine.Bool(beacon))
}
-11
View File
@@ -1,11 +0,0 @@
# SliverArmory CNA corpus
These unmodified CNA scripts are pinned regression fixtures for the OPFOR client integration. The tests pair them with inert object bytes and verify script loading, command registration, resource resolution, BOF argument packing, and Sliver dispatch; they do not execute the embedded BOF code locally.
| Fixture | Upstream revision | License |
| --- | --- | --- |
| `firefoxdump/firefoxdump.cna` | [`sliverarmory/firefoxdump@19bec6f`](https://github.com/sliverarmory/firefoxdump/tree/19bec6fb2def510b299430955fb2be79c1f51820) | GPL-3.0-only; see the Sliver repository root `LICENSE` |
| `bof_collection/cat.cna` | [`sliverarmory/bof_collection@d937d25`](https://github.com/sliverarmory/bof_collection/tree/d937d25a164f40192e289b67e9c550d2bd4c369a) | Apache-2.0; copied `LICENSE` and `NOTICE` are retained here |
| `operatorskit/finddotnet.cna` and `findsysmon.cna` | [`sliverarmory/OperatorsKit@66368f4`](https://github.com/sliverarmory/OperatorsKit/tree/66368f4738528d26cc1ccc6d9a3c93d44d63edc1) | MIT; copied `LICENSE` is retained here |
The object files are intentionally not vendored. Live BOF execution belongs in a safety-gated Windows integration environment, while these fixtures keep the client test suite deterministic and platform independent.
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,26 +0,0 @@
bof_collection
Copyright 2026 Pim Beune
This product includes a modified copy of beacon.h derived from the
Cobalt-Strike/bof_template project, which is licensed under the Apache License,
Version 2.0. The local copy has been updated from the upstream template and is
distributed under the same license.
Armory packaging and integration changes were made by Sliver Armory
contributors.
Research and reference acknowledgements retained from the source project:
- Outflank: Remote Registry Protocol information-disclosure techniques
(https://www.youtube.com/watch?v=MxDq552Di3Y) and remote named-pipe
enumeration research
(https://outflank.nl/blog/2023/10/19/listing-remote-named-pipes/).
- Julian Peña: CVE-2019-16098 exploitation write-up referenced by the driver
proof of concept
(https://julian-pena.com/2025-09-29-exploiting-drivers-1/).
- Eric Esquivel: ntoskrnl function-offset technique acknowledged in the
CVE-2019-16098 source.
These research acknowledgements identify referenced techniques and materials;
they do not assert that the named researchers authored this collection's code
or that they endorse this package.
@@ -1,32 +0,0 @@
# author: Pim Beune
beacon_command_register(
"cat", "Prints content of a file",
"INFO: Prints content of a file.\n\n" .
"FEATURES: UTF-8 and UTF-16 (LE/BE) compatible, with or without BOM\n" .
" Refuses files larger than 128 KB\n" .
" Sanitizes output: strips control characters and invalid\n" .
" byte sequences (replaced with '?'), keeps tabs/newlines\n" .
" Paths do not need to be quoted or backslash-escaped\n\n" .
"EXAMPLES: cat C:\\Users\\User\\Desktop\\flag.txt\n" .
" cat C:\\Program Files\\Something\\notes.txt\n" .
" cat \\\\server\\share\\folder\\file.txt");
alias cat {
$bid = $1;
$file = join(" ", sublist(@_, 1));
if ($file eq "") {
berror($bid, "Please specify a file path.");
return;
}
# Read in the right BOF file
$handle = openf(script_resource("dist/cat." . barch($bid) . ".o"));
$data = readb($handle, -1);
closef($handle);
# Pack our arguments
$args = bof_pack($bid, "z", $file);
beacon_inline_execute($bid, $data, "go", $args);
}
@@ -1,52 +0,0 @@
# Firefox Dump BOF - Extract Firefox passwords and cookies
# Standalone version for Cobalt Strike
beacon_command_register(
"firefoxdump",
"Extract Firefox secrets (cookies, passwords) from Firefox profile.",
"INFO:\n" .
"Extracts browser secrets from Firefox browser.\n" .
"This BOF reads Firefox profile files (key4.db, logins.json, cookies.sqlite).\n\n" .
"SUPPORTED DATA:\n" .
" - Saved passwords (from logins.json - decrypted using NSS)\n" .
" - Cookies (from cookies.sqlite)\n\n" .
"USAGE:\n" .
" firefoxdump - Extract passwords only (default)\n" .
" firefoxdump /all - Extract passwords and cookies\n" .
" firefoxdump /cookies - Extract cookies only\n\n" .
"OUTPUT:\n" .
"Displays extracted passwords (cleartext) and/or cookies.\n" .
"Passwords are decrypted using NSS libraries from Firefox installation.\n\n" .
"NOTE:\n" .
"Requires access to the user's Firefox profile directory.\n" .
"Works best when run from the same user context as Firefox.\n" .
"Requires Firefox to be installed (for NSS libraries).\n"
);
alias firefoxdump {
local('$bid $barch $handle $bof $option $data');
$bid = $1;
$barch = barch($bid);
# Extract option from command line
if (size(@_) > 1) {
$option = @_[1];
} else {
$option = "";
}
# Read the BOF file
$handle = openf(script_resource("bin/firefoxdump." . $barch . ".o"));
$bof = readb($handle, -1);
closef($handle);
if (strlen($bof) == 0) {
berror($bid, "Could not read firefoxdump BOF file");
return;
}
# Pack option for BOF
$data = bof_pack($bid, "z", $option);
btask($bid, "Extracting Firefox secrets");
beacon_inline_execute($bid, $bof, "go", $data);
}
@@ -1,22 +0,0 @@
# author REDMED-X
beacon_command_register(
"finddotnet", "Find processes that most likely have .NET loaded.",
"INFO:\nFind processes that most likely have .NET loaded by searching for the section name: \BaseNamedObjects\Cor_Private_IPCBlock(_v4)_<ProcessId>\n\n" .
"USAGE:\nfinddotnet\n\n");
alias finddotnet {
$bid = $1;
# Read in the right BOF file
$handle = openf(script_resource("finddotnet.o"));
$data = readb($handle, -1);
closef($handle);
blog($bid, "Tasked to search for processes that have .NET loaded..");
beacon_inline_execute($bid, $data, "go", $null);
}
@@ -1,32 +0,0 @@
# author REDMED-X
beacon_command_register(
"findsysmon", "Verify if Sysmon is running.\n",
"INFO:\nVerify if Sysmon is running. This can be done by checking the registry or by enumerating Minifilter drivers and search for one that is associated with Sysmon.\n\nOPTIONS:\n[reg]: search the registry to check if Sysmon is present on the system and return the Sysmon service PID if active.\n[driver]: list all the Minifilter drivers on the system to check manually (requires elevated privileges).\n\n" .
"USAGE:\nfindsysmon <reg | driver>\n\n");
alias findsysmon {
$bid = $1;
$action = $2;
if ($action eq "reg" || $action eq "driver") {
}
else {
berror($bid, "Please specify one of the following enumeration options: reg | driver\n");
return;
}
# Read in the right BOF file
$handle = openf(script_resource("findsysmon.o"));
$data = readb($handle, -1);
closef($handle);
# Pack our arguments
$arg_data = bof_pack($bid, "z", $action);
blog($bid, "Tasked to find Sysmon..");
beacon_inline_execute($bid, $data, "go", $arg_data);
}
-112
View File
@@ -1,112 +0,0 @@
//go:build client
package opfor
import (
"context"
"fmt"
"strings"
"github.com/bishopfox/sliver/client/forms"
opforengine "github.com/sliverarmory/opfor"
)
// promptUI is deliberately small and injectable. The production adapter uses
// Sliver's existing Bubble Tea/Huh forms; tests can exercise callback behavior
// without taking over the terminal.
type promptUI interface {
Confirm(title string, defaultValue bool) (bool, error)
Input(title, defaultValue string) (string, error)
}
type formsPromptUI struct{}
func (formsPromptUI) Confirm(title string, defaultValue bool) (bool, error) {
value := defaultValue
err := forms.Confirm(title, &value)
return value, err
}
func (formsPromptUI) Input(title, defaultValue string) (string, error) {
value := defaultValue
err := forms.Input(title, &value)
return value, err
}
func (manager *Manager) presentPrompt(
ctx context.Context,
presentation opforengine.AggressorPromptPresentation,
responder opforengine.AggressorPromptResponder,
) error {
if responder == nil {
return errorsPromptResponderNil()
}
if err := ctx.Err(); err != nil {
return err
}
switch presentation.Kind {
case opforengine.AggressorPromptConfirm:
title := joinPromptTitle(presentation.Title, presentation.Text)
answer, err := manager.confirmPrompt(title, false)
if err != nil {
return err
}
if !answer {
return responder.Dismiss()
}
_, err = responder.Accept(ctx)
return err
case opforengine.AggressorPromptText,
opforengine.AggressorPromptDirectoryOpen,
opforengine.AggressorPromptFileOpen,
opforengine.AggressorPromptFileSave:
title := presentation.Text
if title == "" {
title = presentation.Title
}
if title == "" {
title = presentation.Name
}
defaultValue := ""
if presentation.HasDefault {
defaultValue = presentation.Default.String()
}
answer, err := manager.inputPrompt(title, defaultValue)
if err != nil {
return err
}
_, err = responder.Accept(ctx, opforengine.String(answer))
return err
default:
return fmt.Errorf("opfor: unsupported prompt kind %q", presentation.Kind)
}
}
func (manager *Manager) confirmPrompt(title string, defaultValue bool) (bool, error) {
manager.promptMu.Lock()
defer manager.promptMu.Unlock()
return manager.ui.Confirm(title, defaultValue)
}
func (manager *Manager) inputPrompt(title, defaultValue string) (string, error) {
manager.promptMu.Lock()
defer manager.promptMu.Unlock()
return manager.ui.Input(title, defaultValue)
}
func joinPromptTitle(parts ...string) string {
nonempty := make([]string, 0, len(parts))
for _, part := range parts {
if part = strings.TrimSpace(part); part != "" {
nonempty = append(nonempty, part)
}
}
return strings.Join(nonempty, ": ")
}
func errorsPromptResponderNil() error {
return fmt.Errorf("opfor: prompt responder is nil")
}
+1 -1
View File
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
pivotsCmd.AddCommand(pivotDetailsCmd)
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
})
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
(*comp)["id"] = PivotIDCompleter(con)
-1
View File
@@ -39,7 +39,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, addCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote target host:port (e.g., 10.0.0.1:445)")
f.StringP("bind", "b", "127.0.0.1:8080", "bind port forward to interface")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(addCmd, func(comp *carapace.ActionMap) {
(*comp)["bind"] = completers.ClientInterfacesCompleter()
+1 -11
View File
@@ -68,23 +68,13 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
bindAddr = fmt.Sprintf("127.0.0.1:%s", bindAddr)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
var keepAlivePeriod time.Duration
if keepAlive > 0 {
keepAlivePeriod = time.Duration(keepAlive) * time.Second
} else if keepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
tcpProxy := &tcpproxy.Proxy{}
channelProxy := &core.ChannelProxy{
Rpc: con.Rpc,
Session: session,
RemoteAddr: remoteAddr,
BindAddr: bindAddr,
KeepAlivePeriod: keepAlivePeriod,
KeepAlivePeriod: 60 * time.Second,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
+1 -1
View File
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GetSystemCmd(cmd, con, args)
},
GroupID: consts.PrivilegesHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
-8
View File
@@ -37,10 +37,6 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if session == nil && beacon == nil {
return
}
if session == nil {
con.PrintErrorf("Command is only supported for sessions.\n")
return
}
targetOS := getOS(session, beacon)
if targetOS != "windows" {
con.PrintErrorf("Command only supported on Windows.\n")
@@ -49,10 +45,6 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
process, _ := cmd.Flags().GetString("process")
config := con.GetActiveSessionConfig()
if config == nil {
con.PrintErrorf("Failed to derive active session config.\n")
return
}
/* If the HTTP C2 Config name is not defined, then put in the default value
This will have no effect on implants that do not use HTTP C2
+1 -1
View File
@@ -52,7 +52,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("loot-name", "N", "", "name to assign when adding the memory dump to the loot store (optional)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
+2 -2
View File
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
timeout, _ := cmd.Flags().GetInt64("timeout")
timeout, _ := cmd.Flags().GetInt32("timeout")
if timeout < 1 {
con.PrintErrorf("Invalid timeout argument\n")
@@ -68,7 +68,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
dump, err := con.Rpc.ProcessDump(context.Background(), &sliverpb.ProcessDumpReq{
Request: con.ActiveTarget.Request(cmd),
Pid: int32(pid),
Timeout: int32(timeout),
Timeout: timeout,
})
ctrl <- true
<-ctrl
+4 -13
View File
@@ -24,7 +24,6 @@ import (
"time"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/spf13/cobra"
@@ -95,24 +94,16 @@ func ReconfigCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if activeC2 != "" {
currentURI, err := url.Parse(activeC2)
if err == nil {
// must be the same protocol
if newURI.Scheme != currentURI.Scheme {
confirm := false
con.PrintWarnf("Switching protocol from %s to %s. This will only work if the protocol was included in the compiled implant. If it was NOT included, YOU WILL LOSE THE BEACON CONNECTION PERMANENTLY.\n",
con.PrintErrorf("Cannot switch protocol from %s to %s (protocol must match compiled implant)\n",
currentURI.Scheme, newURI.Scheme)
_ = forms.Confirm("Do you want to continue?", &confirm)
if !confirm {
return
}
return
}
// Warning: Switching to a different host typically fails because the implant and server must share the same crypto keys.
// for now only support the same Host/C2 Server (becuase the implant crypto keys problem)
if newURI.Hostname() != currentURI.Hostname() {
con.PrintWarnf("Switching to a different host (%s -> %s). This only works if both servers share the same crypto keys.\n",
currentURI.Hostname(), newURI.Hostname())
confirm := false
_ = forms.Confirm("Do you want to continue?", &confirm)
if !confirm {
return
}
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("registry", true, registryCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
registryReadCmd := &cobra.Command{
-1
View File
@@ -39,7 +39,6 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, rportfwdAddCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote address <ip>:<port> connection is forwarded to")
f.StringP("bind", "b", "", "bind address <ip>:<port> for implants to listen on")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(rportfwdAddCmd, func(comp *carapace.ActionMap) {
(*comp)["remote"] = completers.ClientInterfacesCompleter()
-3
View File
@@ -53,13 +53,10 @@ func StartRportFwdListenerCmd(cmd *cobra.Command, con *console.SliverClient, arg
if portNumberOnlyRegexp.MatchString(forwardAddress) {
forwardAddress = fmt.Sprintf("127.0.0.1:%s", forwardAddress)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
rportfwdListener, err := con.Rpc.StartRportFwdListener(context.Background(), &sliverpb.RportFwdStartListenerReq{
Request: con.ActiveTarget.Request(cmd),
BindAddress: bindAddress,
ForwardAddress: forwardAddress,
KeepAlive: keepAlive,
})
if err != nil {
con.PrintWarnf("%s\n", err)
-6
View File
@@ -22,7 +22,6 @@ import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/command/ai"
"github.com/bishopfox/sliver/client/command/aka"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/armory"
@@ -33,7 +32,6 @@ import (
"github.com/bishopfox/sliver/client/command/clean"
"github.com/bishopfox/sliver/client/command/crack"
"github.com/bishopfox/sliver/client/command/creds"
docscmd "github.com/bishopfox/sliver/client/command/docs"
"github.com/bishopfox/sliver/client/command/exit"
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/command/generate"
@@ -45,7 +43,6 @@ import (
"github.com/bishopfox/sliver/client/command/mcp"
"github.com/bishopfox/sliver/client/command/monitor"
"github.com/bishopfox/sliver/client/command/operators"
"github.com/bishopfox/sliver/client/command/opfor"
"github.com/bishopfox/sliver/client/command/reaction"
"github.com/bishopfox/sliver/client/command/serverctx"
"github.com/bishopfox/sliver/client/command/sessions"
@@ -101,9 +98,6 @@ func ServerCommands(con *client.SliverClient, serverCmds func() []*cobra.Command
// Core
bind(consts.GenericHelpGroup,
exit.Command,
ai.ServerCommands,
opfor.ServerCommands,
docscmd.ServerCommands,
serverctx.Commands,
licenses.Commands,
settings.Commands,
+11 -11
View File
@@ -13,15 +13,15 @@ import (
"sync"
"time"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/huh/v2"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
clienttheme "github.com/bishopfox/sliver/client/theme"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
"google.golang.org/grpc/connectivity"
)
@@ -158,7 +158,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
}
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: inst.ConfigKey, Config: inst.Config}); err != nil {
_ = transport.CloseGRPCConnection(conn)
_ = conn.Close()
con.PrintErrorf("Switch failed: %s\n", err)
return
}
@@ -180,7 +180,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
}
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: result.ConfigKey, Config: cfg}); err != nil {
_ = transport.CloseGRPCConnection(conn)
_ = conn.Close()
con.PrintErrorf("Switch failed: %s\n", err)
return
}
@@ -347,6 +347,7 @@ type switchTUIModel struct {
result switchResult
width int
height int
theme *huh.Theme
styles switchStyles
activeID string
counts map[string]int
@@ -366,7 +367,7 @@ type switchStyles struct {
func runSwitchTUI(instances []*consoleInstance, activeID string, configKeys []string, counts map[string]int) (switchResult, error) {
m := newSwitchTUIModel(instances, activeID, configKeys, counts)
prog := tea.NewProgram(m)
prog := tea.NewProgram(m, tea.WithAltScreen())
finalModel, err := prog.Run()
if err != nil {
return switchResult{}, err
@@ -464,6 +465,7 @@ func newSwitchTUIModel(instances []*consoleInstance, activeID string, configKeys
tab: tabExisting,
existing: existingForm,
newConn: newForm,
theme: huhTheme,
styles: styles,
activeID: activeID,
counts: counts,
@@ -489,7 +491,7 @@ func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.newConn = modelB.(*huh.Form)
return m, tea.Batch(cmdA, cmdB)
case tea.KeyPressMsg:
case tea.KeyMsg:
switch msg.String() {
case "tab":
if m.tab == tabExisting {
@@ -546,7 +548,7 @@ func (m switchTUIModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m switchTUIModel) View() tea.View {
func (m switchTUIModel) View() string {
header := m.renderHeader()
body := ""
@@ -560,9 +562,7 @@ func (m switchTUIModel) View() tea.View {
footer := m.renderFooter()
content := lipgloss.JoinVertical(lipgloss.Left, header, body, footer)
view := tea.NewView(m.styles.frame.Render(content))
view.AltScreen = true
return view
return m.styles.frame.Render(content)
}
func (m switchTUIModel) renderHeader() string {
+1 -1
View File
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("sessions", false, sessionsCmd, func(f *pflag.FlagSet) {
f.StringP("interact", "i", "", "interact with a session")
+1 -6
View File
@@ -50,12 +50,7 @@ func IsUserAnAdultWithPrompt(con *console.SliverClient, prompt string) bool {
return true
}
confirm := false
if err := forms.Confirm(prompt, &confirm); err != nil {
// The interactive form is known to fail on some terminals (#2133);
// surface the error instead of silently treating it as a rejection.
con.PrintErrorf("Confirmation prompt failed: %s\n", err)
return false
}
_ = forms.Confirm(prompt, &confirm)
return confirm
}
-32
View File
@@ -1,32 +0,0 @@
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
package shell
import (
"io"
"os"
)
// This fallback preserves the legacy blocking behavior on unsupported client
// platforms. Unix and Windows clients use cancellable implementations.
type cancellableShellInput struct {
reader io.Reader
}
func newCancellableShellInput(_ *os.File, reader io.Reader) (*cancellableShellInput, error) {
return &cancellableShellInput{reader: reader}, nil
}
func (s *cancellableShellInput) Read(data []byte, done <-chan struct{}) (int, bool, error) {
select {
case <-done:
return 0, true, nil
default:
}
n, err := s.reader.Read(data)
return n, false, err
}
func (s *cancellableShellInput) Close() error {
return nil
}
-129
View File
@@ -1,129 +0,0 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
package shell
import (
"errors"
"io"
"os"
"testing"
"time"
)
func closeShellInputTestFile(t *testing.T, file *os.File) {
t.Helper()
if err := file.Close(); err != nil {
t.Errorf("close %s: %v", file.Name(), err)
}
}
func TestCancellableShellInputWakesOnTunnelClose(t *testing.T) {
readFile, writeFile, err := os.Pipe()
if err != nil {
t.Fatalf("create stdin pipe: %v", err)
}
t.Cleanup(func() { closeShellInputTestFile(t, readFile) })
t.Cleanup(func() { closeShellInputTestFile(t, writeFile) })
input, err := newCancellableShellInput(readFile, newFilterReader(readFile))
if err != nil {
t.Fatalf("prepare cancellable stdin: %v", err)
}
t.Cleanup(func() {
if err := input.Close(); err != nil {
t.Errorf("close cancellable stdin: %v", err)
}
})
done := make(chan struct{})
type readResult struct {
n int
err error
tunnelClosed bool
}
readDone := make(chan readResult, 1)
go func() {
n, tunnelClosed, err := input.Read(make([]byte, 16), done)
readDone <- readResult{n: n, err: err, tunnelClosed: tunnelClosed}
}()
select {
case result := <-readDone:
t.Fatalf("stdin Read returned before data or tunnel close: %+v", result)
case <-time.After(50 * time.Millisecond):
}
close(done)
select {
case result := <-readDone:
if result.n != 0 || result.err != nil || !result.tunnelClosed {
t.Fatalf("stdin Read after tunnel close = %+v, want n=0 err=nil tunnelClosed=true", result)
}
case <-time.After(time.Second):
t.Fatal("stdin Read did not wake when the tunnel closed")
}
}
func TestCancellableShellInputReportsPipeEOF(t *testing.T) {
readFile, writeFile, err := os.Pipe()
if err != nil {
t.Fatalf("create stdin pipe: %v", err)
}
t.Cleanup(func() { closeShellInputTestFile(t, readFile) })
if err := writeFile.Close(); err != nil {
t.Fatalf("close stdin pipe writer: %v", err)
}
input, err := newCancellableShellInput(readFile, newFilterReader(readFile))
if err != nil {
t.Fatalf("prepare cancellable stdin: %v", err)
}
t.Cleanup(func() {
if err := input.Close(); err != nil {
t.Errorf("close cancellable stdin: %v", err)
}
})
n, tunnelClosed, err := input.Read(make([]byte, 16), make(chan struct{}))
if n != 0 || tunnelClosed || !errors.Is(err, io.EOF) {
t.Fatalf("stdin Read after pipe EOF = n=%d tunnelClosed=%v err=%v, want n=0 tunnelClosed=false err=EOF", n, tunnelClosed, err)
}
}
func TestCancellableShellInputReadsRegularFile(t *testing.T) {
file, err := os.CreateTemp(t.TempDir(), "shell-stdin-*")
if err != nil {
t.Fatalf("create stdin file: %v", err)
}
t.Cleanup(func() { closeShellInputTestFile(t, file) })
const inputData = "whoami\n"
if _, err := file.WriteString(inputData); err != nil {
t.Fatalf("write stdin file: %v", err)
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
t.Fatalf("rewind stdin file: %v", err)
}
input, err := newCancellableShellInput(file, newFilterReader(file))
if err != nil {
t.Fatalf("prepare cancellable stdin: %v", err)
}
t.Cleanup(func() {
if err := input.Close(); err != nil {
t.Errorf("close cancellable stdin: %v", err)
}
})
data := make([]byte, len(inputData))
n, tunnelClosed, err := input.Read(data, make(chan struct{}))
if err != nil {
t.Fatalf("read stdin file: %v", err)
}
if tunnelClosed {
t.Fatal("stdin file read reported a closed tunnel")
}
if got := string(data[:n]); got != inputData {
t.Fatalf("stdin file data = %q, want %q", got, inputData)
}
}
Loaded 100 of 3231 files, more files were not shown because too many files have changed in this diff. Show more