Compare commits

..
5 Commits
13177 changed files with 2656042 additions and 7059478 deletions

No files matched your search

-7
View File
@@ -1,9 +1,4 @@
# go-assets.sh downloads
/server/assets/fs/**.zip
/server/assets/fs/**/garble*
/server/assets/fs/**/zig.tar.xz
/server/assets/fs/**/zig.zip
/server/assets/fs/src.zip
/*.sdk.tar.xz
@@ -24,5 +19,3 @@
/SECURITY.md
/README.md
/CONTRIBUTING.md
.downloaded_assets
+1 -1
View File
@@ -1 +1 @@
* @BishopFox/bf-red-team
* @BishopFox/red-team
+115 -88
View File
@@ -2,109 +2,136 @@ name: Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
branches: [master]
jobs:
tag-check:
name: Check Release Tag
servers-build:
name: Release Build Server
if: startsWith( github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Check Out Code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate Release 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
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'
runs-on: ubuntu-latest
environment: release
timeout-minutes: 45
steps:
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "^1.21"
- name: OS Packages
run: |
sudo apt-get update --fix-missing && sudo apt-get -y install \
git build-essential zlib1g zlib1g-dev wget zip unzip
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make
run: make servers
- name: Server Artifacts
uses: actions/upload-artifact@v3
with:
fetch-depth: 0
name: servers
path: ./sliver-server_*
- name: Go 1.27.1
uses: actions/setup-go@v6
clients-build:
name: Release Build Client
if: startsWith( github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "1.27.1"
go-version: "^1.21"
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
- name: Check Out Code
uses: actions/checkout@v4
- name: Build Clients
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make
run: make clients
- name: Client Artifacts
uses: actions/upload-artifact@v3
with:
name: clients
path: ./sliver-client_*
tagged-release:
needs: [servers-build, clients-build]
name: "Tagged Release"
if: startsWith( github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- run: mkdir -p ./builds
- uses: actions/download-artifact@v3
with:
path: ./builds
- name: Extract Artifacts
run: |
make clients
mkdir -p artifacts
mv sliver-client_* artifacts/
mkdir -p ./artifacts
cp ./builds/clients/* ./artifacts
cp ./builds/servers/* ./artifacts
- name: Build Servers
- name: GPG Key(s)
run: |
make servers
mv sliver-server_* artifacts/
cat <(echo -e "${{ secrets.SLIVER_GPG }}") | gpg --batch --import
gpg --list-secret-keys --keyid-format LONG
- name: Sign Artifacts
env:
MINISIGN_KEY: ${{ secrets.MINISIGN_SLIVER_RELEASE_KEY }}
# Linux
- name: Linux 386 (Client Only)
run: |
echo "$MINISIGN_KEY" | base64 -d > minisign.key
echo "minisign.key bytes: $(wc -c < minisign.key | tr -d ' ')"
echo "minisign.key sha256: $(sha256sum minisign.key | awk '{print $1}')"
chmod 600 minisign.key
for file in artifacts/sliver-*; do
[ -f "$file" ] || continue
minisign -S -s minisign.key -m "$file" -x "$file.minisig" -W
done
rm -f minisign.key
- name: Create Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-386.sig --detach-sig ./artifacts/sliver-client_linux-386
- name: Linux AMD64
run: |
tag="${{ needs.tag-check.outputs.tag }}"
if gh release view "$tag" >/dev/null 2>&1; then
echo "Release $tag already exists."
exit 0
fi
gh release create "$tag" --generate-notes
gpg --default-key 4449039C --output ./artifacts/sliver-server_linux-amd64.sig --detach-sig ./artifacts/sliver-server_linux-amd64
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-amd64.sig --detach-sig ./artifacts/sliver-client_linux-amd64
- name: Linux ARM64
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-server_linux-arm64.sig --detach-sig ./artifacts/sliver-server_linux-arm64
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-arm64.sig --detach-sig ./artifacts/sliver-client_linux-arm64
- name: Upload Artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ needs.tag-check.outputs.tag }}" artifacts/* --clobber
# Windows
- name: Windows 386 (Client Only)
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-386.sig --detach-sig ./artifacts/sliver-client_windows-386.exe
- name: Windows AMD64
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-server_windows-amd64.sig --detach-sig ./artifacts/sliver-server_windows-amd64.exe
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-amd64.sig --detach-sig ./artifacts/sliver-client_windows-amd64.exe
- name: Windows ARM64 (Client Only)
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-arm64.sig --detach-sig ./artifacts/sliver-client_windows-arm64.exe
# MacOS
- name: MacOS AMD64
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-server_macos-amd64.sig --detach-sig ./artifacts/sliver-server_macos-amd64
gpg --default-key 4449039C --output ./artifacts/sliver-client_macos-amd64.sig --detach-sig ./artifacts/sliver-client_macos-amd64
- name: Package MacOS ARM64
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-server_macos-arm64.sig --detach-sig ./artifacts/sliver-server_macos-arm64
gpg --default-key 4449039C --output ./artifacts/sliver-client_macos-arm64.sig --detach-sig ./artifacts/sliver-client_macos-arm64
# FreeBSD
- name: FreeBSD AMD64 (Client Only)
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-client_freebsd-amd64.sig --detach-sig ./artifacts/sliver-client_freebsd-amd64.exe
- name: FreeBSD ARM64 (Client Only)
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-client_freebsd-arm64.sig --detach-sig ./artifacts/sliver-client_freebsd-arm64.exe
- name: "Publish Release"
uses: "bishopfox/action-gh-release@v1"
with:
files: |
./artifacts/*
+7 -7
View File
@@ -16,13 +16,13 @@ jobs:
steps:
# Setup go environment
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "1.27.1"
go-version: "^1.21"
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
# Must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head of the pull request.
@@ -36,10 +36,10 @@ jobs:
git build-essential zlib1g zlib1g-dev wget zip unzip
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
languages: go
@@ -48,6 +48,6 @@ jobs:
run: make
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v2
env:
VERSION: ${{github.sha}}
-551
View File
@@ -1,551 +0,0 @@
name: Comprehensive e2e Tests
on:
workflow_dispatch:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: true
default: comprehensive
type: choice
options:
- comprehensive
- rportfwd
workflow_call:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: false
default: comprehensive
type: string
permissions:
contents: read
env:
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
concurrency:
group: comprehensive-e2e-${{ github.ref }}
cancel-in-progress: false
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 }}
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
with:
script: |
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
return;
}
core.info(`Validated E2E test scope ${testScope}`);
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
if (isReleaseTag) {
core.info(`Authorized release test invocation for ${context.ref}`);
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");
}
rportfwd-security-regression:
name: Reverse-port-forward malicious protocol regression
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Test malicious destination poisoning and cross-session isolation
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: >-
go test -buildvcs=false -mod=vendor
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
./server/c2
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
-count=1 -v
darwin:
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
needs: authorize
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 selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os darwin \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os darwin
-target-arch ${{ matrix.goarch }}
-transports mtls,wg,http
- 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 selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os linux \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os linux
-target-arch ${{ matrix.goarch }}
-transports mtls,wg,http
- 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
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: 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 selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e.exe \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server.exe \
-server-arch ${{ matrix.server_arch }} \
-target-os windows \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os windows
-target-arch ${{ matrix.goarch }}
-transports mtls,wg,http
- 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-rportfwd:
name: Aggregate reverse-port-forward E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download target coverage
id: download
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: comprehensive-e2e-target-*
path: rportfwd-coverage-input
- name: Prepare aggregate artifact
if: ${{ always() }}
run: |
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
- name: Aggregate coverage and enforce all 162 required cells
id: aggregate
if: ${{ always() }}
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input rportfwd-coverage-input
-output rportfwd-coverage-summary
- name: Publish reverse-port-forward coverage table
if: ${{ always() }}
run: |
report="rportfwd-coverage-summary/rportfwd-coverage.md"
if [[ -s "${report}" ]]; then
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
else
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
fi
- name: Upload aggregate reverse-port-forward coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-rportfwd-coverage-summary
path: rportfwd-coverage-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
aggregate-coverage:
name: Aggregate comprehensive E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- windows
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
outputs:
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
runs-on: ubuntu-24.04
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 prerequisites or failed or not-run coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
reflektor:
name: Existing Reflektor test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/reflektor-tests.yml
shellcode:
name: Shellcode E2E test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/shellcode-tests.yml
+6 -6
View File
@@ -35,7 +35,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Detect package manager
id: detect-package-manager
@@ -56,17 +56,17 @@ jobs:
fi
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
node-version: "lts/*"
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v3
- name: Restore cache
uses: actions/cache@v5
uses: actions/cache@v3
with:
path: |
.next/cache
@@ -85,7 +85,7 @@ jobs:
run: ${{ steps.detect-package-manager.outputs.runner }} next build
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v1
with:
path: ${{ github.workspace }}/docs/sliver-docs/out
@@ -99,4 +99,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v2
-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.27.1
uses: actions/setup-go@v6
with:
go-version: "1.27.1"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.13.1
# Gate only findings introduced since the event base revision.
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
-164
View File
@@ -1,164 +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 }}
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-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 }}
-201
View File
@@ -1,201 +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: 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
+33 -100
View File
@@ -6,15 +6,11 @@ on:
paths:
- "go.mod"
- "go.sum"
- ".gitattributes"
- "client/command/opfor/testdata/corpus/**"
- ".github/workflows/unit-tests.yml"
- "Makefile"
- "**/*.go"
- "**/*.c"
- "**/*.h"
- "**/*.sh"
- "**/*.ps1"
types:
- opened
- reopened
@@ -22,108 +18,55 @@ 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
timeout-minutes: 90
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "1.27.1"
go-version: "^1.21"
- name: OS Packages
run: |
sudo apt-get update --fix-missing && sudo apt-get -y install \
git build-essential zlib1g zlib1g-dev wget zip unzip
git build-essential zlib1g zlib1g-dev wget zip unzip \
mingw-w64 binutils-mingw-w64 g++-mingw-w64 gcc-multilib
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make Linux (amd64)
run: make linux-amd64
- name: Test Linux
run: ./sliver-server unpack --force && ./go-tests.sh
- name: Make Windows (amd64)
run: make windows-amd64
- name: Make Linux (arm64)
run: make linux-arm64
- name: Make Linux (amd64)
run: make linux-amd64
- name: Determine go-tests flags
id: go-tests-flags
shell: bash
run: |
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
fi
- 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.27.1
uses: actions/setup-go@v6
with:
go-version: "1.27.1"
- 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
timeout-minutes: 90
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "1.27.1"
go-version: "^1.21"
- name: Mingw
run: brew install mingw-w64
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
@@ -131,37 +74,27 @@ jobs:
- name: Make MacOS (amd64)
run: make macos-amd64
- name: Test MacOS (amd64)
env:
SLIVER_CC_32: /usr/local/bin/i686-w64-mingw32-gcc
SLIVER_CC_64: /usr/local/bin/x86_64-w64-mingw32-gcc
run: ./sliver-server unpack --force && ./go-tests.sh
- name: Make MacOS (arm64)
run: make macos-arm64
- name: Determine go-tests flags
id: go-tests-flags
shell: bash
run: |
set -euo pipefail
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.sha }}"
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
echo "args=" >> "$GITHUB_OUTPUT"
else
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
fi
- name: Test MacOS
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
clients-build:
name: Clients Build
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Go 1.27.1
uses: actions/setup-go@v6
- name: Go 1.21
uses: actions/setup-go@v5
with:
go-version: "1.27.1"
go-version: "^1.21"
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
-12
View File
@@ -1,17 +1,7 @@
/crush
/examples
/.vscode
/.vscode/*
/.devcontainer
/.idea
/artifacts
/artifacts/**
*.rc
/Dockerfile.*
/*.sdk.tar.xz
@@ -55,8 +45,6 @@ sliver-server-windows.zip
.DS_Store
dist/
.gocache/
.gomodcache/
# Flag to show that go-assets.sh has been run
.downloaded_assets
-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
-18
View File
@@ -1,18 +0,0 @@
# Repository Guidelines
## Build, Test, and Development Commands
- `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.
- 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
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
- Do not import `server` packages from `client`.
## Testing Guidelines
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
- Add or update tests when behavior changes; some server packages run longer tests.
+15 -14
View File
@@ -5,12 +5,11 @@
#
# For unit testing:
# docker build --target test .
# docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .
#
# STAGE: base
## Compiles Sliver for use
FROM golang:latest AS base
FROM --platform=linux/amd64 golang:1.21.4 as base
### Base packages
RUN apt-get update --fix-missing && apt-get -y install \
@@ -21,33 +20,30 @@ RUN groupadd -g 999 sliver && useradd -r -u 999 -g sliver sliver
RUN mkdir -p /home/sliver/ && chown -R sliver:sliver /home/sliver
### Build sliver:
RUN mkdir -p /go/src/github.com/bishopfox/sliver
WORKDIR /go/src/github.com/bishopfox/sliver
ADD . /go/src/github.com/bishopfox/sliver/
RUN make
RUN make clean-all
RUN make
RUN cp -vv sliver-server /opt/sliver-server
# STAGE: test
## Run unit tests against the compiled instance
## Use `--target test` in the docker build command to run this stage
FROM base AS test
ARG GO_TESTS_FLAGS=""
ENV GO_TESTS_FLAGS="${GO_TESTS_FLAGS}"
FROM --platform=linux/amd64 base as test
RUN apt-get update --fix-missing \
&& apt-get -y upgrade \
&& apt-get -y install \
curl
curl gcc-multilib build-essential mingw-w64 binutils-mingw-w64 g++-mingw-w64
RUN /opt/sliver-server unpack --force
### Run unit tests
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh ${GO_TESTS_FLAGS}
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh
# STAGE: production
## Final dockerized form of Sliver
FROM debian:bookworm-slim AS production
FROM --platform=linux/amd64 debian:bookworm-slim as production
### Install production packages
RUN apt-get update --fix-missing \
@@ -60,7 +56,8 @@ RUN apt-get update --fix-missing \
postgresql-contrib postgresql-client libpq-dev \
curl libapr1 libaprutil1 libsvn1 \
libpcap-dev libsqlite3-dev libgmp3-dev \
nasm
mingw-w64 binutils-mingw-w64 g++-mingw-w64 \
nasm gcc-multilib
### Install MSF for stager generation
RUN curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > msfinstall \
@@ -95,11 +92,15 @@ ENTRYPOINT [ "/opt/sliver-server" ]
# STAGE: production-slim (about 1Gb smaller)
FROM debian:bookworm-slim AS production-slim
### Slim production image, i.e. without MSF and assoicated libraries
### Still include GCC and MinGW for cross-platform generation
FROM --platform=linux/amd64 debian:bookworm-slim as production-slim
### Install production packages
RUN apt-get update --fix-missing \
&& apt-get -y upgrade
&& apt-get -y upgrade \
&& apt-get -y install \
build-essential mingw-w64 binutils-mingw-w64 g++-mingw-w64 gcc-multilib
### Cleanup unneeded packages
RUN apt-get autoremove -y \
+167 -295
View File
@@ -2,330 +2,202 @@
# 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
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 27
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
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 zip date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Prerequisites
# Version Information
#
# 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_VERSION = $(shell $(GO) version)
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 = 21
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)
VERSION ?= $(shell git describe --abbrev=0)
COMPILED_AT = $(shell date +%s)
RELEASES_URL ?= https://api.github.com/repos/BishopFox/sliver/releases
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
VERSION_PKG = github.com/bishopfox/sliver/client/version
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
GIT_COMMIT = $(shell git rev-parse HEAD)
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(VERSION_PKG).Version=$(VERSION) \
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
-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 $(VERSION_PKG).Version=$(VERSION) \
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
-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), )
ENV += GOARCH=arm64
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(VERSION_PKG).Version=$(VERSION) \
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# >>> WINDOWS <<<
# Targets
#
else
.PHONY: default
default: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) 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:
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=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:
./go-assets.sh
touch ./.downloaded_assets
+28 -7
View File
@@ -1,16 +1,33 @@
# Sliver
Sliver is an open-source, cross-platform adversary emulation and red team framework designed for organizations of all sizes to perform authorized security testing.
Sliver is an open source cross-platform adversary emulation/red team framework, it can be used by organizations of all sizes to perform security testing. Sliver's implants support C2 over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS and are dynamically compiled with per-binary asymmetric encryption keys.
Sliver implants support command-and-control (C2) over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS. Implants are dynamically compiled with unique per-binary asymmetric encryption keys and support a wide range of post-exploitation capabilities across macOS, Windows, and Linux.
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).
Sliver includes support for native and shellcode payloads, userspace reflective loading, built-in shellcode encoding, and BOF/COFF execution on both amd64 and arm64 architectures for macOS, Windows, and Linux.
[![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)
The Sliver server and client run on macOS, Windows, and Linux. Implants officially support those same platforms and may also run on additional targets supported by the Go compiler, although those configurations are not regularly tested.
# v1.6.0 / `master`
[![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)
**NOTE:** You are looking the latest master branch of Sliver v1.6.0; new PRs should target this branch. However, this branch is NOT RECOMMENDED for production use yet. Please use release tagged versions for the best experience.
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
For PRs containing bug fixes specific to Sliver v1.5, please target the [`v1.5.x/master` branch](https://github.com/BishopFox/sliver/tree/v1.5.x/master).
### Features
- Dynamic code generation
- Compile-time obfuscation
- Multiplayer-mode
- Staged and Stageless payloads
- [Procedurally generated C2](https://sliver.sh/docs?name=HTTPS+C2) over HTTP(S)
- [DNS canary](https://sliver.sh/docs?name=DNS+C2) blue team detection
- [Secure C2](https://sliver.sh/docs?name=Transport+Encryption) over mTLS, WireGuard, HTTP(S), and DNS
- Fully scriptable using [JavaScript/TypeScript](https://github.com/moloch--/sliver-script) or [Python](https://github.com/moloch--/sliver-py)
- Windows process migration, process injection, user token manipulation, etc.
- Let's Encrypt integration
- In-memory .NET assembly execution
- COFF/BOF in-memory loader
- TCP and named pipe pivots
- Much more!
### Getting Started
@@ -22,12 +39,16 @@ Download the latest [release](https://github.com/BishopFox/sliver/releases) and
### Help!
Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions).
Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions). We also tend to hang out in the #golang Slack channel on the [Bloodhound Gang](https://bloodhoundgang.herokuapp.com/) server.
### Compile From Source
See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
### Feedback
Please take a moment and fill out [our survey](https://forms.gle/SwVsHFNh24ChG58C6)
### License - GPLv3
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
Executable
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
"""
Sliver Implant Framework
Copyright (C) 2019 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 os
import sys
import argparse
INFO = "\033[1m\033[36m[*]\033[0m "
WARN = "\033[1m\033[31m[!]\033[0m "
BOLD = "\033[1m"
NORM = "\033[0m"
def exec_cmd(cmd, ignore_status=False):
"""Executes a command with some sugar"""
print("\n" + INFO + " %s\n" % cmd)
status = os.system(cmd)
if status != 0 and not ignore_status:
print("\n" + WARN + "Command did not exit cleanly (%s) " % status)
if input("continue? [Y/n]: ").upper() != "Y":
sys.exit(status)
return status
def docker_rm_volumes():
exec_cmd("docker volume rm $(docker volume ls -q)", ignore_status=True)
def docker_rm_containers():
exec_cmd("docker rm $(docker ps -a -q)", ignore_status=True)
def docker_rm_images():
exec_cmd("docker rmi -f $(docker images -q)", ignore_status=True)
def docker_prune():
exec_cmd("docker image prune -f", ignore_status=True)
def docker_rm_all():
docker_rm_containers()
docker_rm_images()
docker_rm_volumes()
def build():
status = exec_cmd("docker build --target test .")
if status == 0:
print("\n\n" + INFO + "Build successful")
def main(args):
"""Execute the respective task(s) based on cli args"""
if args.rm_images:
docker_rm_images()
if args.rm_containers:
docker_rm_containers()
if args.rm_volumes:
docker_rm_volumes()
if args.rm_all:
docker_rm_all()
if not args.no_prune:
docker_prune()
if not args.no_build:
build()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="automates interactions with docker")
parser.add_argument(
"--no-build",
"-b",
help="do not run build",
dest="no_build",
action="store_true",
)
parser.add_argument(
"--no-prune",
"-np",
help="do not prune images",
dest="no_prune",
action="store_true",
)
parser.add_argument(
"--rm-all",
"-rma",
help="rm all containers, images, and volumes",
dest="rm_all",
action="store_true",
)
parser.add_argument(
"--rm-containers",
"-rmc",
help="rm all containers",
dest="rm_containers",
action="store_true",
)
parser.add_argument(
"--rm-images",
"-rmi",
help="rm all images",
dest="rm_images",
action="store_true",
)
parser.add_argument(
"--rm-volumes",
"-rmv",
help="rm dangling volumes only",
dest="rm_volumes",
action="store_true",
)
try:
main(parser.parse_args())
except KeyboardInterrupt:
print("\n\n" + WARN + "User stopped process")
+15 -23
View File
@@ -1,26 +1,18 @@
# client
Client
=======
## Overview
This directory contains the client implementation, pretty much all of this code is also used by the server console.
Main command-line client for interacting with and administering Sliver servers. Provides the Cobra entrypoint and aggregates console, command, and transport subsystems.
__Important:__ No code in this directory can import any code from `server/`.
## Go Files
- `main.go` – Entry point that initializes the CLI, processes flags, and launches the interactive console.
## Sub-packages
- `assets/` – Manages embedded client asset manifests such as operator profiles, settings, and armory catalogs. Handles serialization, bundling, and lookup of static client metadata.
- `cli/` – Defines the Cobra root command and CLI initialization for the Sliver client binary. Sets up persistent flags, environment bootstrapping, and command wiring.
- `command/` – Shared registration glue, scaffolding, and helpers for Sliver client console command groups. Hosts common utilities for binding Cobra commands to RPC-backed actions.
- `console/` – Renders the interactive console UI, logging, and terminal helpers for the client. Coordinates readline behavior, prompt styling, and message formatting.
- `constants/` – Centralizes constants shared across the client runtime and command packages. Captures strings, flag names, and feature toggles used throughout the CLI.
- `core/` – Maintains client application state, background workers, and RPC coordination. Orchestrates session tracking, event streams, and health checks.
- `credentials/` – Credential management utilities including sniffers and import helpers for operators. Provides parsers, storage helpers, and output formatting for recovered secrets.
- `licenses/` – Holds embedded license metadata bundled with the client distribution. Supplies license retrieval utilities used for attribution commands.
- `overlord/` – Supervises long-running client jobs, scheduling, and task orchestration. Tracks job lifecycle events and exposes status updates to the console.
- `packages/` – Deals with packaged extensions and module discovery for the client. Scans, validates, and caches extension manifests and assets.
- `spin/` – Lightweight terminal spinner utilities used for progress feedback. Wraps spinner styles and timing helpers shared across commands.
- `tcpproxy/` – Implements the local TCP proxy used for port forwarding from the client side. Manages listener lifecycles, connection pumps, and cleanup hooks.
- `transport/` – Client-side transports and RPC wiring to communicate with Sliver servers. Builds authenticated gRPC clients, TLS dialers, and reconnect logic.
- `version/` – Build-time version and semantic metadata for the client binary. Exposes git information, release channels, and helper formatters.
* `assets/` - Static asset files and management code (e.g. client configs)
* `cli/` - Command line interface implementation
* `command/` - Console command implementations
* `constants/` - Various shared constant values
* `core/` - Client state management
* `licenses/` - Open source licenses
* `spin/` - Console spinner library
* `tcpproxy/` - Local TCP proxy listener code
* `transport/` - Wires the client to the server
* `version/` - Version information
* `main.go` - Entrypoint
+2 -14
View File
@@ -1,15 +1,3 @@
# client/assets
Assets
======
## Overview
Manages embedded client asset manifests such as operator profiles, settings, and armory catalogs. Handles serialization, bundling, and lookup of static client metadata. Core logic addresses aliases, armories, C2 profiles, and extensions within the assets package.
## Go Files
- `aliases.go` – Provides helpers for discovering, installing, and reading alias manifests shipped with the client.
- `armories.go` – Loads configured armory definitions and persists operator customizations.
- `assets.go` – Central entry point that exposes asset directories, lazy initialization, and filesystem utilities.
- `c2profiles.go` – Embeds default HTTP C2 profile templates and exposes lookup utilities.
- `config.go` – Tracks client configuration paths and manages persistence of runtime options.
- `extensions.go` – Handles embedded extension manifests and supports lookup by command name.
- `settings.go` – Stores default console settings and manages serialization to disk.
+11 -50
View File
@@ -27,8 +27,7 @@ import (
)
const (
ArmoryConfigFileName = "armories.json"
DefaultArmoryName = "Default"
armoryConfigFileName = "armories.json"
)
var (
@@ -37,11 +36,9 @@ var (
// DefaultArmoryRepoURL - The default repo url for the armory
DefaultArmoryRepoURL string
DefaultArmoryConfig = &ArmoryConfig{
defaultArmoryConfig = &ArmoryConfig{
PublicKey: DefaultArmoryPublicKey,
RepoURL: DefaultArmoryRepoURL,
Name: DefaultArmoryName,
Enabled: true,
}
)
@@ -51,65 +48,29 @@ type ArmoryConfig struct {
RepoURL string `json:"repo_url"`
Authorization string `json:"authorization"`
AuthorizationCmd string `json:"authorization_cmd"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
}
func RefreshArmoryAuthorization(armories []*ArmoryConfig) {
for _, armoryConfig := range armories {
if armoryConfig.AuthorizationCmd != "" {
armoryConfig.Authorization = executeAuthorizationCmd(armoryConfig)
}
}
}
// GetArmoriesConfig - The parsed armory config file
func GetArmoriesConfig() []*ArmoryConfig {
armoryConfigPath := filepath.Join(GetRootAppDir(), ArmoryConfigFileName)
armoryConfigPath := filepath.Join(GetRootAppDir(), armoryConfigFileName)
if _, err := os.Stat(armoryConfigPath); os.IsNotExist(err) {
return []*ArmoryConfig{DefaultArmoryConfig}
return []*ArmoryConfig{defaultArmoryConfig}
}
data, err := os.ReadFile(armoryConfigPath)
if err != nil {
return []*ArmoryConfig{DefaultArmoryConfig}
return []*ArmoryConfig{defaultArmoryConfig}
}
var armoryConfigsFromFile []*ArmoryConfig
var armoryConfigs []*ArmoryConfig
err = json.Unmarshal(data, &armoryConfigsFromFile)
err = json.Unmarshal(data, &armoryConfigs)
if err != nil {
return []*ArmoryConfig{DefaultArmoryConfig}
return []*ArmoryConfig{defaultArmoryConfig}
}
// Force the default armory to be the last
defaultArmorySpecified := false
for _, config := range armoryConfigsFromFile {
if config.Name == DefaultArmoryName {
defaultArmorySpecified = true
continue
} else {
armoryConfigs = append(armoryConfigs, config)
for _, armoryConfig := range armoryConfigs {
if armoryConfig.AuthorizationCmd != "" {
armoryConfig.Authorization = executeAuthorizationCmd(armoryConfig)
}
}
if defaultArmorySpecified {
armoryConfigs = append(armoryConfigs, DefaultArmoryConfig)
}
RefreshArmoryAuthorization(armoryConfigs)
return armoryConfigs
}
func SaveArmoriesConfig(armories []*ArmoryConfig) error {
configData, err := json.Marshal(armories)
if err != nil {
return err
}
armoryConfigPath := filepath.Join(GetRootAppDir(), ArmoryConfigFileName)
err = os.WriteFile(armoryConfigPath, configData, 0640)
if err != nil {
return err
}
return nil
return append(armoryConfigs, defaultArmoryConfig)
}
func executeAuthorizationCmd(armoryConfig *ArmoryConfig) string {
+6 -24
View File
@@ -34,19 +34,12 @@ const (
SliverClientDirName = ".sliver-client"
versionFileName = "version"
envVarName = "SLIVER_CLIENT_ROOT_DIR"
)
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
func GetRootAppDir() string {
value := os.Getenv(envVarName)
var dir string
if len(value) == 0 {
user, _ := user.Current()
dir = filepath.Join(user.HomeDir, SliverClientDirName)
} else {
dir = value
}
user, _ := user.Current()
dir := filepath.Join(user.HomeDir, SliverClientDirName)
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0700)
if err != nil {
@@ -80,18 +73,6 @@ func GetConsoleLogsDir() string {
return consoleLogsDir
}
// GetMCPLogsDir - Get the Sliver client MCP logs dir ~/.sliver-client/logs/mcp/
func GetMCPLogsDir() string {
mcpLogsDir := filepath.Join(GetClientLogsDir(), "mcp")
if _, err := os.Stat(mcpLogsDir); os.IsNotExist(err) {
err = os.MkdirAll(mcpLogsDir, 0700)
if err != nil {
log.Fatal(err)
}
}
return mcpLogsDir
}
func assetVersion() string {
appDir := GetRootAppDir()
data, err := os.ReadFile(filepath.Join(appDir, versionFileName))
@@ -115,7 +96,7 @@ func Setup(force bool, echo bool) {
if force || localVer == "" || localVer != ver.GitCommit {
if echo {
fmt.Printf(`
Sliver Copyright (C) 2026 Bishop Fox
Sliver Copyright (C) 2022 Bishop Fox
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'licenses' for details.`)
@@ -123,6 +104,7 @@ under certain conditions; type 'licenses' for details.`)
}
saveAssetVersion(appDir)
}
_, _ = LoadSettings()
_, _ = LoadSpoofMetadataConfig()
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
SaveSettings(nil)
}
}
+25 -12
View File
@@ -35,7 +35,7 @@ type NameValueProbability struct {
Name string `json:"name"`
Value string `json:"value"`
Probability int `json:"probability"`
Method string `json:"method"`
Methods []string
}
// HTTPC2ImplantConfig - Implant configuration options
@@ -57,17 +57,30 @@ type HTTPC2ImplantConfig struct {
NonceQueryArgChars string `json:"nonce_query_args"`
URLParameters []NameValueProbability `json:"url_parameters"`
Headers []NameValueProbability `json:"headers"`
NonceQueryLength int `json:"nonce_query_length"`
NonceMode string `json:"nonce_mode"`
MaxFileGen int `json:"max_files"`
MinFileGen int `json:"min_files"`
MaxPathGen int `json:"max_paths"`
MinPathGen int `json:"min_paths"`
MaxPathLength int `json:"max_path_length"`
MinPathLength int `json:"min_path_length"`
MaxFiles int `json:"max_files"`
MinFiles int `json:"min_files"`
MaxPaths int `json:"max_paths"`
MinPaths int `json:"min_paths"`
Extensions []string `json:"extensions"`
Files []string `json:"files"`
Paths []string `json:"paths"`
// Stager files and paths
StagerFileExt string `json:"stager_file_ext"`
StagerFiles []string `json:"stager_files"`
StagerPaths []string `json:"stager_paths"`
// Poll files and paths
PollFileExt string `json:"poll_file_ext"`
PollFiles []string `json:"poll_files"`
PollPaths []string `json:"poll_paths"`
// Session files and paths
StartSessionFileExt string `json:"start_session_file_ext"`
SessionFileExt string `json:"session_file_ext"`
SessionFiles []string `json:"session_files"`
SessionPaths []string `json:"session_paths"`
// Close session files and paths
CloseFileExt string `json:"close_file_ext"`
CloseFiles []string `json:"close_files"`
ClosePaths []string `json:"close_paths"`
}
+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")
}
}
-27
View File
@@ -1,27 +0,0 @@
package assets
import (
"os"
"path/filepath"
"strings"
)
func legacyBackupPath(legacyPath string) string {
dir := filepath.Dir(legacyPath)
base := filepath.Base(legacyPath)
name := strings.TrimSuffix(base, filepath.Ext(base))
return filepath.Join(dir, "."+name+".json-old")
}
func renameLegacyConfig(legacyPath string) error {
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
return nil
}
backupPath := legacyBackupPath(legacyPath)
if _, err := os.Stat(backupPath); err == nil {
if err := os.Remove(backupPath); err != nil {
return err
}
}
return os.Rename(legacyPath, backupPath)
}
+16 -92
View File
@@ -22,111 +22,39 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
const (
settingsFileName = "tui-settings.yaml"
settingsLegacyFileName = "tui-settings.json"
)
const (
// Accepted values for tui-settings.yaml "prompt:":
// - host: show "[host]" prefix on sliver-client, "[server]" on server console
// - operator-host: show "[operator@host]" prefix on sliver-client, "[server]" on server console
// - basic: show "sliver >" (no prefix)
// - custom: render full prompt from prompt_template
PromptStyleHost = "host"
PromptStyleOperatorHost = "operator-host"
PromptStyleBasic = "basic"
PromptStyleCustom = "custom"
settingsFileName = "tui-settings.json"
)
// ClientSettings - Client JSON config
type ClientSettings struct {
TableStyle string `json:"tables" yaml:"tables"`
AutoAdult bool `json:"autoadult" yaml:"autoadult"`
BeaconAutoResults bool `json:"beacon_autoresults" yaml:"beacon_autoresults"`
SmallTermWidth int `json:"small_term_width" yaml:"small_term_width"`
AlwaysOverflow bool `json:"always_overflow" yaml:"always_overflow"`
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
UserConnect bool `json:"user_connect" yaml:"user_connect"`
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
PromptStyle string `json:"prompt" yaml:"prompt"`
PromptTemplate string `json:"prompt_template" yaml:"prompt_template"`
TableStyle string `json:"tables"`
AutoAdult bool `json:"autoadult"`
BeaconAutoResults bool `json:"beacon_autoresults"`
SmallTermWidth int `json:"small_term_width"`
AlwaysOverflow bool `json:"always_overflow"`
VimMode bool `json:"vim_mode"`
UserConnect bool `json:"user_connect"`
ConsoleLogs bool `json:"console_logs"`
}
// LoadSettings - Load the client settings from disk
func LoadSettings() (*ClientSettings, error) {
rootDir, _ := filepath.Abs(GetRootAppDir())
settingsPath := filepath.Join(rootDir, settingsFileName)
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
data, err := os.ReadFile(filepath.Join(rootDir, settingsFileName))
if err != nil {
return defaultSettings(), err
}
settings := defaultSettings()
migratedLegacy := false
data, err := os.ReadFile(settingsPath)
if err == nil {
if err = yaml.Unmarshal(data, settings); err != nil {
return defaultSettings(), err
}
} else if !os.IsNotExist(err) {
err = json.Unmarshal(data, settings)
if err != nil {
return defaultSettings(), err
} else if data, err = os.ReadFile(legacyPath); err == nil {
if err = json.Unmarshal(data, settings); err != nil {
return defaultSettings(), err
}
migratedLegacy = true
} else if !os.IsNotExist(err) {
return defaultSettings(), err
}
// Ensure any missing/unknown values are coerced to a supported prompt style.
settings.PromptStyle = NormalizePromptStyle(settings.PromptStyle)
if err := SaveSettings(settings); err != nil {
return settings, err
}
if migratedLegacy {
if err := renameLegacyConfig(legacyPath); err != nil {
return settings, err
}
}
return settings, nil
}
// NormalizePromptStyle canonicalizes prompt style strings and returns a safe
// default if the value is empty/unknown.
func NormalizePromptStyle(v string) string {
switch strings.ToLower(strings.TrimSpace(v)) {
case PromptStyleOperatorHost:
return PromptStyleOperatorHost
case PromptStyleHost:
return PromptStyleHost
case PromptStyleBasic:
return PromptStyleBasic
case PromptStyleCustom:
return PromptStyleCustom
// Backward compatible aliases.
case "show host":
return PromptStyleHost
case "show user and host":
return PromptStyleOperatorHost
case "show operator and host":
return PromptStyleOperatorHost
case "operator@host":
return PromptStyleOperatorHost
case "user@host":
return PromptStyleOperatorHost
case "":
return PromptStyleHost
default:
return PromptStyleHost
}
}
func defaultSettings() *ClientSettings {
return &ClientSettings{
TableStyle: "SliverDefault",
@@ -136,20 +64,16 @@ func defaultSettings() *ClientSettings {
AlwaysOverflow: false,
VimMode: false,
ConsoleLogs: true,
PromptStyle: PromptStyleHost,
PromptTemplate: DefaultPromptTemplate,
}
}
const DefaultPromptTemplate = `{{- if .IsServer -}}{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- else -}}{{- if .Host -}}{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }} {{- end -}}{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- end -}}`
// SaveSettings - Save the current settings to disk
func SaveSettings(settings *ClientSettings) error {
rootDir, _ := filepath.Abs(GetRootAppDir())
if settings == nil {
settings = defaultSettings()
}
data, err := yaml.Marshal(settings)
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
-137
View File
@@ -1,137 +0,0 @@
package assets
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestLoadSettingsParsesYAML(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
rootDir, _ := filepath.Abs(GetRootAppDir())
settingsPath := filepath.Join(rootDir, settingsFileName)
data := []byte(`tables: "Compact"
autoadult: true
beacon_autoresults: false
small_term_width: 120
always_overflow: true
vim_mode: true
user_connect: true
console_logs: false
prompt: "basic"
prompt_template: "{{.Host}}"
`)
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
t.Fatalf("failed to write yaml settings: %v", err)
}
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "Compact" {
t.Fatalf("expected TableStyle %q, got %q", "Compact", settings.TableStyle)
}
if !settings.AutoAdult {
t.Fatalf("expected AutoAdult true")
}
if settings.BeaconAutoResults {
t.Fatalf("expected BeaconAutoResults false")
}
if settings.SmallTermWidth != 120 {
t.Fatalf("expected SmallTermWidth %d, got %d", 120, settings.SmallTermWidth)
}
if !settings.AlwaysOverflow {
t.Fatalf("expected AlwaysOverflow true")
}
if !settings.VimMode {
t.Fatalf("expected VimMode true")
}
if !settings.UserConnect {
t.Fatalf("expected UserConnect true")
}
if settings.ConsoleLogs {
t.Fatalf("expected ConsoleLogs false")
}
if settings.PromptStyle != PromptStyleBasic {
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleBasic, settings.PromptStyle)
}
if settings.PromptTemplate != "{{.Host}}" {
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Host}}", settings.PromptTemplate)
}
}
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
rootDir, _ := filepath.Abs(GetRootAppDir())
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
legacy := &ClientSettings{
TableStyle: "Legacy",
AutoAdult: true,
BeaconAutoResults: false,
SmallTermWidth: 200,
AlwaysOverflow: true,
VimMode: true,
UserConnect: true,
ConsoleLogs: false,
PromptStyle: PromptStyleOperatorHost,
PromptTemplate: "{{.Operator}}@{{.Host}} sliver > ",
}
data, err := json.MarshalIndent(legacy, "", " ")
if err != nil {
t.Fatalf("failed to marshal legacy settings: %v", err)
}
if err := os.WriteFile(legacyPath, data, 0o600); err != nil {
t.Fatalf("failed to write legacy settings: %v", err)
}
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "Legacy" {
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
}
if settings.PromptStyle != PromptStyleOperatorHost {
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleOperatorHost, settings.PromptStyle)
}
if settings.PromptTemplate != "{{.Operator}}@{{.Host}} sliver > " {
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Operator}}@{{.Host}} sliver > ", settings.PromptTemplate)
}
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected yaml settings to exist: %v", err)
}
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
t.Fatalf("expected legacy settings to be renamed: %v", err)
}
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
t.Fatalf("expected legacy backup to exist: %v", err)
}
}
func TestLoadSettingsWritesDefault(t *testing.T) {
t.Setenv(envVarName, t.TempDir())
settings, err := LoadSettings()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if settings.TableStyle != "SliverDefault" {
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
}
if settings.PromptStyle != PromptStyleHost {
t.Fatalf("expected default PromptStyle %q, got %q", PromptStyleHost, settings.PromptStyle)
}
if settings.PromptTemplate != DefaultPromptTemplate {
t.Fatalf("expected default PromptTemplate %q, got %q", DefaultPromptTemplate, settings.PromptTemplate)
}
rootDir, _ := filepath.Abs(GetRootAppDir())
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
t.Fatalf("expected default settings to be written: %v", err)
}
}
-216
View File
@@ -1,216 +0,0 @@
package assets
/*
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 (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
const spoofMetadataFileName = "spoof-metadata.yaml"
const defaultSpoofMetadataConfigTemplate = `# Sliver executable metadata spoofing configuration.
# Used by generate commands when --spoof-metadata is set.
#
# Fields that reference file content support either:
# - path: read bytes from a local file path
# - base64: inline base64 file content
# Do not set both path and base64 for the same object.
#
# Example 1: Minimal path-based metadata cloning (most common)
pe:
source:
name: metadata-source.exe
path: ""
# Optional icon donor. Reserved for future standalone icon mutation support.
icon:
name: icon.ico
path: ""
# Example 2: Inline/base64 source (uncomment and remove "path" above)
# source:
# name: metadata-source.exe
# base64: "TVqQAAMAAAAEAAAA..."
# Example 3: Optional PE structure overrides (advanced)
# Applied after metadata/resources are cloned from source.
# Omitted fields keep cloned values.
# resource_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# number_of_named_entries: 0
# number_of_id_entries: 0
#
# resource_directory_entries:
# - name: 1
# offset_to_data: 0
#
# resource_data_entries:
# - offset_to_data: 0
# size: 0
# code_page: 0
# reserved: 0
#
# export_directory:
# characteristics: 0
# time_date_stamp: 0
# major_version: 1
# minor_version: 0
# name: 0
# base: 0
# number_of_functions: 0
# number_of_names: 0
# address_of_functions: 0
# address_of_names: 0
# address_of_name_ordinals: 0
`
// SpoofMetadataConfig stores default metadata spoofing inputs for generate commands.
type SpoofMetadataConfig struct {
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
}
// PESpoofMetadataConfig stores PE-specific spoofing inputs.
type PESpoofMetadataConfig struct {
Source *SpoofMetadataDataSource `json:"source,omitempty" yaml:"source,omitempty"`
Icon *SpoofMetadataDataSource `json:"icon,omitempty" yaml:"icon,omitempty"`
ResourceDirectory *ImageResourceDirectory `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
ResourceDirectoryEntries []*ImageResourceDirectoryEntry `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
ResourceDataEntries []*ImageResourceDataEntry `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
ExportDirectory *ImageExportDirectory `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
}
// SpoofMetadataDataSource points to bytes that should be sent to the server.
// Either Path or Base64 may be set.
type SpoofMetadataDataSource struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Base64 string `json:"base64,omitempty" yaml:"base64,omitempty"`
}
type ImageResourceDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
NumberOfNamedEntries uint32 `json:"number_of_named_entries,omitempty" yaml:"number_of_named_entries,omitempty"`
NumberOfIDEntries uint32 `json:"number_of_id_entries,omitempty" yaml:"number_of_id_entries,omitempty"`
}
type ImageResourceDirectoryEntry struct {
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
}
type ImageResourceDataEntry struct {
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
Size uint32 `json:"size,omitempty" yaml:"size,omitempty"`
CodePage uint32 `json:"code_page,omitempty" yaml:"code_page,omitempty"`
Reserved uint32 `json:"reserved,omitempty" yaml:"reserved,omitempty"`
}
type ImageExportDirectory struct {
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
Base uint32 `json:"base,omitempty" yaml:"base,omitempty"`
NumberOfFunctions uint32 `json:"number_of_functions,omitempty" yaml:"number_of_functions,omitempty"`
NumberOfNames uint32 `json:"number_of_names,omitempty" yaml:"number_of_names,omitempty"`
AddressOfFunctions uint32 `json:"address_of_functions,omitempty" yaml:"address_of_functions,omitempty"`
AddressOfNames uint32 `json:"address_of_names,omitempty" yaml:"address_of_names,omitempty"`
AddressOfNameOrdinals uint32 `json:"address_of_name_ordinals,omitempty" yaml:"address_of_name_ordinals,omitempty"`
}
// SpoofMetadataConfigPath returns the spoof metadata config file path.
func SpoofMetadataConfigPath() string {
rootDir, _ := filepath.Abs(GetRootAppDir())
return filepath.Join(rootDir, spoofMetadataFileName)
}
// LoadSpoofMetadataConfig loads spoof metadata config from disk, writing defaults
// if the file is missing.
func LoadSpoofMetadataConfig() (*SpoofMetadataConfig, error) {
config := defaultSpoofMetadataConfig()
configPath := SpoofMetadataConfigPath()
data, err := os.ReadFile(configPath)
if err != nil {
if !os.IsNotExist(err) {
return defaultSpoofMetadataConfig(), err
}
if err := os.WriteFile(configPath, []byte(defaultSpoofMetadataConfigTemplate), 0o600); err != nil {
return defaultSpoofMetadataConfig(), err
}
data = []byte(defaultSpoofMetadataConfigTemplate)
}
if err := yaml.Unmarshal(data, config); err != nil {
return defaultSpoofMetadataConfig(), err
}
normalizeSpoofMetadataConfig(config)
return config, nil
}
// SaveSpoofMetadataConfig writes spoof metadata config to disk.
func SaveSpoofMetadataConfig(config *SpoofMetadataConfig) error {
if config == nil {
config = defaultSpoofMetadataConfig()
}
normalizeSpoofMetadataConfig(config)
data, err := yaml.Marshal(config)
if err != nil {
return err
}
return os.WriteFile(SpoofMetadataConfigPath(), data, 0o600)
}
func normalizeSpoofMetadataConfig(config *SpoofMetadataConfig) {
if config.PE == nil {
config.PE = &PESpoofMetadataConfig{}
}
if config.PE.Source == nil {
config.PE.Source = &SpoofMetadataDataSource{Name: "metadata-source.exe"}
}
if config.PE.Icon == nil {
config.PE.Icon = &SpoofMetadataDataSource{Name: "icon.ico"}
}
}
func defaultSpoofMetadataConfig() *SpoofMetadataConfig {
return &SpoofMetadataConfig{
PE: &PESpoofMetadataConfig{
Source: &SpoofMetadataDataSource{
Name: "metadata-source.exe",
Path: "",
},
Icon: &SpoofMetadataDataSource{
Name: "icon.ico",
Path: "",
},
},
}
}
+3 -13
View File
@@ -1,14 +1,4 @@
# client/cli
CLI
===
## Overview
Defines the Cobra root command and CLI initialization for the Sliver client binary. Sets up persistent flags, environment bootstrapping, and command wiring. Core logic addresses console, implant, import, and version within the cli package.
## Go Files
- `cli.go` – Builds the root Cobra command, wires global flags, and starts the console UI.
- `config.go` – Parses CLI configuration files and environment variables used during startup.
- `console.go` – Launches the interactive console mode and handles profile selection.
- `implant.go` – Implements CLI entry points for implant-specific operations without launching the console.
- `import.go` – Provides import routines for bringing external state into the client (e.g., implants or loot).
- `version.go` – Prints version/build information for the CLI binary.
This package implements the Sliver client cli.
+8 -17
View File
@@ -24,17 +24,20 @@ import (
"os"
"path"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/version"
)
const (
logFileName = "sliver-client.log"
)
// Initialize logging.
var sliverServerVersion = fmt.Sprintf("v%s", version.FullVersion())
// Initialize logging
func initLogging(appDir string) *os.File {
log.SetFlags(log.LstdFlags | log.Lshortfile)
logFile, err := os.OpenFile(path.Join(appDir, logFileName), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
@@ -46,16 +49,7 @@ func initLogging(appDir string) *os.File {
}
func init() {
appDir := assets.GetRootAppDir()
logFile := initLogging(appDir)
defer logFile.Close()
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().String(ConsoleConfigFlagName, "", "path to client config 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
@@ -74,9 +68,6 @@ func init() {
// other command trees below, such as the implant one.
rootCmd.AddCommand(consoleCmd(con))
// MCP stdio server.
rootCmd.AddCommand(mcpCmd(con))
// Implant.
// The implant command allows users to run commands on slivers from their
// system shell. It makes use of pre-runners for connecting to the server
@@ -97,10 +88,10 @@ var rootCmd = &cobra.Command{
Long: ``,
}
// Execute - Execute root command.
// Execute - Execute root command
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Printf("root command: %s\n", err)
fmt.Println(err)
os.Exit(1)
}
}
+30 -27
View File
@@ -20,49 +20,52 @@ package cli
import (
"fmt"
"path/filepath"
"sort"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/forms"
"gopkg.in/AlecAivazis/survey.v1"
)
func selectConfig(configPath string, stdinTTY bool) (string, *assets.ClientConfig, error) {
if configPath != "" {
config, err := loadMCPClientConfig(configPath)
if err != nil {
return "", nil, fmt.Errorf("load client config: %w", err)
}
return filepath.Base(configPath), config, nil
}
return selectConfigFrom(assets.GetConfigs(), stdinTTY)
}
func selectConfig() *assets.ClientConfig {
configs := assets.GetConfigs()
func selectConfigFrom(configs map[string]*assets.ClientConfig, stdinTTY bool) (string, *assets.ClientConfig, error) {
if len(configs) == 0 {
return "", nil, fmt.Errorf("no config files found at %s", assets.GetConfigDir())
return nil
}
if len(configs) == 1 {
for key, config := range configs {
return key, config, nil
for _, config := range configs {
return config
}
}
if !stdinTTY {
return "", nil, fmt.Errorf("multiple configs found; use --config to select one")
answer := struct{ Config string }{}
qs := getPromptForConfigs(configs)
err := survey.Ask(qs, &answer)
if err != nil {
fmt.Println(err.Error())
return nil
}
keys := make([]string, 0, len(configs))
for key := range configs {
keys = append(keys, key)
return configs[answer.Config]
}
func getPromptForConfigs(configs map[string]*assets.ClientConfig) []*survey.Question {
keys := []string{}
for k := range configs {
keys = append(keys, k)
}
sort.Strings(keys)
selection := keys[0]
err := forms.Select("Select a server:", keys, &selection)
if err != nil {
return "", nil, err
return []*survey.Question{
{
Name: "config",
Prompt: &survey.Select{
Message: "Select a server:",
Options: keys,
Default: keys[0],
},
},
}
return selection, configs[selection], nil
}
-46
View File
@@ -1,46 +0,0 @@
package cli
import (
"testing"
)
func TestConfigFlagRoutesBeforeAndAfterConsoleAndMCP(t *testing.T) {
flag := rootCmd.PersistentFlags().Lookup(ConsoleConfigFlagName)
if flag == nil {
t.Fatal("root --config flag is missing")
}
oldValue, oldChanged := flag.Value.String(), flag.Changed
t.Cleanup(func() {
_ = flag.Value.Set(oldValue)
flag.Changed = oldChanged
})
for _, command := range []string{"console", "mcp"} {
for _, before := range []bool{true, false} {
name := command + " after --config"
args := []string{"--config", "selected.cfg", command}
if !before {
name = command + " before --config"
args = []string{command, "--config", "selected.cfg"}
}
t.Run(name, func(t *testing.T) {
_ = flag.Value.Set("")
flag.Changed = false
cmd, remaining, err := rootCmd.Traverse(args)
if err != nil {
t.Fatalf("traverse %q: %v", args, err)
}
if cmd.Name() != command {
t.Fatalf("command = %q, want %q", cmd.Name(), command)
}
if err := cmd.ParseFlags(remaining); err != nil {
t.Fatalf("parse %q: %v", remaining, err)
}
got, err := cmd.Flags().GetString(ConsoleConfigFlagName)
if err != nil || got != "selected.cfg" {
t.Fatalf("--config = %q, error = %v", got, err)
}
})
}
}
}
-53
View File
@@ -1,53 +0,0 @@
package cli
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/bishopfox/sliver/client/assets"
)
func TestSelectConfigFromNonTTYRequiresExplicitConfigWhenMultiple(t *testing.T) {
configs := map[string]*assets.ClientConfig{
"first": {Operator: "first"},
"second": {Operator: "second"},
}
key, config, err := selectConfigFrom(configs, false)
if err == nil || !strings.Contains(err.Error(), "--config") {
t.Fatalf("error = %v, want --config guidance", err)
}
if key != "" || config != nil {
t.Fatalf("selected %q (%v) without an explicit config", key, config)
}
}
func TestSelectConfigFromNonTTYUsesSoleConfig(t *testing.T) {
want := &assets.ClientConfig{Operator: "only"}
key, config, err := selectConfigFrom(map[string]*assets.ClientConfig{"only": want}, false)
if err != nil || key != "only" || config != want {
t.Fatalf("selection = %q (%v), error = %v", key, config, err)
}
}
func TestSelectConfigWithExplicitPathOnNonTTY(t *testing.T) {
want := &assets.ClientConfig{Operator: "selected", LHost: "127.0.0.1"}
data, err := json.Marshal(want)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "selected.cfg")
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
key, config, err := selectConfig(path, false)
if err != nil {
t.Fatalf("select explicit config: %v", err)
}
if key != "selected.cfg" || config.Operator != want.Operator || config.LHost != want.LHost {
t.Fatalf("selection = %q (%v)", key, config)
}
}
-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))
}
-145
View File
@@ -1,145 +0,0 @@
package cli
import (
"bytes"
"strings"
"testing"
"testing/synctest"
"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) {
synctest.Test(t, testConnectWithSpinnerOutput)
}
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) {
synctest.Test(t, testConnectWithSpinnerFastSuccessStillShowsMultipleFrames)
}
func testConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
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) {
synctest.Test(t, testConnectWithSpinnerFastSuccessShowsEachStatus)
}
func testConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
var out bytes.Buffer
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
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)
}
}
-26
View File
@@ -1,26 +0,0 @@
package cli
import (
"context"
"fmt"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
)
func handleConnectionLost(ln *grpc.ClientConn) {
if ln == nil {
return
}
currentState := ln.GetState()
// currentState should be "Ready" when the connection is established.
if ln.WaitForStateChange(context.Background(), currentState) {
newState := ln.GetState()
// newState will be "Idle" if the connection is lost.
if newState == connectivity.Idle {
fmt.Println("\nLost connection to server. Exiting now.")
os.Exit(1)
}
}
}
+36 -64
View File
@@ -1,100 +1,72 @@
package cli
/*
Sliver Implant Framework
Copyright (C) 2019 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"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/grpc"
)
// ConsoleConfigFlagName is the flag used to select a client config file.
const ConsoleConfigFlagName = "config"
// consoleCmd generates the console with required pre/post runners.
func consoleCmd(con *console.SliverClient) *cobra.Command {
// consoleCmd generates the console with required pre/post runners
func consoleCmd(con *console.SliverConsoleClient) *cobra.Command {
consoleCmd := &cobra.Command{
Use: "console",
Short: "Start the sliver client console",
}
consoleCmd.Flags().String(RCFlagName, "", "path to rc script file")
consoleCmd.RunE, consoleCmd.PersistentPostRunE = consoleRunnerCmd(con, true)
return consoleCmd
}
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
}
func consoleRunnerCmd(con *console.SliverConsoleClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
var ln *grpc.ClientConn
rcScript, err := ReadRCScript(cmd)
if err != nil {
return err
}
pre = func(_ *cobra.Command, _ []string) error {
appDir := assets.GetRootAppDir()
logFile := initLogging(appDir)
defer logFile.Close()
configPath := ""
if cmd.Flags().Lookup(ConsoleConfigFlagName) != nil {
configPath, err = cmd.Flags().GetString(ConsoleConfigFlagName)
if err != nil {
return err
}
configs := assets.GetConfigs()
if len(configs) == 0 {
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
return nil
}
stdinTTY := term.IsTerminal(int(os.Stdin.Fd()))
configKey, config, err := selectConfig(configPath, stdinTTY)
if err != nil {
return err
config := selectConfig()
if config == nil {
return nil
}
target := fmt.Sprintf("%s:%d", 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 && stdinTTY {
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)
if run {
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
}
var rpc rpcpb.SliverRPCClient
var err error
rpc, ln, err = transport.MTLSConnect(config)
if err != nil {
return fmt.Errorf("connection to server failed: %w", err)
fmt.Printf("Connection to server failed %s", err)
return nil
}
if err := console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript); err != nil {
_ = con.CloseConnection()
return err
}
return nil
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run)
}
// Close the RPC connection once exiting
post = func(_ *cobra.Command, _ []string) error {
return con.CloseConnection()
if ln != nil {
return ln.Close()
}
return nil
}
return pre, post
+10 -29
View File
@@ -1,36 +1,19 @@
package cli
/*
Sliver Implant Framework
Copyright (C) 2019 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 (
"errors"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/bishopfox/sliver/client/command"
"github.com/bishopfox/sliver/client/command/use"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func implantCmd(con *console.SliverClient) *cobra.Command {
func implantCmd(con *console.SliverConsoleClient) *cobra.Command {
con.IsCLI = true
makeCommands := command.SliverCommands(con)
@@ -42,7 +25,7 @@ func implantCmd(con *console.SliverClient) *cobra.Command {
implantFlags.StringP("use", "s", "", "interact with a session")
cmd.Flags().AddFlagSet(implantFlags)
// Pre-runners (console setup, connection, etc)
// Prerunners (console setup, connection, etc)
cmd.PersistentPreRunE, cmd.PersistentPostRunE = makeRunners(cmd, con)
// Completions
@@ -51,15 +34,13 @@ func implantCmd(con *console.SliverClient) *cobra.Command {
return cmd
}
func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, post func(cmd *cobra.Command, args []string) error) {
func makeRunners(implantCmd *cobra.Command, con *console.SliverConsoleClient) (pre, post func(cmd *cobra.Command, args []string) error) {
startConsole, closeConsole := consoleRunnerCmd(con, false)
// The pre-run function connects to the server and sets up a "fake" console,
// so we can have access to active sessions/beacons, and other stuff needed.
pre = func(_ *cobra.Command, args []string) error {
if err := startConsole(implantCmd, args); err != nil {
return err
}
startConsole(implantCmd, args)
// Set the active target.
target, _ := implantCmd.Flags().GetString("use")
@@ -78,7 +59,7 @@ func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, pos
return pre, closeConsole
}
func makeCompleters(cmd *cobra.Command, con *console.SliverClient) {
func makeCompleters(cmd *cobra.Command, con *console.SliverConsoleClient) {
comps := carapace.Gen(cmd)
comps.PreRun(func(cmd *cobra.Command, args []string) {
@@ -86,7 +67,7 @@ func makeCompleters(cmd *cobra.Command, con *console.SliverClient) {
})
// Bind completers to flags (wrap them to use the same pre-runners)
command.BindFlagCompletions(cmd, func(comp *carapace.ActionMap) {
command.FlagComps(cmd, func(comp *carapace.ActionMap) {
(*comp)["use"] = carapace.ActionCallback(func(c carapace.Context) carapace.Action {
cmd.PersistentPreRunE(cmd, c.Args)
return use.SessionIDCompleter(con)
+2 -1
View File
@@ -22,9 +22,10 @@ import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/assets"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/assets"
)
func importCmd() *cobra.Command {
-97
View File
@@ -1,97 +0,0 @@
package cli
/*
Sliver Implant Framework
Copyright (C) 2023 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"
"path/filepath"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
clientmcp "github.com/bishopfox/sliver/client/mcp"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
func mcpCmd(con *console.SliverClient) *cobra.Command {
mcpCmd := &cobra.Command{
Use: "mcp",
Short: "Start the MCP stdio server",
RunE: func(cmd *cobra.Command, args []string) error {
return runMCP(cmd, con)
},
}
return mcpCmd
}
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 {
fmt.Printf("%s\n", err)
return nil
}
rpc, ln, err := transport.MTLSConnect(config)
if err != nil {
fmt.Printf("Connection to server failed %s\n", err)
return nil
}
defer transport.CloseGRPCConnection(ln)
con.Rpc = rpc
go handleConnectionLost(ln)
cfg := clientmcp.DefaultConfig()
if err := clientmcp.ServeStdio(cfg, con.Rpc); err != nil {
fmt.Printf("MCP server error: %s\n", err)
return nil
}
return nil
}
func loadMCPClientConfig(path string) (*assets.ClientConfig, error) {
if path != "" {
configPath := path
if !filepath.IsAbs(configPath) {
if _, err := os.Stat(configPath); err != nil {
configPath = filepath.Join(assets.GetConfigDir(), configPath)
}
}
return assets.ReadConfig(configPath)
}
configs := assets.GetConfigs()
switch len(configs) {
case 0:
return nil, fmt.Errorf("no config files found at %s (use --config)", assets.GetConfigDir())
case 1:
for _, config := range configs {
return config, nil
}
default:
return nil, fmt.Errorf("multiple configs found; use --config to select one")
}
return nil, fmt.Errorf("no config files found")
}
-31
View File
@@ -1,31 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
const RCFlagName = "rc"
func ReadRCScript(cmd *cobra.Command) (string, error) {
if cmd == nil {
return "", nil
}
if cmd.Flags().Lookup(RCFlagName) == nil {
return "", nil
}
rcPath, err := cmd.Flags().GetString(RCFlagName)
if err != nil {
return "", err
}
if rcPath == "" {
return "", nil
}
data, err := os.ReadFile(rcPath)
if err != nil {
return "", fmt.Errorf("read rc script %q: %w", rcPath, err)
}
return string(data), nil
}
-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",
},
}
}
+2 -1
View File
@@ -21,8 +21,9 @@ package cli
import (
"fmt"
"github.com/bishopfox/sliver/client/version"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/version"
)
var cmdVersion = &cobra.Command{
+11 -65
View File
@@ -1,68 +1,14 @@
# client/command
Command
=======
## Overview
This package contains all of the client command implementations that are shared between the client and the server console.
Shared registration glue, scaffolding, and helpers for Sliver client console command groups. Hosts common utilities for binding Cobra commands to RPC-backed actions. Core logic addresses sliver within the command package.
### Developers
## Go Files
- `command.go` – Provides helpers for binding Cobra commands, flag sets, and visibility filters into the client command tree.
- `server.go` – Defines the server menu composition, wiring each server-side command group onto the root console structure at runtime.
- `main.go` – Builds the implant/Sliver menu, attaching per-target command groups and loading alias/extension manifests before exposure.
## 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.
- `beacons/` – Implements the 'beacons' command group for the Sliver client console.
- `builders/` – Implements the 'builders' command group for the Sliver client console.
- `c2profiles/` – Implements the 'c2profiles' command group for the Sliver client console.
- `certificates/` – Implements the 'certificates' command group for the Sliver client console.
- `clean/` – Implements the 'clean' command group for the Sliver client console.
- `completers/` – Implements the 'completers' command group for the Sliver client console.
- `crack/` – Implements the 'crack' command group for the Sliver client console.
- `creds/` – Implements the 'creds' command group for the Sliver client console.
- `cursed/` – Implements the 'cursed' command group for the Sliver client console.
- `dllhijack/` – Implements the 'dllhijack' command group for the Sliver client console.
- `edit/` – Implements the 'edit' command for the Sliver client console.
- `environment/` – Implements the 'environment' command group for the Sliver client console.
- `exec/` – Implements the 'exec' command group for the Sliver client console.
- `exit/` – Implements the 'exit' command group for the Sliver client console.
- `extensions/` – Implements the 'extensions' command group for the Sliver client console.
- `filesystem/` – Implements the 'filesystem' command group for the Sliver client console.
- `flags/` – Implements the 'flags' command group for the Sliver client console.
- `generate/` – Implements the 'generate' command group for the Sliver client console.
- `help/` – Implements the 'help' command group for the Sliver client console.
- `hosts/` – Implements the 'hosts' command group for the Sliver client console.
- `info/` – Implements the 'info' command group for the Sliver client console.
- `jobs/` – Implements the 'jobs' command group for the Sliver client console.
- `kill/` – Implements the 'kill' command group for the Sliver client console.
- `licenses/` – Implements the 'licenses' command group for the Sliver client console.
- `loot/` – Implements the 'loot' command group for the Sliver client console.
- `monitor/` – Implements the 'monitor' command group for the Sliver client console.
- `mcp/` – Implements the 'mcp' command group for the Sliver client console.
- `network/` – Implements the 'network' command group for the Sliver client console.
- `operators/` – Implements the 'operators' command group for the Sliver client console.
- `pivots/` – Implements the 'pivots' command group for the Sliver client console.
- `portfwd/` – Implements the 'portfwd' command group for the Sliver client console.
- `privilege/` – Implements the 'privilege' command group for the Sliver client console.
- `processes/` – Implements the 'processes' command group for the Sliver client console.
- `reaction/` – Implements the 'reaction' command group for the Sliver client console.
- `reconfig/` – Implements the 'reconfig' command group for the Sliver client console.
- `registry/` – Implements the 'registry' command group for the Sliver client console.
- `rportfwd/` – Implements the 'rportfwd' command group for the Sliver client console.
- `screenshot/` – Implements the 'screenshot' command group for the Sliver client console.
- `sessions/` – Implements the 'sessions' command group for the Sliver client console.
- `settings/` – Implements the 'settings' command group for the Sliver client console.
- `shell/` – Implements the 'shell' command group for the Sliver client console.
- `shikata-ga-nai/` – Implements the 'shikata-ga-nai' command group for the Sliver client console.
- `socks/` – Implements the 'socks' command group for the Sliver client console.
- `taskmany/` – Implements the 'taskmany' command group for the Sliver client console.
- `tasks/` – Implements the 'tasks' command group for the Sliver client console.
- `update/` – Implements the 'update' command group for the Sliver client console.
- `use/` – Implements the 'use' command group for the Sliver client console.
- `wasm/` – Implements the 'wasm' command group for the Sliver client console.
- `websites/` – Implements the 'websites' command group for the Sliver client console.
- `wireguard/` – Implements the 'wireguard' command group for the Sliver client console.
General guidance on the structure of this package:
* One command per file and the name of the file should reflect the command it implements (or there about, trivial commands can go into the same file). This makes it easy to find where a command implementation lives within a package or to search by file name if it is not clear which package implements a given command.
* The root command, and reused code should go into the file named after the command
* For example, code shared between the `generate` and the `regenerate` command should go in `generate.go`, and any `regenerate` specific code should go in `regenerate.go`
* Command entrypoint functions should have the suffix `Cmd` e.g., `GenerateCmd` is the entrypoint for `generate`
* Command entrypoints should always a function signature of `func (ctx *grumble.Context, con *console.SliverConsoleClient)`
* Functions that are only ever exported for other commands to use should go in a `helpers.go`, if the function is used internally and exported follow the guidance above on shared code.
-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
-23
View File
@@ -1,23 +0,0 @@
# client/command/aka
## Overview
Implements the 'aka' command group for the Sliver client console. "Aka"s are
command aliases (like 'alias' in 'bash') that allow operators to specify
shorthand commands and default arguments for commonly used commands.
### Examples:
- `aka create bg background`: Allows an operator to run `bg` from the implant
menu to quickly move back to the server menu
- `aka create execw execute -o "C:\Windows\System32\cmd.exe" /c`: Shorthand for
executing binaries on Windows targets like: `execw arp -a`
## Go Files
- `commands.go` – Wires Cobra commands for listing, creating, and deleting command aliases in the client console.
- `create.go` – Handles creating command aliases to use by the client.
- `delete.go` – Removes command aliases currently tracked by the client.
- `hook.go` – The main hooking logic to intercept commands ran by the client console. The hook will intercept processing before hitting the Cobra tree to transform the command alias to a full command with default arguments from the command alias and user-provided arguments to the alias.
- `list.go` - Lists the current command aliases tracked by the client.
- `storage.go` - Supports the saving of aliases to disk to keep between runs of the client.
-59
View File
@@ -1,59 +0,0 @@
package aka
import (
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
)
func ServerCommands(con *console.SliverClient) []*cobra.Command {
return commands(con, constants.GenericHelpGroup)
}
func ImplantCommands(con *console.SliverClient) []*cobra.Command {
return commands(con, constants.SliverCoreHelpGroup)
}
func commands(con *console.SliverClient, group string) []*cobra.Command {
akaCommand := &cobra.Command{
Use: "aka",
Short: "Manage command aliases (aka's)",
Run: func(cmd *cobra.Command, args []string) {
AkaListCmd(cmd, con, args)
},
GroupID: group,
}
createAka := &cobra.Command{
Use: "create <alias> <command> [args...]",
Short: "Create a new command alias (aka)",
Args: cobra.MinimumNArgs(2),
DisableFlagParsing: true, // No flags with aka create so we capture flags to other commands
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
AkaCreateCmd(cmd, con, args)
},
}
createAka.InitDefaultHelpFlag()
createAka.Flags().Lookup("help").Hidden = true
akaCommand.AddCommand(createAka)
deleteAka := &cobra.Command{
Use: "delete <alias>",
Short: "Delete a command alias (aka)",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
AkaDeleteCmd(cmd, con, args)
},
}
akaCommand.AddCommand(deleteAka)
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, Cmdhook)
err := LoadAkaAliases()
if err != nil {
con.PrintErrorf("Failed to load aka alises: %s\n", err)
}
return []*cobra.Command{akaCommand}
}
-46
View File
@@ -1,46 +0,0 @@
package aka
import (
"fmt"
"strings"
"github.com/bishopfox/sliver/client/console"
"github.com/spf13/cobra"
)
func AkaCreateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
aliasName := args[0]
command := args[1]
defaultArgs := args[2:] // anything else is going to be the default args to the alias
if _, exists := akaAliases[aliasName]; exists {
con.PrintErrorf("Alias '%s' already exists\n", aliasName)
return
}
var desc string
if len(defaultArgs) == 0 {
desc = fmt.Sprintf("%s", command)
} else {
desc = fmt.Sprintf("%s %s", command, strings.Join(defaultArgs, " "))
}
alias := &AkaAlias{
Name: aliasName,
Command: command,
DefaultArgs: defaultArgs,
Description: desc,
}
akaAliases[aliasName] = alias
// save this new alias to disk
err := SaveAkaAliases()
if err != nil {
con.PrintErrorf("Failed to save alias '%s' to disk: %s\n", aliasName, err)
// we still technically added it to the map, so the alias did get created in
// memory so we should still print the Info message below
}
con.PrintInfof("Create alias for '%s' -> '%v'\n", aliasName, alias.Description)
}
-25
View File
@@ -1,25 +0,0 @@
package aka
import (
"github.com/bishopfox/sliver/client/console"
"github.com/spf13/cobra"
)
func AkaDeleteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
aliasName := args[0]
if _, exists := akaAliases[aliasName]; !exists {
con.PrintErrorf("Alias '%s' does not exist\n", aliasName)
return
}
delete(akaAliases, aliasName)
// Save updated map to disk
err := SaveAkaAliases()
if err != nil {
con.PrintErrorf("Failed to save aliases to disk: %s\n", err)
return
}
con.PrintInfof("Deleted alias '%s'\n", aliasName)
}
-27
View File
@@ -1,27 +0,0 @@
package aka
import (
"strings"
)
func Cmdhook(args []string) ([]string, error) {
if len(args) == 0 {
return args, nil
}
if aliasArgs, exists := akaAliases[args[0]]; exists {
var expandedArgs []string
expandedArgs = append(expandedArgs, aliasArgs.Command)
expandedArgs = append(expandedArgs, aliasArgs.DefaultArgs...)
// join the user passed args so the shell processes it without flags if
// available
if len(args[1:]) > 0 {
expandedArgs = append(expandedArgs, strings.Join(args[1:], " "))
}
return expandedArgs, nil
}
return args, nil
}
-31
View File
@@ -1,31 +0,0 @@
package aka
import (
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
func AkaListCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if len(akaAliases) == 0 {
con.PrintInfof("No command aliases created. Use `aka create` to create one.\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Alias",
"Command",
})
for alias, cmd := range akaAliases {
tw.AppendRow(table.Row{
alias,
cmd.Description,
})
}
con.Printf("%s\n", tw.Render())
}
-68
View File
@@ -1,68 +0,0 @@
package aka
import (
"encoding/json"
"os"
"path/filepath"
"github.com/bishopfox/sliver/client/assets"
)
type AkaAlias struct {
Name string `json:"name"` // alias Name
Command string `json:"command"` // command being aliased
DefaultArgs []string `json:"default_args"` // args passed by the user
Description string `json:"description"`
}
var akaAliases = make(map[string]*AkaAlias)
const (
akaAliasFileName = "aka-aliases.json"
)
func GetAkaAliasesFilePath() string {
return filepath.Join(assets.GetRootAppDir(), akaAliasFileName)
}
func LoadAkaAliases() error {
filepath := GetAkaAliasesFilePath()
data, err := os.ReadFile(filepath)
if err != nil {
// if file hasn't been created yet, we fail gracefully
if os.IsNotExist(err) {
return nil
}
// otherwise, return the actual error; something else went wrong
return err
}
aliases := []AkaAlias{}
err = json.Unmarshal(data, &aliases)
if err != nil {
return err
}
akaAliases = make(map[string]*AkaAlias)
for _, alias := range aliases {
akaAliases[alias.Name] = &alias
}
return nil
}
func SaveAkaAliases() error {
filepath := GetAkaAliasesFilePath()
aliases := make([]*AkaAlias, 0, len(akaAliases))
for _, alias := range akaAliases {
aliases = append(aliases, alias)
}
data, err := json.MarshalIndent(aliases, "", " ")
if err != nil {
return err
}
return os.WriteFile(filepath, data, 0o600)
}
+7 -10
View File
@@ -1,14 +1,11 @@
# client/command/alias
Alias
===========
## Overview
Wraps external DLLs/tool in Sliver console commands.
Implements the 'alias' command group for the Sliver client console. Handlers map Cobra invocations to alias workflows such as install, load, and remove.
## Go Files
- `alias.go` – Implements runtime alias management, printing installed aliases and exposing tab-completion helpers.
- `alias_test.go` *(tests)* – Exercises manifest parsing logic to ensure alias metadata is interpreted correctly.
- `commands.go` – Wires Cobra commands for listing, installing, loading, and removing aliases into the client console.
- `install.go` – Handles installing alias bundles from directories or archives, writing manifests and payload artifacts to disk.
- `load.go` – Loads alias manifests, validates target compatibility, and registers commands against the active console.
- `remove.go` – Removes installed aliases, prompting for confirmation and deleting associated on-disk assets.
### Format
* `alias-dir/`
* * `manifest.json` -
+8 -7
View File
@@ -27,14 +27,15 @@ import (
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// AliasesCmd - The alias command.
func AliasesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) error {
// AliasesCmd - The alias command
func AliasesCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) error {
if 0 < len(loadedAliases) {
PrintAliases(con)
} else {
@@ -44,8 +45,8 @@ func AliasesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) er
return nil
}
// PrintAliases - Print a list of loaded aliases.
func PrintAliases(con *console.SliverClient) {
// PrintAliases - Print a list of loaded aliases
func PrintAliases(con *console.SliverConsoleClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
@@ -87,8 +88,8 @@ func PrintAliases(con *console.SliverClient) {
con.Println(tw.Render())
}
// AliasCommandNameCompleter - Completer for installed extensions command names.
func AliasCommandNameCompleter(prefix string, args []string, con *console.SliverClient) []string {
// AliasCommandNameCompleter - Completer for installed extensions command names
func AliasCommandNameCompleter(prefix string, args []string, con *console.SliverConsoleClient) []string {
results := []string{}
for name := range loadedAliases {
if strings.HasPrefix(name, prefix) {
@@ -128,7 +129,7 @@ func getInstalledManifests() map[string]*AliasManifest {
return installedManifests
}
// AliasCommandNameCompleter - Completer for installed extensions command names.
// AliasCommandNameCompleter - Completer for installed extensions command names
func AliasCompleter() carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
results := []string{}
-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")
}
}
-64
View File
@@ -1,64 +0,0 @@
package alias
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// Commands returns the `alias` command and its child commands.
func Commands(con *console.SliverClient) []*cobra.Command {
aliasCmd := &cobra.Command{
Use: consts.AliasesStr,
Short: "List current aliases",
Long: help.GetHelpFor([]string{consts.AliasesStr}),
Run: func(cmd *cobra.Command, args []string) {
AliasesCmd(cmd, con, args)
},
GroupID: consts.GenericHelpGroup,
}
aliasLoadCmd := &cobra.Command{
Use: consts.LoadStr + " [ALIAS]",
Short: "Load a command alias",
Long: help.GetHelpFor([]string{consts.AliasesStr, consts.LoadStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
AliasesLoadCmd(cmd, con, args)
},
}
aliasCmd.AddCommand(aliasLoadCmd)
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
aliasInstallCmd := &cobra.Command{
Use: consts.InstallStr + " [ALIAS]",
Short: "Install a command alias",
Long: help.GetHelpFor([]string{consts.AliasesStr, consts.InstallStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
AliasesInstallCmd(cmd, con, args)
},
}
aliasCmd.AddCommand(aliasInstallCmd)
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
aliasRemove := &cobra.Command{
Use: consts.RmStr + " [ALIAS]",
Short: "Remove an alias",
Long: help.GetHelpFor([]string{consts.RmStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
AliasesRemoveCmd(cmd, con, args)
},
}
flags.NewCompletions(aliasRemove).PositionalCompletion(AliasCompleter())
aliasCmd.AddCommand(aliasRemove)
return []*cobra.Command{aliasCmd}
}
+19 -22
View File
@@ -24,30 +24,31 @@ import (
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/util"
"github.com/spf13/cobra"
)
// AliasesInstallCmd - Install an alias.
func AliasesInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// AliasesInstallCmd - Install an alias
func AliasesInstallCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
aliasLocalPath := args[0]
fi, err := os.Stat(aliasLocalPath)
if os.IsNotExist(err) {
con.PrintErrorf("Alias path '%s' does not exist", aliasLocalPath)
con.PrintErrorf("Extension path '%s' does not exist", aliasLocalPath)
return
}
if !fi.IsDir() {
InstallFromFile(aliasLocalPath, "", false, con)
InstallFromFile(aliasLocalPath, false, con)
} else {
installFromDir(aliasLocalPath, con)
}
}
// Install an extension from a directory.
func installFromDir(aliasLocalPath string, con *console.SliverClient) {
// Install an extension from a directory
func installFromDir(aliasLocalPath string, con *console.SliverConsoleClient) {
manifestData, err := os.ReadFile(filepath.Join(aliasLocalPath, ManifestFileName))
if err != nil {
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
@@ -62,7 +63,8 @@ func installFromDir(aliasLocalPath string, con *console.SliverClient) {
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
con.PrintInfof("Alias '%s' already exists", manifest.CommandName)
confirm := false
_ = forms.Confirm("Overwrite current install?", &confirm)
prompt := &survey.Confirm{Message: "Overwrite current install?"}
survey.AskOne(prompt, &confirm)
if !confirm {
return
}
@@ -98,8 +100,8 @@ func installFromDir(aliasLocalPath string, con *console.SliverClient) {
con.Printf("done!\n")
}
// Install an extension from a .tar.gz file.
func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite bool, con *console.SliverClient) *string {
// Install an extension from a .tar.gz file
func InstallFromFile(aliasGzFilePath string, autoOverwrite bool, con *console.SliverConsoleClient) *string {
manifestData, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", ManifestFileName))
if err != nil {
con.PrintErrorf("Failed to read %s from '%s': %s\n", ManifestFileName, aliasGzFilePath, err)
@@ -107,21 +109,16 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
}
manifest, err := ParseAliasManifest(manifestData)
if err != nil {
errorMsg := ""
if aliasName != "" {
errorMsg = fmt.Sprintf("Error processing manifest for alias %s - failed to parse %s: %s\n", aliasName, ManifestFileName, err)
} else {
errorMsg = fmt.Sprintf("Failed to parse %s: %s\n", ManifestFileName, err)
}
con.PrintErrorf("%s", errorMsg)
con.PrintErrorf("Failed to parse %s: %s\n", ManifestFileName, err)
return nil
}
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.CommandName))
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
if promptToOverwrite {
if !autoOverwrite {
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
confirm := false
_ = forms.Confirm("Overwrite current install?", &confirm)
prompt := &survey.Confirm{Message: "Overwrite current install?"}
survey.AskOne(prompt, &confirm)
if !confirm {
return nil
}
@@ -143,7 +140,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
}
for _, aliasFile := range manifest.Files {
if aliasFile.Path != "" {
err := installArtifact(aliasGzFilePath, installPath, aliasFile.Path)
err := installArtifact(aliasGzFilePath, installPath, aliasFile.Path, con)
if err != nil {
con.PrintErrorf("Failed to install file: %s\n", err)
forceRemoveAll(installPath)
@@ -155,7 +152,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
return &installPath
}
func installArtifact(aliasGzFilePath string, installPath, artifactPath string) error {
func installArtifact(aliasGzFilePath string, installPath, artifactPath string, con *console.SliverConsoleClient) error {
data, err := util.ReadFileFromTarGz(aliasGzFilePath, fmt.Sprintf("./%s", strings.TrimPrefix(artifactPath, string(os.PathSeparator))))
if err != nil {
return err
+71 -317
View File
@@ -27,20 +27,19 @@ import (
"path/filepath"
"strings"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/client/packages"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/carapace-sh/carapace"
"github.com/AlecAivazis/survey/v2"
app "github.com/reeflective/console"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"google.golang.org/protobuf/proto"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
)
const (
@@ -54,40 +53,30 @@ const (
)
var (
// alias name -> manifest/command.
// alias name -> manifest/command
loadedAliases = map[string]*loadedAlias{}
defaultHostProc = map[string]string{
"windows": windowsDefaultHostProc,
"linux": linuxDefaultHostProc,
"linux": windowsDefaultHostProc,
"darwin": macosDefaultHostProc,
}
)
// Ties the manifest struct to the command struct.
// Ties the manifest struct to the command struct
type loadedAlias struct {
Manifest *AliasManifest
Command *cobra.Command
}
// AliasFile - An OS/Arch specific file.
// AliasFile - An OS/Arch specific file
type AliasFile struct {
OS string `json:"os"`
Arch string `json:"arch"`
Path string `json:"path"`
}
// AliasArgument - An argument for an alias command.
type AliasArgument struct {
Name string `json:"name"`
Type string `json:"type"`
Desc string `json:"desc"`
Optional bool `json:"optional"`
Default interface{} `json:"default,omitempty"`
Choices []string `json:"choices,omitempty"`
}
// AliasManifest - The manifest for an alias, contains metadata.
// AliasManifest - The manifest for an alias, contains metadata
type AliasManifest struct {
Name string `json:"name"`
Version string `json:"version"`
@@ -97,18 +86,14 @@ type AliasManifest struct {
Help string `json:"help"`
LongHelp string `json:"long_help"`
Entrypoint string `json:"entrypoint"`
AllowArgs bool `json:"allow_args"`
DefaultArgs string `json:"default_args"`
Arguments []*AliasArgument `json:"arguments"`
Files []*AliasFile `json:"files"`
IsReflective bool `json:"is_reflective"`
IsAssembly bool `json:"is_assembly"`
Schema *packages.OutputSchema `json:"schema"`
Entrypoint string `json:"entrypoint"`
AllowArgs bool `json:"allow_args"`
DefaultArgs string `json:"default_args"`
Files []*AliasFile `json:"files"`
IsReflective bool `json:"is_reflective"`
IsAssembly bool `json:"is_assembly"`
RootPath string `json:"-"`
ArmoryName string `json:"-"`
ArmoryPK string `json:"-"`
RootPath string `json:"-"`
}
func (ec *AliasManifest) getDefaultProcess(targetOS string) (proc string, err error) {
@@ -119,7 +104,7 @@ func (ec *AliasManifest) getDefaultProcess(targetOS string) (proc string, err er
return
}
func (a *AliasManifest) getFileForTarget(_ string, targetOS string, targetArch string) (string, error) {
func (a *AliasManifest) getFileForTarget(cmdName string, targetOS string, targetArch string) (string, error) {
filePath := ""
for _, extFile := range a.Files {
if targetOS == extFile.OS && targetArch == extFile.Arch {
@@ -139,7 +124,7 @@ func (a *AliasManifest) getFileForTarget(_ string, targetOS string, targetArch s
}
// AliasesLoadCmd - Locally load a alias into the Sliver shell.
func AliasesLoadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func AliasesLoadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
dirPath := args[0]
// dirPath := ctx.Args.String("dir-path")
alias, err := LoadAlias(dirPath, cmd.Root(), con)
@@ -150,8 +135,8 @@ func AliasesLoadCmd(cmd *cobra.Command, con *console.SliverClient, args []string
}
}
// LoadAlias - Load an alias into the Sliver shell from a given directory.
func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClient) (*AliasManifest, error) {
// LoadAlias - Load an alias into the Sliver shell from a given directory
func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverConsoleClient) (*AliasManifest, error) {
// retrieve alias manifest
var err error
manifestPath, err = filepath.Abs(manifestPath)
@@ -170,69 +155,21 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
}
aliasManifest.RootPath = filepath.Dir(manifestPath)
// Build usage string including arguments
usage := strings.Builder{}
usage.WriteString(aliasManifest.CommandName)
for _, arg := range aliasManifest.Arguments {
usage.WriteString(" ")
if arg.Optional {
usage.WriteString("[")
}
usage.WriteString(strings.ToUpper(arg.Name))
if arg.Optional {
usage.WriteString("]")
}
}
// Build long help message
longHelp := strings.Builder{}
longHelp.WriteString("[[.Bold]]Command:[[.Normal]]")
longHelp.WriteString(usage.String())
longHelp.WriteString("\n")
if len(aliasManifest.Help) > 0 || len(aliasManifest.LongHelp) > 0 {
longHelp.WriteString("[[.Bold]]About:[[.Normal]]")
if len(aliasManifest.Help) > 0 {
longHelp.WriteString(aliasManifest.Help)
longHelp.WriteString("\n")
}
if len(aliasManifest.LongHelp) > 0 {
longHelp.WriteString(aliasManifest.LongHelp)
longHelp.WriteString("\n")
}
}
if len(aliasManifest.Arguments) > 0 {
longHelp.WriteString("[[.Bold]]Arguments:[[.Normal]]")
for _, arg := range aliasManifest.Arguments {
longHelp.WriteString("\n\t")
optStr := ""
if arg.Optional {
optStr = "[OPTIONAL]"
}
aType := arg.Type
if aType == "wstring" {
aType = "string"
}
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
}
}
// for each alias command, add a new app command
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
longHelpMsg := help.FormatHelpTmpl(aliasManifest.LongHelp)
longHelpMsg += "\n\n⚠️ If you're having issues passing arguments to the alias please read:\n"
longHelpMsg += "https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing"
addAliasCmd := &cobra.Command{
Use: usage.String(),
Use: aliasManifest.CommandName,
Short: helpMsg,
Long: help.FormatHelpTmpl(longHelp.String()),
Long: longHelpMsg,
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 {
@@ -242,9 +179,9 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("app-domain", "d", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
f.StringP("arch", "a", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
f.BoolP("in-process", "i", false, "Run in the current sliver process")
f.StringP("runtime", "r", "v4.0.30319", "Runtime to use for running the assembly")
f.BoolP("amsi-bypass", "M", false, "Bypass AMSI on Windows")
f.BoolP("etw-bypass", "E", false, "Bypass ETW on Windows")
f.StringP("runtime", "r", "", "Runtime to use for running the assembly (only supported when used with --in-process)")
f.BoolP("amsi-bypass", "M", false, "Bypass AMSI on Windows (only supported when used with --in-process)")
f.BoolP("etw-bypass", "E", false, "Bypass ETW on Windows (only supported when used with --in-process)")
addAliasCmd.Flags().AddFlagSet(f)
}
@@ -253,13 +190,9 @@ 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
comps := carapace.Gen(addAliasCmd)
makeAliasArgCompleter(aliasManifest, comps)
cmd.AddCommand(addAliasCmd)
// Have to use a global map here, as passing the aliasCmd
@@ -272,7 +205,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
return aliasManifest, nil
}
// ParseAliasManifest - Parse an alias manifest.
// ParseAliasManifest - Parse an alias manifest
func ParseAliasManifest(data []byte) (*AliasManifest, error) {
// parse it
alias := &AliasManifest{}
@@ -305,17 +238,10 @@ func ParseAliasManifest(data []byte) (*AliasManifest, error) {
}
}
if alias.Schema != nil {
if !packages.IsValidSchemaType(alias.Schema.Name) {
return nil, fmt.Errorf("%s is not a valid schema type", alias.Schema.Name)
}
alias.Schema.IngestColumns()
}
return alias, nil
}
func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runAliasCommand(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
@@ -336,47 +262,26 @@ 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)
return
}
// args := ctx.Args.StringList("arguments")
var extArgsStr string
var extArgs string
if len(aliasManifest.DefaultArgs) != 0 && len(args) == 0 {
extArgsStr = aliasManifest.DefaultArgs
extArgs = aliasManifest.DefaultArgs
} else {
extArgsStr = strings.Join(args, " ")
extArgs = strings.Join(args, " ")
}
extArgsStr = strings.TrimSpace(extArgsStr)
extArgs = strings.TrimSpace(extArgs)
entryPoint := aliasManifest.Entrypoint
processArgsStr, _ := cmd.Flags().GetString("process-arguments")
// Special case for payloads with pass to Donut (.NET assemblies and sideloaded payloads):
// The Donut loader has a hard limit of 256 characters for the command line arguments, so
// we're alerting the user that the arguments will be truncated.
if len(extArgsStr) > 256 && (aliasManifest.IsAssembly || !aliasManifest.IsReflective) {
if len(extArgs) > 256 && (aliasManifest.IsAssembly || !aliasManifest.IsReflective) {
msgStr := ""
// The --in-process flag only exists for .NET assemblies (aliasManifest.IsAssembly == true).
// Groupping the two conditions together could crash the client since ctx.Flags.Type panics
@@ -396,9 +301,10 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
} else if !aliasManifest.IsReflective {
msgStr = " Arguments are limited to 256 characters when using the default fork/exec model for non-reflective PE payloads.\n"
}
con.PrintWarnf("%s", msgStr)
con.PrintWarnf(msgStr)
confirm := false
forms.Confirm("Do you want to continue?", &confirm)
prompt := &survey.Confirm{Message: "Do you want to continue?"}
survey.AskOne(prompt, &confirm, nil)
if !confirm {
return
}
@@ -424,13 +330,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)
@@ -453,13 +353,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
// Execute Assembly
ctrl := make(chan bool)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
con.SpinUntil(msg, ctrl)
executeAssemblyResp, err := con.Rpc.ExecuteAssembly(context.Background(), &sliverpb.ExecuteAssemblyReq{
Request: con.ActiveTarget.Request(cmd),
IsDLL: isDLL,
Process: processName,
Arguments: args,
Arguments: extArgs,
Assembly: binData,
Arch: arch,
Method: method,
@@ -486,11 +386,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
con.PrintErrorf("Failed to decode call ext response %s\n", err)
return
}
PrintAssemblyOutput(cmd.Name(), aliasManifest.Schema, executeAssemblyResp, outFilePath, con)
PrintAssemblyOutput(cmd.Name(), executeAssemblyResp, outFilePath, con)
})
con.PrintAsyncResponse(executeAssemblyResp.Response)
} else {
PrintAssemblyOutput(cmd.Name(), aliasManifest.Schema, executeAssemblyResp, outFilePath, con)
PrintAssemblyOutput(cmd.Name(), executeAssemblyResp, outFilePath, con)
}
} else if aliasManifest.IsReflective {
@@ -499,11 +399,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
// Spawn DLL
ctrl := make(chan bool)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
con.SpinUntil(msg, ctrl)
spawnDllResp, err := con.Rpc.SpawnDll(context.Background(), &sliverpb.InvokeSpawnDllReq{
Request: con.ActiveTarget.Request(cmd),
Args: args,
Args: strings.Trim(extArgs, " "),
Data: binData,
ProcessName: processName,
EntryPoint: aliasManifest.Entrypoint,
@@ -525,11 +425,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
con.PrintErrorf("Failed to decode call ext response %s\n", err)
return
}
PrintSpawnDLLOutput(cmd.Name(), aliasManifest.Schema, spawnDllResp, outFilePath, con)
PrintSpawnDLLOutput(cmd.Name(), spawnDllResp, outFilePath, con)
})
con.PrintAsyncResponse(spawnDllResp.Response)
} else {
PrintSpawnDLLOutput(cmd.Name(), aliasManifest.Schema, spawnDllResp, outFilePath, con)
PrintSpawnDLLOutput(cmd.Name(), spawnDllResp, outFilePath, con)
}
} else {
@@ -538,11 +438,11 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
// Sideload
ctrl := make(chan bool)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgsStr)
msg := fmt.Sprintf("Executing %s %s ...", cmd.Name(), extArgs)
con.SpinUntil(msg, ctrl)
sideloadResp, err := con.Rpc.Sideload(context.Background(), &sliverpb.SideloadReq{
Request: con.ActiveTarget.Request(cmd),
Args: args,
Args: extArgs,
Data: binData,
EntryPoint: entryPoint,
ProcessName: processName,
@@ -565,80 +465,36 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
con.PrintErrorf("Failed to decode call ext response %s\n", err)
return
}
PrintSideloadOutput(cmd.Name(), aliasManifest.Schema, sideloadResp, outFilePath, con)
PrintSideloadOutput(cmd.Name(), sideloadResp, outFilePath, con)
})
con.PrintAsyncResponse(sideloadResp.Response)
} else {
PrintSideloadOutput(cmd.Name(), aliasManifest.Schema, sideloadResp, outFilePath, con)
PrintSideloadOutput(cmd.Name(), sideloadResp, outFilePath, con)
}
}
}
func getOutputWithSchema(schema *packages.OutputSchema, result string) string {
if schema == nil {
return result
}
outputSchema := packages.GetNewPackageOutput(schema.Name)
if outputSchema == nil {
return result
}
err := outputSchema.IngestData([]byte(result), schema.Columns(), schema.GroupBy)
if err != nil {
return result
}
return outputSchema.CreateTable()
}
// PrintSpawnDLLOutput - Prints the output of a spawn dll command.
func PrintSpawnDLLOutput(cmdName string, schema *packages.OutputSchema, spawnDllResp *sliverpb.SpawnDll, outFilePath *os.File, con *console.SliverClient) {
var result string
if schema != nil {
result = getOutputWithSchema(schema, spawnDllResp.GetResult())
} else {
result = spawnDllResp.GetResult()
}
con.PrintInfof("%s output:\n%s", cmdName, result)
// Output the raw result to the file
// PrintSpawnDLLOutput - Prints the output of a spawn dll command
func PrintSpawnDLLOutput(cmdName string, spawnDllResp *sliverpb.SpawnDll, outFilePath *os.File, con *console.SliverConsoleClient) {
con.PrintInfof("%s output:\n%s", cmdName, spawnDllResp.GetResult())
if outFilePath != nil {
outFilePath.WriteString(spawnDllResp.GetResult())
outFilePath.Write([]byte(spawnDllResp.GetResult()))
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
}
}
// PrintSideloadOutput - Prints the output of a sideload command.
func PrintSideloadOutput(cmdName string, schema *packages.OutputSchema, sideloadResp *sliverpb.Sideload, outFilePath *os.File, con *console.SliverClient) {
var result string
if schema != nil {
result = getOutputWithSchema(schema, sideloadResp.GetResult())
} else {
result = sideloadResp.GetResult()
}
con.PrintInfof("%s output:\n%s", cmdName, result)
// Output the raw result to the file
// PrintSideloadOutput - Prints the output of a sideload command
func PrintSideloadOutput(cmdName string, sideloadResp *sliverpb.Sideload, outFilePath *os.File, con *console.SliverConsoleClient) {
con.PrintInfof("%s output:\n%s", cmdName, sideloadResp.GetResult())
if outFilePath != nil {
outFilePath.WriteString(sideloadResp.GetResult())
outFilePath.Write([]byte(sideloadResp.GetResult()))
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
}
}
// PrintAssemblyOutput - Prints the output of an execute-assembly command.
func PrintAssemblyOutput(cmdName string, schema *packages.OutputSchema, execAsmResp *sliverpb.ExecuteAssembly, outFilePath *os.File, con *console.SliverClient) {
var result string
if schema != nil {
result = getOutputWithSchema(schema, string(execAsmResp.GetOutput()))
} else {
result = string(execAsmResp.GetOutput())
}
con.PrintInfof("%s output:\n%s", cmdName, result)
// Output the raw result to the file
// PrintAssemblyOutput - Prints the output of an execute-assembly command
func PrintAssemblyOutput(cmdName string, execAsmResp *sliverpb.ExecuteAssembly, outFilePath *os.File, con *console.SliverConsoleClient) {
con.PrintInfof("%s output:\n%s", cmdName, string(execAsmResp.GetOutput()))
if outFilePath != nil {
outFilePath.Write(execAsmResp.GetOutput())
con.PrintInfof("Output saved to %s\n", outFilePath.Name())
@@ -680,105 +536,3 @@ func makeAliasPlatformFilters(alias *AliasManifest) map[string]string {
app.CommandFilterKey: strings.Join(all, ","),
}
}
// makeAliasArgCompleter builds the positional and dash arguments completer for the alias.
// It provides completion for:
// 1. Positional arguments (before --)
// 2. Flag-style arguments after -- (e.g., --target, --port)
func makeAliasArgCompleter(alias *AliasManifest, comps *carapace.Carapace) {
if len(alias.Arguments) == 0 {
return
}
var actions []carapace.Action
for _, arg := range alias.Arguments {
var action carapace.Action
// If choices are defined, use them for completion
if len(arg.Choices) > 0 {
action = carapace.ActionValues(arg.Choices...).Tag("choices")
} else {
// Fall back to type-based completion
switch arg.Type {
case "file":
action = carapace.ActionFiles().Tag("alias data")
default:
action = carapace.ActionValues()
}
}
usage := fmt.Sprintf("(%s) %s", arg.Type, arg.Desc)
if arg.Optional {
usage += " (optional)"
}
actions = append(actions, action.Usage("%s", usage))
}
comps.PositionalCompletion(actions...)
// Add dash completion for flag-style arguments after --
// Pre-build the flag completions at registration time (not in a callback)
flagCompletion := makeAliasFlagNameCompletion(alias)
// Build value completions for each argument type
valueCompletions := make(map[string]carapace.Action)
for _, arg := range alias.Arguments {
// If choices are defined, use them for completion
if len(arg.Choices) > 0 {
valueCompletions[arg.Name] = carapace.ActionValues(arg.Choices...).Tag("choices")
} else {
// Fall back to type-based completion
switch arg.Type {
case "file":
valueCompletions[arg.Name] = carapace.ActionFiles().Tag("file path")
case "bool":
valueCompletions[arg.Name] = carapace.ActionValues("true", "false").Tag("boolean")
default:
valueCompletions[arg.Name] = carapace.ActionValues()
}
}
}
// Use DashAnyCompletion with a smart action that determines context
comps.DashAnyCompletion(
carapace.ActionCallback(func(c carapace.Context) carapace.Action {
// If typing a flag (starts with -)
if strings.HasPrefix(c.Value, "-") {
return flagCompletion
}
// If previous arg was a flag, complete its value
if len(c.Args) > 0 {
lastArg := c.Args[len(c.Args)-1]
if strings.HasPrefix(lastArg, "-") {
flagName := strings.TrimLeft(lastArg, "-")
if action, ok := valueCompletions[flagName]; ok {
return action
}
}
}
// Default: show flag names
return flagCompletion
}),
)
}
// makeAliasFlagNameCompletion creates completion for flag names
func makeAliasFlagNameCompletion(alias *AliasManifest) carapace.Action {
var results []string
for _, arg := range alias.Arguments {
flagName := fmt.Sprintf("--%s", arg.Name)
desc := arg.Desc
if arg.Optional {
desc = fmt.Sprintf("[optional] %s", desc)
}
desc = fmt.Sprintf("(%s) %s", arg.Type, desc)
results = append(results, flagName, desc)
}
return carapace.ActionValuesDescribed(results...).Tag("alias arguments")
}
+8 -6
View File
@@ -24,14 +24,15 @@ import (
"os"
"path/filepath"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/spf13/cobra"
)
// AliasesRemoveCmd - Locally load a alias into the Sliver shell.
func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
name := args[0]
// name := ctx.Args.String("name")
if name == "" {
@@ -39,7 +40,8 @@ func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
return
}
confirm := false
_ = forms.Confirm(fmt.Sprintf("Remove '%s' alias?", name), &confirm)
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' alias?", name)}
survey.AskOne(prompt, &confirm)
if !confirm {
return
}
@@ -52,8 +54,8 @@ func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
}
}
// RemoveAliasByCommandName - Remove an alias by command name.
func RemoveAliasByCommandName(commandName string, con *console.SliverClient) error {
// RemoveAliasByCommandName - Remove an alias by command name
func RemoveAliasByCommandName(commandName string, con *console.SliverConsoleClient) error {
if commandName == "" {
return errors.New("command name is required")
}
-16
View File
@@ -1,16 +0,0 @@
# client/command/armory
## Overview
Implements the 'armory' command group for the Sliver client console. Handlers map Cobra invocations to armory workflows such as install, manage, parsers, and search.
## Go Files
- `armory.go` – Maintains the armory cache lifecycle, fetching package metadata, verifying signatures, and presenting bundle listings.
- `armory_test.go` *(tests)* – Placeholder for armory command tests; currently used to keep the package's test harness wired up.
- `commands.go` – Declares the Cobra command hierarchy for armory actions and binds shared flag/completion plumbing.
- `install.go` – Resolves dependencies and installs aliases or extensions from cached metadata, prompting when overwriting existing packages.
- `manage.go` – Implements management helpers for viewing, adding, verifying, and persisting armory configurations.
- `parsers.go` – Provides parsers and HTTP helpers for retrieving armory indexes and packages, including Minisign verification.
- `search.go` – Implements the regex-driven search command that filters cached armory packages.
- `update.go` – Calculates available package updates, presents options to the user, and orchestrates update installation.
+87 -377
View File
@@ -23,7 +23,6 @@ import (
"errors"
"fmt"
"net/url"
"slices"
"sync"
"time"
@@ -55,24 +54,13 @@ type ArmoryPackage struct {
RepoURL string `json:"repo_url"`
PublicKey string `json:"public_key"`
IsAlias bool `json:"-"`
ArmoryName string `json:"-"`
/*
With support for multiple armories, the command name of a package
is not unique anymore, so we need something that is unique
to be able to keep track of packages.
This ID will be a hash calculated from properties of the package.
*/
ID string `json:"-"`
ArmoryPK string `json:"-"`
IsAlias bool `json:"-"`
}
// ArmoryBundle - A list of packages
type ArmoryBundle struct {
Name string `json:"name"`
Packages []string `json:"packages"`
ArmoryName string `json:"-"`
Name string `json:"name"`
Packages []string `json:"packages"`
}
// ArmoryHTTPConfig - Configuration for armory HTTP client
@@ -101,75 +89,48 @@ type pkgCacheEntry struct {
Alias *alias.AliasManifest
Extension *extensions.ExtensionManifest
LastErr error
// This corresponds to Pkg.ID
ID string
}
var (
// public key -> armoryCacheEntry
indexCache = sync.Map{}
// package ID -> armoryPkgCacheEntry
// public key -> armoryPkgCacheEntry
pkgCache = sync.Map{}
// public key -> assets.ArmoryConfig
currentArmories = sync.Map{}
// cacheTime - How long to cache the index/pkg manifests
//cacheTime = time.Hour
cacheTime = 2 * time.Minute
cacheTime = time.Hour
// This will kill a download if exceeded so needs to be large
defaultTimeout = 15 * time.Minute
// Track whether armories have been initialized so that we know if we need to pull from the config
armoriesInitialized = false
// Track whether the default armory has been removed by the user (this is needed to prevent it from being readded in if they have removed it)
defaultArmoryRemoved = false
)
// ArmoryCmd - The main armory command
func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
armoriesConfig := getCurrentArmoryConfiguration()
if len(armoriesConfig) == 1 {
con.Printf("Reading armory index ... ")
} else {
con.PrintInfof("Reading %d armory indexes ... ", len(armoriesConfig))
}
func ArmoryCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
armoriesConfig := assets.GetArmoriesConfig()
con.PrintInfof("Fetching %d armory index(es) ... ", len(armoriesConfig))
clientConfig := parseArmoryHTTPConfig(cmd)
indexes := fetchIndexes(clientConfig)
indexes := fetchIndexes(armoriesConfig, clientConfig)
if len(indexes) != len(armoriesConfig) {
con.Printf("errors!\n")
indexCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(indexCacheEntry)
if cacheEntry.LastErr != nil {
con.PrintErrorf("%s (%s) - %s\n", cacheEntry.ArmoryConfig.Name, cacheEntry.RepoURL, cacheEntry.LastErr)
con.PrintErrorf("%s - %s\n", cacheEntry.RepoURL, cacheEntry.LastErr)
}
return true
})
} else {
con.Printf("done!\n")
}
armoriesInitialized = true
if len(indexes) == 0 {
con.PrintInfof("No indexes found\n")
return
}
aliases := []*alias.AliasManifest{}
exts := []*extensions.ExtensionManifest{}
for _, index := range indexes {
if 0 < len(indexes) {
con.PrintInfof("Fetching package information ... ")
fetchPackageSignatures(indexes, clientConfig)
errorCount := 0
con.PrintInfof("Reading package information for armory %s ... ", index.ArmoryConfig.Name)
fetchPackageSignatures(index, clientConfig)
aliases := []*alias.AliasManifest{}
exts := []*extensions.ExtensionManifest{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Something is wrong with this entry
pkgCache.Delete(key)
return true
}
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
return true
}
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.LastErr != nil {
errorCount++
if errorCount == 0 {
@@ -188,52 +149,32 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
if errorCount == 0 {
con.Printf("done!\n")
}
}
if 0 < len(aliases) || 0 < len(exts) {
if 0 < len(aliases) || 0 < len(exts) {
con.Println()
PrintArmoryPackages(aliases, exts, con)
} else {
con.PrintInfof("No packages found\n")
}
con.Println()
PrintArmoryPackages(aliases, exts, con)
bundles := bundlesInCache()
if 0 < len(bundles) {
PrintArmoryBundles(bundles, con)
} else {
con.PrintInfof("No bundles found\n")
}
} else {
con.PrintInfof("No packages found\n")
}
con.Println()
bundles := bundlesInCache()
if 0 < len(bundles) {
PrintArmoryBundles(bundles, con)
} else {
con.PrintInfof("No bundles found\n")
con.PrintInfof("No indexes found\n")
}
}
func refresh(clientConfig ArmoryHTTPConfig) {
getCurrentArmoryConfiguration()
indexes := fetchIndexes(clientConfig)
for _, index := range indexes {
fetchPackageSignatures(index, clientConfig)
}
armoriesConfig := assets.GetArmoriesConfig()
indexes := fetchIndexes(armoriesConfig, clientConfig)
fetchPackageSignatures(indexes, clientConfig)
}
func countUniqueCommandsFromManifests(aliases []*alias.AliasManifest, exts []*extensions.ExtensionManifest) (int, int) {
uniqueAliasNames := []string{}
uniqueExtensionNames := []string{}
for _, alias := range aliases {
if !slices.Contains(uniqueAliasNames, alias.CommandName) {
uniqueAliasNames = append(uniqueAliasNames, alias.CommandName)
}
}
for _, ext := range exts {
for _, cmd := range ext.ExtCommand {
if !slices.Contains(uniqueExtensionNames, cmd.CommandName) {
uniqueExtensionNames = append(uniqueExtensionNames, cmd.CommandName)
}
}
}
return len(uniqueAliasNames), len(uniqueExtensionNames)
}
func packageManifestsInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest) {
func packagesInCache() ([]*alias.AliasManifest, []*extensions.ExtensionManifest) {
aliases := []*alias.AliasManifest{}
exts := []*extensions.ExtensionManifest{}
pkgCache.Range(func(key, value interface{}) bool {
@@ -250,125 +191,6 @@ func packageManifestsInCache() ([]*alias.AliasManifest, []*extensions.ExtensionM
return aliases, exts
}
func armoryLookupByName(name string) *assets.ArmoryConfig {
var result *assets.ArmoryConfig
indexCache.Range(func(key, value interface{}) bool {
indexEntry, ok := value.(indexCacheEntry)
if !ok {
// Keep going
return true
}
if indexEntry.ArmoryConfig.Name == name {
result = indexEntry.ArmoryConfig
return false
}
return true
})
return result
}
// Returns the packages in the cache with a given name
func packageCacheLookupByName(name string) []*pkgCacheEntry {
var result []*pkgCacheEntry = make([]*pkgCacheEntry, 0)
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Keep going
return true
}
if cacheEntry.Pkg.Name == name {
result = append(result, &cacheEntry)
}
return true
})
return result
}
// Returns the packages in the cache for a given command name
func packageCacheLookupByCmd(commandName string) []*pkgCacheEntry {
var result []*pkgCacheEntry = make([]*pkgCacheEntry, 0)
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Keep going
return true
}
if cacheEntry.Pkg.CommandName == commandName {
result = append(result, &cacheEntry)
}
return true
})
return result
}
// Returns the package in the cache for a given command name and armory
func packageCacheLookupByCmdAndArmory(commandName string, armoryPublicKey string) *pkgCacheEntry {
var result *pkgCacheEntry
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Keep going
return true
}
if cacheEntry.ArmoryConfig.PublicKey == armoryPublicKey && cacheEntry.Pkg.CommandName == commandName {
result = &cacheEntry
// Stop iterating
return false
}
return true
})
return result
}
// Returns the package hashes in the cache for a given armory
func packageHashLookupByArmory(armoryPublicKey string) []string {
result := []string{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Keep going
return true
}
if cacheEntry.ArmoryConfig.PublicKey == armoryPublicKey {
result = append(result, cacheEntry.ID)
}
return true
})
return result
}
func packageCacheLookupByID(packageID string) *pkgCacheEntry {
var packageEntry *pkgCacheEntry
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Keep going
return true
}
if cacheEntry.LastErr == nil {
if cacheEntry.ID == packageID {
packageEntry = &cacheEntry
// Stop iterating
return false
}
}
return true
})
return packageEntry
}
func bundlesInCache() []*ArmoryBundle {
bundles := []*ArmoryBundle{}
indexCache.Range(func(key, value interface{}) bool {
@@ -385,7 +207,7 @@ func AliasExtensionOrBundleCompleter() carapace.Action {
var action carapace.Action
results := []string{}
aliases, exts := packageManifestsInCache()
aliases, exts := packagesInCache()
bundles := bundlesInCache()
for _, aliasPkg := range aliases {
@@ -420,7 +242,7 @@ func AliasExtensionOrBundleCompleter() carapace.Action {
}
// PrintArmoryPackages - Prints the armory packages
func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.ExtensionManifest, con *console.SliverClient) {
func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.ExtensionManifest, con *console.SliverConsoleClient) {
width, _, err := term.GetSize(0)
if err != nil {
width = 1
@@ -428,13 +250,12 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.SetTitle(console.StyleBold.Render("Packages"))
tw.SetTitle(console.Bold + "Packages" + console.Normal)
urlMargin := 150 // Extra margin needed to show URL column
if con.Settings.SmallTermWidth+urlMargin < width {
tw.AppendHeader(table.Row{
"Armory",
"Command Name",
"Version",
"Type",
@@ -443,7 +264,6 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
})
} else {
tw.AppendHeader(table.Row{
"Armory",
"Command Name",
"Version",
"Type",
@@ -453,11 +273,10 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
// Columns start at 1 for some dumb reason
tw.SortBy([]table.SortBy{
{Number: 2, Mode: table.Asc},
{Number: 1, Mode: table.Asc},
})
type pkgInfo struct {
ArmoryName string
CommandName string
Version string
Type string
@@ -465,10 +284,8 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
URL string
}
entries := []pkgInfo{}
for _, aliasPkg := range aliases {
entries = append(entries, pkgInfo{
ArmoryName: aliasPkg.ArmoryName,
CommandName: aliasPkg.CommandName,
Version: aliasPkg.Version,
Type: "Alias",
@@ -479,7 +296,6 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
for _, extm := range exts {
for _, extension := range extm.ExtCommand {
entries = append(entries, pkgInfo{
ArmoryName: extm.ArmoryName,
CommandName: extension.CommandName,
Version: extension.Manifest.Version,
Type: "Extension",
@@ -493,26 +309,24 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
rows := []table.Row{}
for _, pkg := range entries {
style := console.StyleNormal
color := console.Normal
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
style = console.StyleGreen
color = console.Green
}
if con.Settings.SmallTermWidth+urlMargin < width {
rows = append(rows, table.Row{
style.Render(pkg.ArmoryName),
style.Render(pkg.CommandName),
style.Render(pkg.Version),
style.Render(pkg.Type),
style.Render(pkg.Help),
style.Render(pkg.URL),
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
fmt.Sprintf(color+"%s"+console.Normal, pkg.URL),
})
} else {
rows = append(rows, table.Row{
style.Render(pkg.ArmoryName),
style.Render(pkg.CommandName),
style.Render(pkg.Version),
style.Render(pkg.Type),
style.Render(pkg.Help),
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
})
}
}
@@ -521,12 +335,11 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
}
// PrintArmoryBundles - Prints the armory bundles
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverConsoleClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.SetTitle(console.StyleBold.Render("Bundles"))
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
tw.AppendHeader(table.Row{
"Armory Name",
"Name",
"Contains",
})
@@ -552,7 +365,6 @@ func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
}
}
tw.AppendRow(table.Row{
bundle.ArmoryName,
bundle.Name,
packages,
})
@@ -590,19 +402,12 @@ func parseArmoryHTTPConfig(cmd *cobra.Command) ArmoryHTTPConfig {
// fetch armory indexes, only returns indexes that were fetched successfully
// errors are still in the cache objects however and can be checked
func fetchIndexes(clientConfig ArmoryHTTPConfig) []ArmoryIndex {
func fetchIndexes(armoryConfigs []*assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) []ArmoryIndex {
wg := &sync.WaitGroup{}
// Try to get a max of 10 indexes at a time
currentRequests := make(chan struct{}, 10)
currentArmories.Range(func(key, value interface{}) bool {
armoryEntry := value.(assets.ArmoryConfig)
if armoryEntry.Enabled {
wg.Add(1)
currentRequests <- struct{}{}
go fetchIndex(&armoryEntry, currentRequests, clientConfig, wg)
}
return true
})
for _, armoryConfig := range armoryConfigs {
wg.Add(1)
go fetchIndex(armoryConfig, clientConfig, wg)
}
wg.Wait()
indexes := []ArmoryIndex{}
indexCache.Range(func(key, value interface{}) bool {
@@ -615,19 +420,13 @@ func fetchIndexes(clientConfig ArmoryHTTPConfig) []ArmoryIndex {
return indexes
}
func fetchIndex(armoryConfig *assets.ArmoryConfig, requestChannel chan struct{}, clientConfig ArmoryHTTPConfig, wg *sync.WaitGroup) {
func fetchIndex(armoryConfig *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig, wg *sync.WaitGroup) {
defer wg.Done()
defer func() {
<-requestChannel
}()
cacheEntry, ok := indexCache.Load(armoryConfig.PublicKey)
if ok {
cached := cacheEntry.(indexCacheEntry)
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
return
} else if time.Since(cached.Fetched) >= cacheTime {
// If an index has gone stale, remove it from the index cache
indexCache.Delete(armoryConfig.PublicKey)
}
}
@@ -664,95 +463,40 @@ func fetchIndex(armoryConfig *assets.ArmoryConfig, requestChannel chan struct{},
}
}
func calculateHashesForIndex(index ArmoryIndex) []string {
result := []string{}
for _, pkg := range index.Aliases {
result = append(result, calculatePackageHash(pkg))
}
for _, pkg := range index.Extensions {
result = append(result, calculatePackageHash(pkg))
}
return result
}
func makePackageCacheConsistent(index ArmoryIndex) {
packagesToRemove := []string{}
// Get the packages for the armory out of the cache
cacheHashesForArmory := packageHashLookupByArmory(index.ArmoryConfig.PublicKey)
indexHashesForArmory := calculateHashesForIndex(index)
if len(cacheHashesForArmory) > len(indexHashesForArmory) {
// Then there are packages in the cache that do not exist in the armory
if len(indexHashesForArmory) == 0 {
packagesToRemove = cacheHashesForArmory
} else {
for _, packageHash := range indexHashesForArmory {
if !slices.Contains(cacheHashesForArmory, packageHash) {
packagesToRemove = append(packagesToRemove, packageHash)
}
}
func fetchPackageSignatures(indexes []ArmoryIndex, clientConfig ArmoryHTTPConfig) {
wg := &sync.WaitGroup{}
for _, index := range indexes {
for _, armoryPkg := range index.Extensions {
wg.Add(1)
armoryPkg.IsAlias = false
go fetchPackageSignature(wg, index.ArmoryConfig, armoryPkg, clientConfig)
}
for _, armoryPkg := range index.Aliases {
wg.Add(1)
armoryPkg.IsAlias = true
go fetchPackageSignature(wg, index.ArmoryConfig, armoryPkg, clientConfig)
}
}
// The remaining case of there being packages in the armory that do not exist in the cache
// will have to be solved with fetchPackageSignatures, and that function calls this one
// after fetching signatures and storing them in the cache, so that case should not apply
for _, packageHash := range packagesToRemove {
pkgCache.Delete(packageHash)
}
}
func fetchPackageSignatures(index ArmoryIndex, clientConfig ArmoryHTTPConfig) {
wg := &sync.WaitGroup{}
// Be kind to armories and limit concurrent requests to 10
// This is an arbritrary number and we may have to tweak it if it causes problems
currentRequests := make(chan struct{}, 10)
for _, armoryPkg := range index.Extensions {
wg.Add(1)
currentRequests <- struct{}{}
armoryPkg.IsAlias = false
go fetchPackageSignature(wg, currentRequests, index.ArmoryConfig, armoryPkg, clientConfig)
}
for _, armoryPkg := range index.Aliases {
wg.Add(1)
currentRequests <- struct{}{}
armoryPkg.IsAlias = true
go fetchPackageSignature(wg, currentRequests, index.ArmoryConfig, armoryPkg, clientConfig)
}
wg.Wait()
// If packages were deleted from the index, make sure the cache is consistent
makePackageCacheConsistent(index)
}
func fetchPackageSignature(wg *sync.WaitGroup, requestChannel chan struct{}, armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) {
func fetchPackageSignature(wg *sync.WaitGroup, armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, clientConfig ArmoryHTTPConfig) {
defer wg.Done()
defer func() {
<-requestChannel
}()
cacheEntry, ok := pkgCache.Load(armoryPkg.ID)
cacheEntry, ok := pkgCache.Load(armoryPkg.CommandName)
if ok {
cached := cacheEntry.(pkgCacheEntry)
if time.Since(cached.Fetched) < cacheTime && cached.LastErr == nil && !clientConfig.IgnoreCache {
return
} else if time.Since(cached.Fetched) >= cacheTime {
// If a package has gone stale, remove it from the package cache
pkgCache.Delete(armoryPkg.ID)
}
}
pkgCacheEntry := &pkgCacheEntry{
ArmoryConfig: armoryConfig,
RepoURL: armoryPkg.RepoURL,
ID: armoryPkg.ID,
}
defer func() {
pkgCacheEntry.Fetched = time.Now()
pkgCache.Store(armoryPkg.ID, *pkgCacheEntry)
pkgCache.Store(armoryPkg.CommandName, *pkgCacheEntry)
}()
repoURL, err := url.Parse(armoryPkg.RepoURL)
@@ -784,53 +528,19 @@ func fetchPackageSignature(wg *sync.WaitGroup, requestChannel chan struct{}, arm
if armoryPkg != nil {
pkgCacheEntry.Pkg = *armoryPkg
}
manifestData, err := base64.StdEncoding.DecodeString(sig.TrustedComment)
if err != nil {
pkgCacheEntry.LastErr = fmt.Errorf("failed to b64 decode trusted comment: %s", err)
return
}
if armoryPkg.IsAlias {
pkgCacheEntry.Alias, err = alias.ParseAliasManifest(manifestData)
pkgCacheEntry.Alias.ArmoryName = armoryConfig.Name
pkgCacheEntry.Alias.ArmoryPK = armoryConfig.PublicKey
} else {
pkgCacheEntry.Extension, err = extensions.ParseExtensionManifest(manifestData)
pkgCacheEntry.Extension.ArmoryName = armoryConfig.Name
pkgCacheEntry.Extension.ArmoryPK = armoryConfig.PublicKey
}
if err != nil {
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse trusted manifest in pkg signature: %s", err)
}
}
func clearAllCaches() {
currentArmories.Range(func(key, value any) bool {
currentArmories.Delete(key)
return true
})
indexCache.Range(func(key, value any) bool {
indexCache.Delete(key)
return true
})
pkgCache.Range(func(key, value any) bool {
pkgCache.Delete(key)
return true
})
}
func getArmoryPublicKey(armoryName string) string {
// Find PK for the armory name
armoryPK := ""
currentArmories.Range(func(key, value any) bool {
armoryEntry := value.(assets.ArmoryConfig)
if armoryEntry.Name == armoryName {
armoryPK = armoryEntry.PublicKey
return false
if err == nil {
manifestData, err := base64.StdEncoding.DecodeString(sig.TrustedComment)
if err != nil {
pkgCacheEntry.LastErr = fmt.Errorf("failed to b64 decode trusted comment: %s", err)
return
}
return true
})
return armoryPK
if armoryPkg.IsAlias {
pkgCacheEntry.Alias, err = alias.ParseAliasManifest(manifestData)
} else {
pkgCacheEntry.Extension, err = extensions.ParseExtensionManifest(manifestData)
}
if err != nil {
pkgCacheEntry.LastErr = fmt.Errorf("failed to parse trusted manifest in pkg signature: %s", err)
}
}
}
-229
View File
@@ -1,229 +0,0 @@
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"
"github.com/spf13/pflag"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
)
// Commands returns the `armory` command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
armoryCmd := &cobra.Command{
Use: consts.ArmoryStr,
Short: "Automatically download and install extensions/aliases",
Long: help.GetHelpFor([]string{consts.ArmoryStr}),
Run: func(cmd *cobra.Command, args []string) {
ArmoryCmd(cmd, con, args)
},
GroupID: consts.GenericHelpGroup,
}
flags.Bind("connection", true, armoryCmd, func(f *pflag.FlagSet) {
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.StringP("proxy", "p", "", "specify a proxy url (e.g. http://localhost:8080)")
f.BoolP("ignore-cache", "c", false, "ignore metadata cache, force refresh")
f.StringP("timeout", "t", "15m", "download timeout")
})
flags.BindFlagCompletions(armoryCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
})
armoryInstallCmd := &cobra.Command{
Use: consts.InstallStr,
Short: "Install an alias or extension",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.InstallStr}),
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
ArmoryInstallCmd(cmd, con, args)
},
}
flags.Bind("", false, armoryInstallCmd, func(f *pflag.FlagSet) {
f.BoolP("force", "f", false, "force installation of package, overwriting the package if it exists")
f.StringP("armory", "a", "", "name of armory to install package from")
})
armoryCmd.AddCommand(armoryInstallCmd)
flags.NewCompletions(armoryInstallCmd).PositionalCompletion(
AliasExtensionOrBundleCompleter().Usage("name of the extension or alias to install"),
)
armoryUpdateCmd := &cobra.Command{
Use: consts.UpdateStr,
Short: "Update installed aliases and extensions",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.UpdateStr}),
Run: func(cmd *cobra.Command, args []string) {
ArmoryUpdateCmd(cmd, con, args)
},
}
flags.Bind("", false, armoryUpdateCmd, func(f *pflag.FlagSet) {
f.StringP("armory", "a", "", "name of armory to get updates from")
})
armoryCmd.AddCommand(armoryUpdateCmd)
armorySearchCmd := &cobra.Command{
Use: consts.SearchStr,
Short: "Search for aliases and extensions by name (regex)",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.SearchStr}),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ArmorySearchCmd(cmd, con, args)
},
}
armoryCmd.AddCommand(armorySearchCmd)
flags.NewCompletions(armorySearchCmd).PositionalCompletion(carapace.ActionValues().Usage("a name regular expression"))
armoryInfoCmd := &cobra.Command{
Use: consts.InfoStr,
Short: "View configured armories or details about a specific armory or package",
Args: cobra.RangeArgs(0, 1),
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.InfoStr}),
Run: func(cmd *cobra.Command, args []string) {
ArmoryInfoCommand(cmd, con, args)
},
}
armoryCmd.AddCommand(armoryInfoCmd)
carapace.Gen(armoryInfoCmd).PositionalCompletion(
carapace.ActionValues().Usage("armory or package name"),
)
armoryAddCmd := &cobra.Command{
Use: consts.AddStr,
Short: "Add a new armory",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.AddStr}),
Args: cobra.ExactArgs(1), // the name of the armory
Run: func(cmd *cobra.Command, args []string) {
AddArmoryCmd(cmd, con, args)
},
}
flags.Bind("", false, armoryAddCmd, func(f *pflag.FlagSet) {
f.StringP("url", "u", "", "The URL of the armory index (required)")
f.StringP("pubkey", "k", "", "The public key for the armory (required)")
f.StringP("auth", "a", "", "Authorization details / credentials for the armory")
f.StringP("authcmd", "x", "", "Local command to run for authorization to the armory")
f.BoolP("no-save", "e", false, "Do not save this armory configuration to disk")
})
armoryAddCmd.MarkFlagRequired("url")
armoryAddCmd.MarkFlagRequired("pubkey")
carapace.Gen(armoryAddCmd).PositionalCompletion(
carapace.ActionValues().Usage("name of the armory"),
)
armoryCmd.AddCommand(armoryAddCmd)
armoryRemoveCmd := &cobra.Command{
Use: consts.RmStr,
Short: "Remove an armory",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.RmStr}),
Args: cobra.ExactArgs(1), // the name of the armory
Run: func(cmd *cobra.Command, args []string) {
RemoveArmoryCmd(cmd, con, args)
},
}
carapace.Gen(armoryAddCmd).PositionalCompletion(
carapace.ActionValues().Usage("name of the armory"),
)
armoryCmd.AddCommand(armoryRemoveCmd)
armorySaveCmd := &cobra.Command{
Use: consts.SaveStr,
Short: "Save the armory configuration to disk",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.SaveStr}),
Run: func(cmd *cobra.Command, args []string) {
SaveArmories(con)
},
}
armoryCmd.AddCommand(armorySaveCmd)
armoryEnableCmd := &cobra.Command{
Use: consts.EnableStr,
Short: "Enable an armory",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.EnableStr}),
Args: cobra.ExactArgs(1), // The name of the armory
Run: func(cmd *cobra.Command, args []string) {
ChangeArmoryEnabledState(cmd, con, args, true)
},
}
carapace.Gen(armoryEnableCmd).PositionalCompletion(
carapace.ActionValues().Usage("name of the armory"),
)
armoryCmd.AddCommand(armoryEnableCmd)
armoryDisableCmd := &cobra.Command{
Use: consts.DisableStr,
Short: "Disable an armory",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.DisableStr}),
Args: cobra.ExactArgs(1), // The name of the armory
Run: func(cmd *cobra.Command, args []string) {
ChangeArmoryEnabledState(cmd, con, args, false)
},
}
carapace.Gen(armoryDisableCmd).PositionalCompletion(
carapace.ActionValues().Usage("name of the armory"),
)
armoryCmd.AddCommand(armoryDisableCmd)
armoryModifyCmd := &cobra.Command{
Use: consts.ModifyStr,
Short: "Modify an armory configuration",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.ModifyStr}),
Args: cobra.ExactArgs(1), // The name of the armory
Run: func(cmd *cobra.Command, args []string) {
ModifyArmoryCmd(cmd, con, args)
},
}
flags.Bind("", false, armoryModifyCmd, func(f *pflag.FlagSet) {
f.StringP("name", "n", "", "new name for the armory")
f.StringP("pubkey", "k", "", "new public key for the armory")
f.StringP("url", "u", "", "new URL for the armory")
f.StringP("auth", "a", "", "new authorization details / credentials for the armory")
f.StringP("authcmd", "x", "", "new local command to run for authorization to the armory")
f.BoolP("no-save", "e", false, "do not save armory configuration to armory configuration file")
})
carapace.Gen(armoryModifyCmd).PositionalCompletion(
carapace.ActionValues().Usage("name of the armory"),
)
armoryCmd.AddCommand(armoryModifyCmd)
armoryRefreshCmd := &cobra.Command{
Use: consts.RefreshStr,
Short: "Refresh armory indexes",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.RefreshStr}),
Run: func(cmd *cobra.Command, args []string) {
RefreshArmories(cmd, con)
},
}
armoryCmd.AddCommand(armoryRefreshCmd)
armoryResetCmd := &cobra.Command{
Use: consts.ResetStr,
Short: "Reset armory configuration",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.ResetStr}),
Run: func(cmd *cobra.Command, args []string) {
ResetArmoryConfig(cmd, con)
},
}
armoryCmd.AddCommand(armoryResetCmd)
return []*cobra.Command{armoryCmd}
}
+130 -600
View File
@@ -19,608 +19,122 @@ package armory
*/
import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/url"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/util/minisign"
)
// ErrPackageNotFound - The package was not found
var ErrPackageNotFound = errors.New("package not found")
var ErrPackageAlreadyInstalled = errors.New("package is already installed")
const (
doNotInstallOption = "Do not install this package"
doNotInstallPackageName = "do not install"
)
// ArmoryInstallCmd - The armory install command
func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
var promptToOverwrite bool
names := []string{}
if len(args) > 0 {
names = []string{args[0]}
}
forceInstallation, err := cmd.Flags().GetBool("force")
if err != nil {
con.PrintErrorf("Could not parse %q flag: %s\n", "force", err)
return
}
if forceInstallation {
promptToOverwrite = false
} else {
promptToOverwrite = true
}
armoryName, err := cmd.Flags().GetString("armory")
if err != nil {
con.PrintErrorf("Could not parse %q flag: %s\n", "armory", err)
return
}
// Find PK for the armory name
armoryPK := getArmoryPublicKey(armoryName)
// If the armory with the name is not found, print a warning
if cmd.Flags().Changed("armory") && armoryPK == "" {
con.PrintWarnf("Could not find a configured armory named %q - searching all configured armories\n\n", armoryName)
}
clientConfig := parseArmoryHTTPConfig(cmd)
refresh(clientConfig)
if len(names) == 0 {
armoryFilterName := ""
if armoryPK != "" {
armoryFilterName = armoryName
}
options := armoryInstallOptions(armoryPK, armoryFilterName)
if len(options) == 0 {
con.PrintInfof("No packages or bundles found\n")
return
}
result, err := forms.ArmoryInstallForm(options)
if err != nil {
if errors.Is(err, forms.ErrUserAborted) {
return
}
con.PrintErrorf("Armory install form failed: %s\n", err)
return
}
if len(result.SelectedNames) == 0 {
return
}
names = result.SelectedNames
}
if len(names) == 0 {
func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
name := args[0]
// name := ctx.Args.String("name")
if name == "" {
con.PrintErrorf("A package or bundle name is required")
return
}
if slices.Contains(names, "all") {
names = []string{"all"}
}
for _, name := range names {
if name == "" {
continue
clientConfig := parseArmoryHTTPConfig(cmd)
refresh(clientConfig)
if name == "all" {
aliases, extensions := packagesInCache()
confirm := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("Install %d aliases and %d extensions?",
len(aliases), len(extensions),
),
}
promptOverwrite := promptToOverwrite
if name == "all" {
aliases, extensions := packageManifestsInCache()
aliasCount, extCount := countUniqueCommandsFromManifests(aliases, extensions)
confirm := false
pluralAliases := "es"
if aliasCount == 1 {
pluralAliases = ""
}
pluralExtensions := "s"
if extCount == 1 {
pluralExtensions = ""
}
forms.Confirm(fmt.Sprintf("Install %d alias%s and %d extension%s?",
aliasCount, pluralAliases, extCount, pluralExtensions,
), &confirm)
if !confirm {
return
}
promptOverwrite = false
}
err = installPackageByName(name, armoryPK, forceInstallation, promptOverwrite, clientConfig, con)
if err == nil {
continue
}
if errors.Is(err, ErrPackageNotFound) {
bundles := bundlesInCache()
for _, bundle := range bundles {
if bundle.Name == name {
installBundle(bundle, armoryPK, forceInstallation, clientConfig, con)
err = nil
break
}
}
if err == nil {
continue
}
// If we have made it here, then there was not a bundle or package that matched the provided name
if armoryPK == "" {
con.PrintErrorf("No package or bundle named %q was found\n", name)
} else {
con.PrintErrorf("No package or bundle named %q was found in armory %s\n", name, armoryName)
}
} else if errors.Is(err, ErrPackageAlreadyInstalled) {
con.PrintErrorf("Package %q is already installed - use the force option to overwrite it\n", name)
} else {
con.PrintErrorf("Could not install package: %s\n", err)
}
}
}
func installBundle(bundle *ArmoryBundle, armoryPK string, forceInstallation bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) {
installList := []string{}
pendingPackages := make(map[string]string)
for _, bundlePkgName := range bundle.Packages {
packageInstallList, err := buildInstallList(bundlePkgName, armoryPK, forceInstallation, pendingPackages)
if err != nil {
if errors.Is(err, ErrPackageAlreadyInstalled) {
con.PrintInfof("Package %s is already installed. Skipping...\n", bundlePkgName)
continue
} else {
con.PrintErrorf("Error for package %s: %s\n", bundlePkgName, err)
return
}
}
for _, pkgID := range packageInstallList {
if !slices.Contains(installList, pkgID) {
installList = append(installList, pkgID)
}
}
}
for _, packageID := range installList {
packageEntry := packageCacheLookupByID(packageID)
if packageEntry == nil {
con.PrintErrorf("The package cache is out of date. Please run armory refresh and try again.\n")
survey.AskOne(prompt, &confirm)
if !confirm {
return
}
if packageEntry.Pkg.IsAlias {
err := installAliasPackage(packageEntry, false, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to install alias '%s': %s", packageEntry.Alias.CommandName, err)
return
}
} else {
err := installExtensionPackage(packageEntry, false, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to install extension '%s': %s", packageEntry.Extension.Name, err)
}
err := installPackageByName(name, clientConfig, con)
if err == nil {
return
}
if err == ErrPackageNotFound {
bundles := bundlesInCache()
for _, bundle := range bundles {
if bundle.Name == name {
installBundle(bundle, clientConfig, con)
return
}
}
}
con.PrintErrorf("No package or bundle named '%s' was found", name)
}
func getInstalledPackageNames() []string {
packageNames := []string{}
installedAliases := assets.GetInstalledAliasManifests()
installedExtensions := assets.GetInstalledExtensionManifests()
for _, aliasFileName := range installedAliases {
alias := &alias.AliasManifest{}
manifestData, err := os.ReadFile(aliasFileName)
func installBundle(bundle *ArmoryBundle, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
for _, pkgName := range bundle.Packages {
err := installPackageByName(pkgName, clientConfig, con)
if err != nil {
continue
}
err = json.Unmarshal(manifestData, alias)
if err != nil {
continue
}
if !slices.Contains(packageNames, alias.CommandName) {
packageNames = append(packageNames, alias.CommandName)
con.PrintErrorf("Failed to install '%s': %s", pkgName, err)
}
}
for _, extensionFileName := range installedExtensions {
extension := &extensions.ExtensionManifest{}
manifestData, err := os.ReadFile(extensionFileName)
if err != nil {
continue
}
err = json.Unmarshal(manifestData, extension)
if err != nil {
continue
}
if len(extension.ExtCommand) == 0 {
extensionOld := &extensions.ExtensionManifest_{}
// Some extension manifests are using an older version
// To maintain compatibility with those extensions, we will
// re-unmarshal the data as the older version
err = json.Unmarshal(manifestData, extensionOld)
if err != nil {
continue
}
if !slices.Contains(packageNames, extensionOld.CommandName) {
packageNames = append(packageNames, extensionOld.CommandName)
}
} else {
for _, command := range extension.ExtCommand {
if !slices.Contains(packageNames, command.CommandName) {
packageNames = append(packageNames, command.CommandName)
}
}
}
}
return packageNames
}
// This is a convenience function to get the names of the commands in the cache
func getCommandsInCache(armoryPK string) []string {
commandNames := []string{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.LastErr == nil {
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
return true
func installPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
aliases, extensions := packagesInCache()
for _, alias := range aliases {
if alias.CommandName == name || name == "all" {
installAlias(alias, clientConfig, con)
if name != "all" {
return nil
}
if cacheEntry.Pkg.IsAlias {
if cacheEntry.Alias.CommandName != "" && !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
commandNames = append(commandNames, cacheEntry.Alias.CommandName)
}
} else {
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName != "" && !slices.Contains(commandNames, command.CommandName) {
commandNames = append(commandNames, command.CommandName)
}
}
}
}
return true
})
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)
}
return true
})
return packages
}
func getPackageIDFromUser(name string, options map[string]string) string {
selectedPackageKey := ""
optionKeys := slices.Collect(maps.Keys(options))
slices.Sort(optionKeys)
// Add a cancel option
optionKeys = append(optionKeys, doNotInstallOption)
options[doNotInstallOption] = doNotInstallPackageName
forms.Select(fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name), optionKeys, &selectedPackageKey)
selectedPackageID := options[selectedPackageKey]
return selectedPackageID
}
func getPackageForCommand(name, armoryPK, minimumVersion string) (*pkgCacheEntry, error) {
packagesWithCommand := getPackagesWithCommandName(name, armoryPK, minimumVersion)
if len(packagesWithCommand) > 1 {
// Build an option map for the user to choose from (option -> pkgID)
optionMap := make(map[string]string)
for _, packageEntry := range packagesWithCommand {
var optionName string
if packageEntry.Pkg.IsAlias {
optionName = fmt.Sprintf("Alias %s %s from armory %s (%s)",
name,
packageEntry.Alias.Version,
packageEntry.ArmoryConfig.Name,
packageEntry.Pkg.RepoURL,
)
} else {
optionName = fmt.Sprintf("Extension %s %s from armory %s with command %s (%s)",
packageEntry.Pkg.Name,
packageEntry.Extension.Version,
packageEntry.ArmoryConfig.Name,
name,
packageEntry.Pkg.RepoURL,
)
}
optionMap[optionName] = packageEntry.ID
}
selectedPackageID := getPackageIDFromUser(name, optionMap)
if selectedPackageID == doNotInstallPackageName {
return nil, fmt.Errorf("user cancelled installation")
}
for _, packageEntry := range packagesWithCommand {
if packageEntry.ID == selectedPackageID {
return packageEntry, nil
}
}
} else if len(packagesWithCommand) == 1 {
return packagesWithCommand[0], nil
}
return nil, ErrPackageNotFound
}
func buildInstallList(name, armoryPK string, forceInstallation bool, pendingPackages map[string]string) ([]string, error) {
packageInstallList := []string{}
installedPackages := getInstalledPackageNames()
/*
Gather information about what we are working with
Find all conflicts within aliases for a given name (or all names), same thing with extensions
Then if there are aliases and extensions with a given name, make sure to note that for when we ask the user what to do
*/
var requestedPackageList []string
if name == "all" {
requestedPackageList = []string{}
allCommands := getCommandsInCache(armoryPK)
for _, cmdName := range allCommands {
if !slices.Contains(installedPackages, cmdName) || forceInstallation {
// Check to see if there is a package pending with that name
if _, ok := pendingPackages[cmdName]; !ok {
requestedPackageList = append(requestedPackageList, cmdName)
}
}
}
} else {
if !slices.Contains(installedPackages, name) || forceInstallation {
// Check to see if there is a package pending with that name
if _, ok := pendingPackages[name]; !ok {
requestedPackageList = []string{name}
}
} else {
return nil, ErrPackageAlreadyInstalled
}
}
for _, packageName := range requestedPackageList {
if _, ok := pendingPackages[packageName]; ok {
// We are already going to install a package with this name, so do not try to resolve it
continue
}
packageEntry, err := getPackageForCommand(packageName, armoryPK, "")
if err != nil {
return nil, err
}
if !slices.Contains(packageInstallList, packageEntry.ID) {
packageInstallList = append(packageInstallList, packageEntry.ID)
pendingPackages[packageName] = packageEntry.ID
}
if !packageEntry.Pkg.IsAlias {
dependencies := make(map[string]*pkgCacheEntry)
err = resolveExtensionPackageDependencies(packageEntry, dependencies, pendingPackages)
if err != nil {
return nil, err
}
for pkgName, packageEntry := range dependencies {
if !slices.Contains(packageInstallList, packageEntry.ID) {
packageInstallList = append(packageInstallList, packageEntry.ID)
}
if _, ok := pendingPackages[pkgName]; !ok {
pendingPackages[pkgName] = packageEntry.ID
for _, extm := range extensions {
for _, ext := range extm.ExtCommand {
if ext.CommandName == name || name == "all" {
installExtension(ext.Manifest, clientConfig, con)
if name != "all" {
return nil
}
}
}
}
return packageInstallList, nil
}
func installPackageByName(name, armoryPK string, forceInstallation, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
pendingPackages := make(map[string]string)
packageInstallList, err := buildInstallList(name, armoryPK, forceInstallation, pendingPackages)
if err != nil {
return err
}
if len(packageInstallList) == 0 && name == "all" {
availableCommands := getCommandsInCache(armoryPK)
if len(availableCommands) == 0 {
con.PrintInfof("No packages or bundles found\n")
} else {
con.PrintInfof("All available packages are already installed\n")
}
return nil
}
if len(packageInstallList) > 0 {
for _, packageID := range packageInstallList {
entry := packageCacheLookupByID(packageID)
if entry == nil {
return errors.New("cache consistency error - please refresh the cache and try again")
}
if entry.Pkg.IsAlias {
err := installAliasPackage(entry, promptToOverwrite, clientConfig, con)
if err != nil {
return fmt.Errorf("failed to install alias '%s': %s", entry.Alias.CommandName, err)
}
} else {
err := installExtensionPackage(entry, promptToOverwrite, clientConfig, con)
if err != nil {
return fmt.Errorf("failed to install extension '%s': %s", entry.Extension.Name, err)
}
}
}
} else {
return ErrPackageNotFound
}
if name == "all" {
con.Printf("\n")
con.PrintSuccessf("Operation complete\n")
con.PrintInfof("All packages installed\n")
return nil
}
return nil
return ErrPackageNotFound
}
func armoryInstallOptions(armoryPK, armoryName string) []forms.ArmoryInstallOption {
options := []forms.ArmoryInstallOption{}
seen := make(map[string]bool)
packageCount := 0
func installAlias(alias *alias.AliasManifest, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
err := installAliasPackageByName(alias.CommandName, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to install alias '%s': %s", alias.CommandName, err)
return
}
}
func installAliasPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
var entry *pkgCacheEntry
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
return true
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.Pkg.CommandName == name {
entry = &cacheEntry
return false
}
if cacheEntry.LastErr != nil {
return true
}
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
return true
}
if cacheEntry.Pkg.IsAlias {
name := cacheEntry.Alias.CommandName
if name == "" {
return true
}
if seen[name] {
return true
}
label := formatArmoryInstallLabel("alias", name, cacheEntry.Alias.Help)
options = append(options, forms.ArmoryInstallOption{
Value: name,
Label: label,
})
seen[name] = true
packageCount++
return true
}
for _, command := range cacheEntry.Extension.ExtCommand {
name := command.CommandName
if name == "" {
continue
}
if seen[name] {
continue
}
label := formatArmoryInstallLabel("extension", name, command.Help)
options = append(options, forms.ArmoryInstallOption{
Value: name,
Label: label,
})
seen[name] = true
packageCount++
}
return true
})
for _, bundle := range bundlesInCache() {
if bundle.Name == "" {
continue
}
if armoryName != "" && bundle.ArmoryName != armoryName {
continue
}
if seen[bundle.Name] {
continue
}
label := fmt.Sprintf("bundle: %s (%d packages)", bundle.Name, len(bundle.Packages))
options = append(options, forms.ArmoryInstallOption{
Value: bundle.Name,
Label: label,
})
seen[bundle.Name] = true
}
if len(options) == 0 {
return options
}
sort.Slice(options, func(i, j int) bool {
return options[i].Label < options[j].Label
})
if packageCount > 0 {
options = append([]forms.ArmoryInstallOption{
{
Value: "all",
Label: "all packages (aliases + extensions)",
},
}, options...)
}
return options
}
func formatArmoryInstallLabel(kind, name, help string) string {
help = strings.TrimSpace(help)
if help == "" {
return fmt.Sprintf("%s: %s", kind, name)
}
help = strings.Join(strings.Fields(help), " ")
return fmt.Sprintf("%s: %s - %s", kind, name, help)
}
func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
if entry == nil {
return errors.New("package not found")
}
if !entry.Pkg.IsAlias {
return errors.New("package is not an alias")
}
repoURL, err := url.Parse(entry.RepoURL)
if err != nil {
return err
@@ -647,15 +161,20 @@ func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientCon
return errors.New("signature verification failed")
}
tmpFileName, err := writeArmoryTempFile(tarGz)
tmpFile, err := os.CreateTemp("", "sliver-armory-")
if err != nil {
return err
}
defer os.Remove(tmpFileName)
defer os.Remove(tmpFile.Name())
_, err = tmpFile.Write(tarGz)
if err != nil {
return err
}
tmpFile.Close()
con.Printf(console.Clearln + "\r") // Clear the line
installPath := alias.InstallFromFile(tmpFileName, entry.Alias.CommandName, promptToOverwrite, con)
installPath := alias.InstallFromFile(tmpFile.Name(), true, con)
if installPath == nil {
return errors.New("failed to install alias")
}
@@ -669,15 +188,50 @@ func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientCon
return nil
}
func installExtension(extm *extensions.ExtensionManifest, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
deps := make(map[string]struct{})
for _, ext := range extm.ExtCommand {
resolveExtensionPackageDependencies(ext.CommandName, deps, clientConfig, con)
sliverMenu := con.App.Menu(constants.ImplantMenu).Root()
for dep := range deps {
if extensions.CmdExists(dep, sliverMenu) {
continue // Dependency is already installed
}
err := installExtensionPackageByName(dep, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to install extension dependency '%s': %s", dep, err)
return
}
}
err := installExtensionPackageByName(ext.CommandName, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to install extension '%s': %s", ext.CommandName, err)
return
}
}
}
const maxDepDepth = 10 // Arbitrary recursive limit for dependencies
func resolveExtensionPackageDependencies(pkg *pkgCacheEntry, deps map[string]*pkgCacheEntry, pendingPackages map[string]string) error {
for _, multiExt := range pkg.Extension.ExtCommand {
func resolveExtensionPackageDependencies(name string, deps map[string]struct{}, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) {
var entry *pkgCacheEntry
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.Pkg.CommandName == name {
entry = &cacheEntry
return false
}
return true
})
if entry == nil {
return
}
for _, multiExt := range entry.Extension.ExtCommand {
if multiExt.DependsOn == "" {
continue // Avoid adding empty dependency
}
if multiExt.DependsOn == pkg.Extension.Name {
if multiExt.DependsOn == name {
continue // Avoid infinite loop of something that depends on itself
}
// We also need to look out for circular dependencies, so if we've already
@@ -685,28 +239,24 @@ func resolveExtensionPackageDependencies(pkg *pkgCacheEntry, deps map[string]*pk
if _, ok := deps[multiExt.DependsOn]; ok {
continue // Already resolved
}
// Check to make sure we are not already going to install a package with this name
if _, ok := pendingPackages[multiExt.DependsOn]; ok {
continue
}
if maxDepDepth < len(deps) {
continue
}
// Figure out what package we need for the dependency
dependencyEntry, err := getPackageForCommand(multiExt.DependsOn, "", "")
if err != nil {
return fmt.Errorf("could not resolve dependency %s for %s: %s", multiExt.DependsOn, pkg.Extension.Name, err)
}
deps[multiExt.DependsOn] = dependencyEntry
err = resolveExtensionPackageDependencies(dependencyEntry, deps, pendingPackages)
if err != nil {
return err
}
deps[multiExt.DependsOn] = struct{}{}
resolveExtensionPackageDependencies(multiExt.DependsOn, deps, clientConfig, con)
}
return nil
}
func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
func installExtensionPackageByName(name string, clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) error {
var entry *pkgCacheEntry
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.Pkg.CommandName == name {
entry = &cacheEntry
return false
}
return true
})
if entry == nil {
return errors.New("package not found")
}
@@ -736,43 +286,23 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
return errors.New("signature verification failed")
}
tmpFileName, err := writeArmoryTempFile(tarGz)
tmpFile, err := os.CreateTemp("", "sliver-armory-")
if err != nil {
return err
}
defer os.Remove(tmpFile.Name())
_, err = tmpFile.Write(tarGz)
if err != nil {
return err
}
err = tmpFile.Sync()
if err != nil {
return err
}
defer os.Remove(tmpFileName)
con.Printf(console.Clearln + "\r") // Clear download message
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
}
extensions.InstallFromDir(tmpFile.Name(), con, true)
func writeArmoryTempFile(data []byte) (string, error) {
if len(data) == 0 {
return "", errors.New("downloaded archive is empty")
}
tmpFile, err := os.CreateTemp("", "sliver-armory-")
if err != nil {
return "", err
}
tmpFileName := tmpFile.Name()
for len(data) > 0 {
n, err := tmpFile.Write(data)
if err != nil {
tmpFile.Close()
os.Remove(tmpFileName)
return "", err
}
data = data[n:]
}
if err := tmpFile.Sync(); err != nil {
tmpFile.Close()
os.Remove(tmpFileName)
return "", err
}
if err := tmpFile.Close(); err != nil {
os.Remove(tmpFileName)
return "", err
}
return tmpFileName, nil
return nil
}
-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)
}
}
-670
View File
@@ -1,670 +0,0 @@
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"
"path/filepath"
"slices"
"strings"
"sync"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
func printArmories(con *console.SliverClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Armory Name",
"Authorization Required",
"Enabled",
"URL",
})
configuredArmories := getCurrentArmoryConfiguration()
for _, armory := range configuredArmories {
var authRequired string
var enabled string
if armory.Authorization != "" {
authRequired = "yes"
} else {
authRequired = "no"
}
if armory.Enabled {
enabled = "yes"
} else {
enabled = "no"
}
tw.AppendRow(table.Row{
armory.Name,
authRequired,
enabled,
armory.RepoURL,
})
}
con.Printf("%s\n", tw.Render())
}
func generateRowsForArmory(armoryInfo *assets.ArmoryConfig) []table.Row {
rows := make([]table.Row, 0)
rows = append(rows, table.Row{
"Name",
armoryInfo.Name,
})
rows = append(rows, table.Row{
"Public Key",
armoryInfo.PublicKey,
})
enabled := "yes"
if !armoryInfo.Enabled {
enabled = "no"
}
rows = append(rows, table.Row{
"Enabled",
enabled,
})
authorizationRequired := "no"
if armoryInfo.Authorization != "" {
authorizationRequired = fmt.Sprintf("yes; current authorization: %s", armoryInfo.Authorization)
}
rows = append(rows, table.Row{
"Authorization required?",
authorizationRequired,
})
if authorizationRequired != "no" {
authCmd := armoryInfo.AuthorizationCmd
if authCmd == "" {
authCmd = "not configured"
}
rows = append(rows, table.Row{
"Authorization Command",
authCmd,
})
}
rows = append(rows, table.Row{
"URL",
armoryInfo.RepoURL,
})
return rows
}
func generateRowsForPackage(packageInfo *pkgCacheEntry) []table.Row {
rows := make([]table.Row, 0)
rows = append(rows, table.Row{
"Name",
packageInfo.Pkg.Name,
})
rows = append(rows, table.Row{
"Command Name",
packageInfo.Pkg.CommandName,
})
rows = append(rows, table.Row{
"Armory Name",
packageInfo.Pkg.ArmoryName,
})
packageType := "alias"
if !packageInfo.Pkg.IsAlias {
packageType = "extension"
}
rows = append(rows, table.Row{
"Package Type",
packageType,
})
if packageType == "alias" {
rows = append(rows, table.Row{
"Version",
packageInfo.Alias.Version,
})
rows = append(rows, table.Row{
"Author",
packageInfo.Alias.OriginalAuthor,
})
rows = append(rows, table.Row{
"Repo URL",
packageInfo.Alias.RepoURL,
})
rows = append(rows, table.Row{
"Help",
packageInfo.Alias.Help,
})
} else {
rows = append(rows, table.Row{
"Version",
packageInfo.Extension.Version,
})
rows = append(rows, table.Row{
"Original Author",
packageInfo.Extension.OriginalAuthor,
})
rows = append(rows, table.Row{
"Extension Author",
packageInfo.Extension.ExtensionAuthor,
})
rows = append(rows, table.Row{
"URL",
packageInfo.Extension.RepoURL,
})
extCommands := []string{}
for _, cmd := range packageInfo.Extension.ExtCommand {
extCommands = append(extCommands, cmd.CommandName)
}
rows = append(rows, table.Row{
"Contains commands",
strings.Join(extCommands, ", "),
})
rows = append(rows, table.Row{"", ""})
rows = append(rows, table.Row{"Command Info", ""})
for _, extCmd := range packageInfo.Extension.ExtCommand {
rows = append(rows, table.Row{
"Command Name",
extCmd.CommandName,
})
rows = append(rows, table.Row{
"Dependencies",
extCmd.DependsOn,
})
rows = append(rows, table.Row{
"Help",
extCmd.Help,
})
rows = append(rows, table.Row{"", ""})
}
}
rows = append(rows, table.Row{"", ""})
return rows
}
func viewDetailedInformation(con *console.SliverClient, entity string) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
"Property",
"Value",
})
// See where this name exists in the armory and package indexes
armoryResult := armoryLookupByName(entity)
if armoryResult != nil {
tw.AppendRows(generateRowsForArmory(armoryResult))
con.Printf("Armory information for %s\n\n", entity)
con.Printf("%s\n", tw.Render())
tw.ResetRows()
con.Printf("\n")
}
// Get packages with this name
packageResult := packageCacheLookupByName(entity)
if len(packageResult) > 0 {
con.Printf("Packages named %s\n\n", entity)
for _, pkg := range packageResult {
tw.AppendRows(generateRowsForPackage(pkg))
con.Printf("%s\n", tw.Render())
tw.ResetRows()
con.Printf("\n")
}
}
// Get extensions containing commands with this name
commandResult := packageCacheLookupByCmd(entity)
commandResultFiltered := make([]*pkgCacheEntry, 0)
for _, cmd := range commandResult {
if !cmd.Pkg.IsAlias {
commandResultFiltered = append(commandResultFiltered, cmd)
}
}
if len(commandResultFiltered) > 0 {
con.Printf("Packages containing commands named %s\n\n", entity)
for _, cmd := range commandResultFiltered {
tw.AppendRows(generateRowsForPackage(cmd))
con.Printf("%s\n", tw.Render())
tw.ResetRows()
}
}
}
func ArmoryInfoCommand(cmd *cobra.Command, con *console.SliverClient, args []string) {
if len(args) == 0 {
printArmories(con)
return
}
// If the user wants info on something specific, make sure the caches are up to date
refresh(parseArmoryHTTPConfig(cmd))
viewDetailedInformation(con, args[0])
}
func verifyArmory(armoryInfo *assets.ArmoryConfig, clientConfig ArmoryHTTPConfig) error {
wg := &sync.WaitGroup{}
// Only making one request, so we can limit ourselves to one concurrent request
requestChannel := make(chan struct{}, 1)
wg.Add(1)
requestChannel <- struct{}{}
go fetchIndex(armoryInfo, requestChannel, clientConfig, wg)
wg.Wait()
result, ok := indexCache.Load(armoryInfo.PublicKey)
if !ok {
return fmt.Errorf("failed to retrieve armory index")
}
cacheResult := result.(indexCacheEntry)
return cacheResult.LastErr
}
func getCurrentArmoryConfiguration() []*assets.ArmoryConfig {
configs := []*assets.ArmoryConfig{}
armoryNames := []string{}
// If the default armory is in the configuration, force it to be last
var defaultConfig *assets.ArmoryConfig
currentArmories.Range(func(key, value interface{}) bool {
armoryEntry := value.(assets.ArmoryConfig)
// Skip over the default armory for now
if armoryEntry.Name != assets.DefaultArmoryName {
configs = append(configs, &armoryEntry)
armoryNames = append(armoryNames, armoryEntry.Name)
} else {
defaultConfig = &armoryEntry
}
return true
})
if !armoriesInitialized {
/*
Armories are initialized on the first call to the armory command
If armories are added or removed before the first call, we want
to make sure we still load in the ones from the configuration
file.
*/
persistentConfigs := assets.GetArmoriesConfig()
for _, config := range persistentConfigs {
if !slices.Contains(armoryNames, config.Name) {
if config.Name == assets.DefaultArmoryName {
if defaultArmoryRemoved {
continue
} else if defaultConfig != nil {
// The user potentially changed something about the default config
configs = append(configs, defaultConfig)
currentArmories.Store(config.PublicKey, *defaultConfig)
continue
}
}
configs = append(configs, config)
currentArmories.Store(config.PublicKey, *config)
}
}
return configs
}
if !defaultArmoryRemoved {
if defaultConfig != nil {
configs = append(configs, defaultConfig)
} else {
configs = append(configs, assets.DefaultArmoryConfig)
}
}
assets.RefreshArmoryAuthorization(configs)
return configs
}
func armoryNameExists(name string) bool {
currentArmoriesConfigured := getCurrentArmoryConfiguration()
for _, armory := range currentArmoriesConfigured {
if armory.Name == name {
return true
}
}
return false
}
func armoryKeyExists(pubKey string) bool {
currentArmoriesConfigured := getCurrentArmoryConfiguration()
for _, armory := range currentArmoriesConfigured {
if armory.PublicKey == pubKey {
return true
}
}
return false
}
func AddArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// Get necessary information
url, _ := cmd.Flags().GetString("url")
pubKey, _ := cmd.Flags().GetString("pubkey")
auth, _ := cmd.Flags().GetString("auth")
authCmd, _ := cmd.Flags().GetString("authcmd")
noSave, _ := cmd.Flags().GetBool("no-save")
name := args[0]
if name == "" {
con.PrintErrorf("an armory name is required\n")
return
}
// Make sure we do not already have an armory config with the supplied name
if armoryNameExists(name) {
con.PrintErrorf("armory %s already exists\n", name)
return
}
clientConfig := parseArmoryHTTPConfig(cmd)
armoryConfig := assets.ArmoryConfig{
PublicKey: pubKey,
RepoURL: url,
Authorization: auth,
AuthorizationCmd: authCmd,
Name: name,
Enabled: true,
}
// verifyArmory will add the armory index to the cache on success, so we do not have to force a refresh
err := verifyArmory(&armoryConfig, clientConfig)
if err != nil {
con.PrintErrorf("could not add armory: %s\n", err)
return
}
currentArmories.Store(armoryConfig.PublicKey, armoryConfig)
if !noSave {
configs := getCurrentArmoryConfiguration()
err = assets.SaveArmoriesConfig(configs)
if err != nil {
con.PrintErrorf("Could not save armory configuration: %s\n", err)
} else {
con.PrintSuccessf("Armory configuration saved\n")
}
}
con.PrintSuccessf("Added armory %s\n", name)
}
// Get a list of package IDs for an armory by its public key
func getPackageIDsForArmory(armoryPublicKey string) []string {
packageIDs := []string{}
pkgCache.Range(func(key, value interface{}) bool {
pkgEntry := value.(pkgCacheEntry)
if pkgEntry.ArmoryConfig.PublicKey == armoryPublicKey {
packageIDs = append(packageIDs, pkgEntry.ID)
}
return true
})
return packageIDs
}
func RemoveArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
name := args[0]
if name == "" {
con.PrintErrorf("an armory name is required\n")
return
}
armories := getCurrentArmoryConfiguration()
deleted := false
newConfiguration := []*assets.ArmoryConfig{}
for _, armory := range armories {
if armory.Name != name {
newConfiguration = append(newConfiguration, armory)
} else {
currentArmories.Delete(armory.PublicKey)
indexCache.Delete(armory.PublicKey)
/*
To delete from the package cache, we will have
to gather a list of package IDs for the
armory then delete them from the cache.
*/
armoryPackageIDs := getPackageIDsForArmory(armory.PublicKey)
for _, id := range armoryPackageIDs {
pkgCache.Delete(id)
}
deleted = true
}
}
if deleted {
con.PrintSuccessf("Armory %s successfully deleted, saving configuration...\n", name)
if name == assets.DefaultArmoryName {
defaultArmoryRemoved = true
}
} else {
con.PrintErrorf("Armory %s is not a configured armory\n", name)
return
}
err := assets.SaveArmoriesConfig(newConfiguration)
if err != nil {
con.PrintErrorf("Could not save new armory configuration: %s\n", err)
} else {
con.PrintSuccessf("Saved armory configuration\n")
}
}
func SaveArmories(con *console.SliverClient) {
configs := getCurrentArmoryConfiguration()
err := assets.SaveArmoriesConfig(configs)
if err != nil {
con.PrintErrorf("Could not save armory configuration: %s\n", err)
} else {
con.PrintSuccessf("Successfully saved armory configuration\n")
}
}
func getArmoryConfig(name string) (assets.ArmoryConfig, error) {
armories := getCurrentArmoryConfiguration()
var armoryPublicKey string
armoryConfig := assets.ArmoryConfig{}
for _, armory := range armories {
if armory.Name == name {
armoryPublicKey = armory.PublicKey
break
}
}
if armoryPublicKey == "" {
return armoryConfig, fmt.Errorf("could not retrieve armory configuration for armory %s. Try running the armory command to refresh indexes", name)
}
value, ok := currentArmories.Load(armoryPublicKey)
if !ok {
return armoryConfig, fmt.Errorf("could not retrieve armory configuration for armory %s. Try running the armory command to refresh indexes", name)
}
armoryConfig = value.(assets.ArmoryConfig)
return armoryConfig, nil
}
func ChangeArmoryEnabledState(cmd *cobra.Command, con *console.SliverClient, args []string, enabled bool) {
// Get the armory's public key, change its state, then update the cache to remove packages from the armory
var name string
if len(args) > 0 && args[0] != "" {
name = args[0]
} else {
con.PrintErrorf("an armory name is required")
return
}
armoryConfig, err := getArmoryConfig(name)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
armoryPublicKey := armoryConfig.PublicKey
armoryConfig.Enabled = enabled
currentArmories.Store(armoryPublicKey, armoryConfig)
if !enabled {
// Remove cached info for the armory if it is disabled
armoryPackageIDs := getPackageIDsForArmory(armoryPublicKey)
for _, id := range armoryPackageIDs {
pkgCache.Delete(id)
}
indexCache.Delete(armoryPublicKey)
}
// Force a refresh
clientConfig := parseArmoryHTTPConfig(cmd)
con.PrintInfof("Refreshing armory information...\n")
refresh(clientConfig)
SaveArmories(con)
con.PrintSuccessf("Done\n")
}
func ModifyArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// Quick check to make sure we were provided with a name
var name string
if len(args) > 0 && args[0] != "" {
name = args[0]
}
if name == "" {
con.PrintErrorf("an armory name is required\n")
return
}
// Get the record for the armory
armoryConfig, err := getArmoryConfig(name)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
// Save the old config so we can delete the existing config
// and revert if needed
previousConfig := assets.ArmoryConfig{
PublicKey: armoryConfig.PublicKey,
RepoURL: armoryConfig.RepoURL,
Authorization: armoryConfig.Authorization,
AuthorizationCmd: armoryConfig.AuthorizationCmd,
Name: armoryConfig.Name,
Enabled: armoryConfig.Enabled,
}
if cmd.Flags().Changed("name") {
newName, _ := cmd.Flags().GetString("name")
// Make sure the name does not already exist
if armoryNameExists(newName) {
con.PrintErrorf("armory with name %s already exists\n", name)
return
}
armoryConfig.Name = newName
}
if cmd.Flags().Changed("pubkey") {
newPubKey, _ := cmd.Flags().GetString("pubkey")
/*
Since public keys are used as the key for the index cache,
make sure we have not been given a key that already exists
*/
if armoryKeyExists(newPubKey) {
con.PrintErrorf("armory provided public key already exists\n")
return
}
armoryConfig.PublicKey = newPubKey
}
if cmd.Flags().Changed("url") {
newUrl, _ := cmd.Flags().GetString("url")
armoryConfig.RepoURL = newUrl
}
if cmd.Flags().Changed("auth") {
newAuth, _ := cmd.Flags().GetString("auth")
armoryConfig.Authorization = newAuth
}
if cmd.Flags().Changed("authcmd") {
newAuthCmd, _ := cmd.Flags().GetString("authcmd")
armoryConfig.AuthorizationCmd = newAuthCmd
}
// We will remove the old armory and add a new one with the changed properties to force a refresh
currentArmories.Delete(previousConfig.PublicKey)
indexCache.Delete(previousConfig.PublicKey)
armoryPackageIDs := getPackageIDsForArmory(previousConfig.PublicKey)
for _, id := range armoryPackageIDs {
pkgCache.Delete(id)
}
clientConfig := parseArmoryHTTPConfig(cmd)
err = verifyArmory(&armoryConfig, clientConfig)
if err != nil {
con.PrintErrorf("could not modify armory: %s\n", err)
confirm := false
_ = forms.Confirm("Would you like to revert the properties of the armory back to their previous values?", &confirm)
if !confirm {
return
} else {
err = verifyArmory(&previousConfig, clientConfig)
if err != nil {
con.PrintErrorf("could not re-add armory %s: %s\n", name, err)
return
}
armoryConfig = previousConfig
con.PrintSuccessf("Re-added armory with previous values")
}
}
currentArmories.Store(armoryConfig.PublicKey, armoryConfig)
if !cmd.Flags().Changed("no-save") {
SaveArmories(con)
}
// Force a refresh
con.PrintInfof("Refreshing armory information...\n")
refresh(clientConfig)
con.PrintSuccessf("Done\n")
}
func RefreshArmories(cmd *cobra.Command, con *console.SliverClient) {
clientConfig := parseArmoryHTTPConfig(cmd)
// Since this being called from the refresh command, force the refresh
clientConfig.IgnoreCache = true
clearAllCaches()
armoriesInitialized = false
con.PrintInfof("Refreshing armory information...")
refresh(clientConfig)
con.PrintSuccessf("Refreshed armory information\n")
}
func ResetArmoryConfig(cmd *cobra.Command, con *console.SliverClient) {
armoryConfigPath := filepath.Join(assets.GetRootAppDir(), assets.ArmoryConfigFileName)
con.PrintInfof("Removing armory configuration file %s...", armoryConfigPath)
err := os.Remove(armoryConfigPath)
if err != nil {
con.PrintErrorf("Could not delete armory configuration file %s: %s", armoryConfigPath, err)
return
}
con.PrintSuccessf("Removed armory configuration file %s\n", armoryConfigPath)
// Force a refresh
RefreshArmories(cmd, con)
con.PrintSuccessf("Successfully reset armory configuration\n")
}
+5 -82
View File
@@ -19,8 +19,6 @@ package armory
*/
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
@@ -69,18 +67,6 @@ type armoryPkgResponse struct {
TarGzURL string `json:"tar_gz_url"` // Raw tar.gz url
}
func calculatePackageHash(pkg *ArmoryPackage) string {
if pkg == nil {
return ""
}
hasher := sha256.New()
// Hash some of the things that make the package unique
packageIdentifier := []byte(pkg.RepoURL + pkg.PublicKey + pkg.ArmoryName + pkg.CommandName)
hasher.Write(packageIdentifier)
return fmt.Sprintf("%x", hasher.Sum(nil))
}
//
// Default Parsers for Self-Hosted Armories
//
@@ -93,10 +79,7 @@ func DefaultArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig Ar
return nil, err
}
resp, body, err := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
if err != nil {
return nil, err
}
resp, body, _ := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
if resp.StatusCode != http.StatusOK {
return nil, errors.New("api returned non-200 status code")
}
@@ -128,45 +111,9 @@ func DefaultArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig Ar
if err != nil {
return nil, err
}
// Populate armory name and ID information for assets
for _, bundle := range armoryIndex.Bundles {
bundle.ArmoryName = armoryIndex.ArmoryConfig.Name
}
for _, alias := range armoryIndex.Aliases {
alias.ArmoryName = armoryIndex.ArmoryConfig.Name
alias.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
if cached := packageCacheLookupByCmdAndArmory(alias.CommandName, armoryIndex.ArmoryConfig.PublicKey); cached != nil {
// A package with this name is in the cache, so we will remove it here so that the latest version is added
// when the index is parsed
pkgCache.Delete(cached.ID)
}
alias.ID = calculatePackageHash(alias)
}
for _, extension := range armoryIndex.Extensions {
extension.ArmoryName = armoryIndex.ArmoryConfig.Name
extension.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
if cached := packageCacheLookupByCmdAndArmory(extension.CommandName, armoryIndex.ArmoryConfig.PublicKey); cached != nil {
// A package with this name is in the cache, so we will remove it here so that the latest version is added
// when the index is parsed
pkgCache.Delete(cached.ID)
}
extension.ID = calculatePackageHash(extension)
}
return armoryIndex, nil
}
func decodePackageSignature(sigString string) ([]byte, error) {
// Base64 decode the signature
decodedData := make([]byte, base64.StdEncoding.DecodedLen(len(sigString)))
_, err := base64.StdEncoding.Decode(decodedData, []byte(sigString))
if err != nil {
return nil, err
}
// If decodedData does not end up being base64.StdEncoding.DecodedLen(len(data)) bytes long, trim the null bytes
decodedData = bytes.Trim(decodedData, "\x00")
return decodedData, nil
}
// DefaultArmoryPkgParser - Parse the armory package manifest directly from the url
func DefaultArmoryPkgParser(armoryConfig *assets.ArmoryConfig, armoryPkg *ArmoryPackage, sigOnly bool, clientConfig ArmoryHTTPConfig) (*minisign.Signature, []byte, error) {
var publicKey minisign.PublicKey
@@ -175,29 +122,25 @@ func DefaultArmoryPkgParser(armoryConfig *assets.ArmoryConfig, armoryPkg *Armory
return nil, nil, err
}
resp, body, err := httpRequest(clientConfig, armoryPkg.RepoURL, armoryConfig, http.Header{})
resp, body, err := httpRequest(clientConfig, armoryConfig.RepoURL, armoryConfig, http.Header{})
if err != nil {
return nil, nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("api returned non-200 status code (%d)", resp.StatusCode)
return nil, nil, errors.New("api returned non-200 status code")
}
pkgResp := &armoryPkgResponse{}
err = json.Unmarshal(body, pkgResp)
if err != nil {
return nil, nil, err
}
decodedSig, err := decodePackageSignature(pkgResp.Minisig)
if err != nil {
return nil, nil, err
}
sig, err := parsePkgMinsig(decodedSig)
sig, err := parsePkgMinsig([]byte(pkgResp.Minisig))
if err != nil {
return nil, nil, err
}
var tarGz []byte
if !sigOnly {
tarGzURL, err := url.Parse(pkgResp.TarGzURL)
tarGzURL, err := url.Parse(armoryPkg.RepoURL)
if err != nil {
return nil, nil, err
}
@@ -299,20 +242,6 @@ func GithubAPIArmoryIndexParser(armoryConfig *assets.ArmoryConfig, clientConfig
if err != nil {
return nil, err
}
// Populate armory name and ID information for assets
for _, bundle := range armoryIndex.Bundles {
bundle.ArmoryName = armoryIndex.ArmoryConfig.Name
}
for _, alias := range armoryIndex.Aliases {
alias.ArmoryName = armoryIndex.ArmoryConfig.Name
alias.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
alias.ID = calculatePackageHash(alias)
}
for _, extension := range armoryIndex.Extensions {
extension.ArmoryName = armoryIndex.ArmoryConfig.Name
extension.ArmoryPK = armoryIndex.ArmoryConfig.PublicKey
extension.ID = calculatePackageHash(extension)
}
return armoryIndex, nil
}
@@ -507,12 +436,6 @@ func downloadRequest(clientConfig ArmoryHTTPConfig, reqURL string, armoryConfig
"Accept": {"application/octet-stream"},
}
resp, body, err := httpRequest(clientConfig, reqURL, armoryConfig, downloadHdr)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("invalid response when downloading %s, try again later", reqURL)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
return nil, fmt.Errorf("error downloading asset: http %d", resp.StatusCode)
}
+2 -2
View File
@@ -29,7 +29,7 @@ import (
)
// ArmorySearchCmd - Search for packages by name
func ArmorySearchCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func ArmorySearchCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
con.PrintInfof("Refreshing package cache ... ")
clientConfig := parseArmoryHTTPConfig(cmd)
refresh(clientConfig)
@@ -47,7 +47,7 @@ func ArmorySearchCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
return
}
aliases, exts := packageManifestsInCache()
aliases, exts := packagesInCache()
matchedAliases := []*alias.AliasManifest{}
for _, a := range aliases {
if nameExpr.MatchString(a.CommandName) {
+36 -283
View File
@@ -19,131 +19,56 @@ package armory
*/
import (
"errors"
"fmt"
"maps"
"os"
"slices"
"sort"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/alias"
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
)
type VersionInformation struct {
OldVersion string
NewVersion string
ArmoryName string
PackageID string
}
type PackageType uint
const (
AliasPackage PackageType = iota
ExtensionPackage
)
type UpdateIdentifier struct {
Type PackageType
Name string
}
// ArmoryUpdateCmd - Update all installed extensions/aliases
func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
var selectedUpdates []UpdateIdentifier
var err error
func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
con.PrintInfof("Refreshing package cache ... ")
clientConfig := parseArmoryHTTPConfig(cmd)
refresh(clientConfig)
con.Printf(console.Clearln + "\r")
armoryName, err := cmd.Flags().GetString("armory")
if err != nil {
con.PrintErrorf("Could not parse %q flag: %s\n", "armory", err)
return
}
// Find PK for the armory name
armoryPK := getArmoryPublicKey(armoryName)
// If the armory with the name is not found, print a warning
if cmd.Flags().Changed("armory") && armoryPK == "" {
con.PrintWarnf("Could not find a configured armory named %q - searching all configured armories\n\n", armoryName)
}
// Check packages for updates
aliasUpdates := checkForAliasUpdates(armoryPK)
extUpdates := checkForExtensionUpdates(armoryPK)
// Display a table of results
if len(aliasUpdates) > 0 || len(extUpdates) > 0 {
updateKeys := sortUpdateIdentifiers(aliasUpdates, extUpdates)
displayAvailableUpdates(con, updateKeys, aliasUpdates, extUpdates)
selectedUpdates, err = getUpdatesFromUser(updateKeys)
if err != nil {
if errors.Is(err, forms.ErrUserAborted) {
return
// Aliases
aliasUpdates := checkForAliasUpdates(clientConfig, con)
if 0 < len(aliasUpdates) {
con.PrintInfof("%d alias(es) out of date: %s\n", len(aliasUpdates), strings.Join(aliasUpdates, ", "))
for _, aliasName := range aliasUpdates {
err := installAliasPackageByName(aliasName, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to update %s: %s\n", aliasName, err)
}
con.PrintErrorf("%s\n", err.Error())
return
}
if len(selectedUpdates) == 0 {
return
}
} else {
con.PrintSuccessf("All packages are up to date\n")
return
con.PrintInfof("All aliases up to date!\n")
}
for _, update := range selectedUpdates {
switch update.Type {
case AliasPackage:
aliasVersionInfo, ok := aliasUpdates[update.Name]
if !ok {
continue
}
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, aliasVersionInfo.NewVersion)
// Extensions
extUpdates := checkForExtensionUpdates(clientConfig, con)
if 0 < len(extUpdates) {
con.PrintInfof("%d extension(s) out of date: %s\n", len(extUpdates), strings.Join(extUpdates, ", "))
for _, extName := range extUpdates {
err := installExtensionPackageByName(extName, clientConfig, con)
if err != nil {
con.PrintErrorf("Could not get update package for alias %s: %s\n", update.Name, err)
// Keep going because other packages might not encounter errors
continue
con.PrintErrorf("Failed to update %s: %s\n", extName, err)
}
err = installAliasPackage(updatedPackage, false, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to update %s: %s\n", update.Name, err)
}
case ExtensionPackage:
extVersionInfo, ok := extUpdates[update.Name]
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)
continue
}
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
if err != nil {
con.PrintErrorf("Failed to update %s: %s\n", update.Name, err)
}
default:
continue
}
} else {
con.PrintInfof("All extensions up to date!\n")
}
}
func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
cachedAliases, _ := packageManifestsInCache()
results := make(map[string]VersionInformation)
func checkForAliasUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) []string {
cachedAliases, _ := packagesInCache()
results := []string{}
for _, aliasManifestPath := range assets.GetInstalledAliasManifests() {
data, err := os.ReadFile(aliasManifestPath)
if err != nil {
@@ -154,44 +79,19 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
continue
}
for _, latestAlias := range cachedAliases {
/*
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,
we have to have some type of versioning. We do not have an official versioning scheme for packages,
so we will rely on the author of the package incrementing the version identifier somehow to determine
if a package is newer.
*/
if latestAlias.CommandName == localManifest.CommandName && latestAlias.Version > localManifest.Version {
if latestAlias.ArmoryPK == armoryPK || armoryPK == "" {
results[localManifest.CommandName] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestAlias.Version,
ArmoryName: latestAlias.ArmoryName,
}
}
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
// the armory differs at all from the local version, the extension is out of date.
if latestAlias.CommandName == localManifest.CommandName && latestAlias.Version != localManifest.Version {
results = append(results, localManifest.CommandName)
}
}
}
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 {
// Return a map of extension name to minimum version to install
results := make(map[string]VersionInformation)
func checkForExtensionUpdates(clientConfig ArmoryHTTPConfig, con *console.SliverConsoleClient) []string {
_, cachedExtensions := packagesInCache()
results := []string{}
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
data, err := os.ReadFile(extManifestPath)
if err != nil {
@@ -201,161 +101,14 @@ 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
for _, latestExt := range cachedExtensions {
// Right now we don't try to enforce any kind of versioning, it is assumed if the version from
// the armory differs at all from the local version, the extension is out of date.
if latestExt.Name == localManifest.Name && latestExt.Version != localManifest.Version {
results = append(results, localManifest.Name)
}
latestExt := cacheEntry.Extension
/*
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,
we have to have some type of versioning. We do not have an official versioning scheme for packages,
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.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,
}
}
}
}
return true
})
}
}
return results
}
func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInformation) []UpdateIdentifier {
/*
This function helps us keep updates straight when the user chooses from them. Just in case
an alias and an extension exist with the same name, we cannot simply combine the two maps.
We will assume that no two aliases and no two extensions have the same name.
*/
result := []UpdateIdentifier{}
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
for _, name := range aliasNames {
result = append(result, UpdateIdentifier{
Type: AliasPackage,
Name: name,
})
}
for _, name := range extensionNames {
result = append(result, UpdateIdentifier{
Type: ExtensionPackage,
Name: name,
})
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result
}
func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdentifier, aliasUpdates, extensionUpdates map[string]VersionInformation) {
var (
aliasSuffix string
extensionSuffix string
title string = "Available Updates (%d alias%s, %d extension%s)"
)
tw := table.NewWriter()
tw.SetAutoIndex(true)
tw.SetStyle(settings.GetTableStyle(con))
if len(aliasUpdates) != 1 {
aliasSuffix = "es"
}
if len(extensionUpdates) != 1 {
extensionSuffix = "s"
}
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix)))
tw.AppendHeader(table.Row{
"Package Name",
"Package Type",
"Installed Version",
"Available Version",
})
for _, key := range updateKeys {
var (
packageName string
packageType string
packageVersion VersionInformation
ok bool
)
switch key.Type {
case AliasPackage:
packageVersion, ok = aliasUpdates[key.Name]
if !ok {
continue
}
packageName = key.Name
packageType = "Alias"
case ExtensionPackage:
packageVersion, ok = extensionUpdates[key.Name]
if !ok {
continue
}
packageName = key.Name
packageType = "Extension"
default:
continue
}
tw.AppendRow(table.Row{
packageName,
packageType,
packageVersion.OldVersion,
fmt.Sprintf("%s (Armory: %s)", packageVersion.NewVersion, packageVersion.ArmoryName),
})
}
con.Printf("%s\n\n", tw.Render())
}
func getUpdatesFromUser(updateKeys []UpdateIdentifier) ([]UpdateIdentifier, error) {
options := make([]forms.ArmoryUpdateOption, 0, len(updateKeys))
optionIndex := make(map[string]UpdateIdentifier, len(updateKeys))
for _, key := range updateKeys {
packageType := "Alias"
if key.Type == ExtensionPackage {
packageType = "Extension"
}
optionID := fmt.Sprintf("%d:%s", key.Type, key.Name)
optionLabel := fmt.Sprintf("%s (%s)", key.Name, packageType)
options = append(options, forms.ArmoryUpdateOption{
ID: optionID,
Label: optionLabel,
})
optionIndex[optionID] = key
}
result, err := forms.ArmoryUpdateForm(options)
if err != nil {
return nil, err
}
chosenUpdates := make([]UpdateIdentifier, 0, len(result.SelectedIDs))
for _, selectedID := range result.SelectedIDs {
if update, ok := optionIndex[selectedID]; ok {
chosenUpdates = append(chosenUpdates, update)
}
}
return chosenUpdates, nil
}
+3 -9
View File
@@ -1,10 +1,4 @@
# client/command/backdoor
Backdoor
========
## Overview
Implements the 'backdoor' command group for the Sliver client console.
## Go Files
- `backdoor.go` – Implements the RPC workflow that backdoors a remote binary with Sliver shellcode for the active session.
- `commands.go` – Registers the Cobra command, binds flags/completions, and restricts usage to applicable targets.
Implements command related to `backdoor`
+4 -3
View File
@@ -21,13 +21,14 @@ package backdoor
import (
"fmt"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
)
// BackdoorCmd - Command to inject implant code into an existing binary.
func BackdoorCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// BackdoorCmd - Command to inject implant code into an existing binary
func BackdoorCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
session := con.ActiveTarget.GetSessionInteractive()
if session == nil {
return
-38
View File
@@ -1,38 +0,0 @@
package backdoor
import (
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
)
// Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
backdoorCmd := &cobra.Command{
Use: consts.BackdoorStr,
Short: "Infect a remote file with a sliver shellcode",
Long: help.GetHelpFor([]string{consts.BackdoorStr}),
Args: cobra.ExactArgs(1),
GroupID: consts.ExecutionHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
Run: func(cmd *cobra.Command, args []string) {
BackdoorCmd(cmd, con, args)
},
}
flags.Bind("", false, backdoorCmd, func(f *pflag.FlagSet) {
f.StringP("profile", "p", "", "profile to use for service binary")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(backdoorCmd, func(comp *carapace.ActionMap) {
(*comp)["profile"] = generate.ProfileNameCompleter(con)
})
carapace.Gen(backdoorCmd).PositionalCompletion(carapace.ActionValues().Usage("path to the remote file to backdoor"))
return []*cobra.Command{backdoorCmd}
}
+3 -13
View File
@@ -1,14 +1,4 @@
# client/command/beacons
Beacons
========
## Overview
Implements the 'beacons' command group for the Sliver client console. Handlers map Cobra invocations to beacons workflows such as prune, RM, and watch.
## Go Files
- `beacons.go` – Retrieves beacon inventories and renders them in tabular form with optional filtering.
- `commands.go` – Declares the beacons command set, binds flags/completions, and wires subcommands like prune, rm, and watch.
- `helpers.go` – Provides helper RPC wrappers for selecting, fetching, and listing beacons.
- `prune.go` – Implements the prune routine that removes stale beacon entries via the backend API.
- `rm.go` – Removes a specific beacon by ID, coordinating confirmation and RPC teardown.
- `watch.go` – Streams beacon events in a watch loop, handling cancellation and console interaction.
This package implements the beacons command, which can be used to list and interact with beacons.
+65 -170
View File
@@ -30,17 +30,15 @@ import (
"github.com/bishopfox/sliver/client/command/kill"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
)
// BeaconsCmd - Display/interact with beacons
func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func BeaconsCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
killFlag, _ := cmd.Flags().GetString("kill")
killAll, _ := cmd.Flags().GetBool("kill-all")
interact, _ := cmd.Flags().GetString("interact")
// Handle kill
if killFlag != "" {
@@ -74,18 +72,6 @@ func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
}
}
if interact != "" {
beacon, err := GetBeacon(con, interact)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
con.ActiveTarget.Set(nil, beacon)
con.PrintInfof("Active beacon %s (%s)\n", beacon.Name, strings.Split(beacon.ID, "-")[0])
return
}
filter, _ := cmd.Flags().GetString("filter")
var filterRegex *regexp.Regexp
if filterRe, _ := cmd.Flags().GetString("filter-re"); filterRe != "" {
@@ -108,7 +94,7 @@ func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
// PrintBeacons - Display a list of beacons
func PrintBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient) {
func PrintBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverConsoleClient) {
if len(beacons) == 0 {
con.PrintInfof("No beacons 🙁\n")
return
@@ -117,35 +103,30 @@ func PrintBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp
con.Printf("%s\n", tw.Render())
}
func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient) table.Writer {
func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverConsoleClient) table.Writer {
width, _, err := term.GetSize(0)
if err != nil {
width = 999
}
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
}
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
// preventing a second boundary from changing the table width while it is fitted.
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
windowsBeaconInList := false
for _, beacon := range beacons {
if beacon.OS == "windows" {
windowsBeaconInList = true
break
}
}
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
})
}
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
if layout.Compact {
wideTermWidth := con.Settings.SmallTermWidth < width
if wideTermWidth {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Operating System",
"Locale",
"Last Check-in",
"Next Check-in",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Name",
@@ -156,154 +137,68 @@ func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *re
"Last Check-in",
"Next Check-in",
})
} else {
header := table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
}
if includeIntegrity {
header = append(header, "Integrity")
}
header = append(header, "Operating System")
if layout.IncludeLocale {
header = append(header, "Locale")
}
header = append(header, "Last Check-in", "Next Check-in")
tw.AppendHeader(header)
}
activeBeaconID := ""
if con.ActiveTarget != nil {
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
activeBeaconID = activeBeacon.ID
}
}
for _, beacon := range beacons {
integrity := beacon.Integrity
if integrity == "" {
integrity = "-"
}
username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")
lastCheckin := time.Unix(beacon.LastCheckin, 0)
nextCheckin := time.Unix(beacon.NextCheckin, 0)
canonicalEntries := []string{
strings.Split(beacon.ID, "-")[0],
beacon.Name,
fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount),
beacon.Transport,
beacon.RemoteAddress,
beacon.Hostname,
username,
fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID),
integrity,
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
beacon.Locale,
formatBeaconDateDelta(lastCheckin, now, true, false),
formatBeaconDateDelta(nextCheckin, now, true, false),
}
if !beaconMatchesFilter(canonicalEntries, filter, filterRegex) {
continue
}
style := console.StyleNormal
if activeBeaconID == beacon.ID {
style = console.StyleGreen
color := console.Normal
activeBeacon := con.ActiveTarget.GetBeacon()
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
color = console.Green
}
// We need a slice of strings so we can apply filters
var rowEntries []string
if layout.Compact {
if wideTermWidth {
rowEntries = []string{
style.Render(strings.Split(beacon.ID, "-")[0]),
style.Render(beacon.Name),
style.Render(beacon.Transport),
style.Render(beacon.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
formatBeaconDateDelta(lastCheckin, now, false, false),
formatBeaconDateDelta(nextCheckin, now, false, true),
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
fmt.Sprintf(color+"%d/%d"+console.Normal, beacon.TasksCountCompleted, beacon.TasksCount),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
fmt.Sprintf(color+"%s"+console.Normal, beacon.RemoteAddress),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Locale),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
}
} else {
processName := beacon.Filename
if layout.ProcessBasename {
processName = targettable.ProcessName(processName)
}
rowEntries = []string{
style.Render(strings.Split(beacon.ID, "-")[0]),
style.Render(beacon.Name),
style.Render(fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount)),
style.Render(beacon.Transport),
style.Render(beacon.RemoteAddress),
style.Render(beacon.Hostname),
style.Render(username),
style.Render(fmt.Sprintf("%s (%d)", processName, beacon.PID)),
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
}
if includeIntegrity {
rowEntries = append(rowEntries, style.Render(integrity))
}
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)))
if layout.IncludeLocale {
rowEntries = append(rowEntries, style.Render(beacon.Locale))
}
rowEntries = append(rowEntries,
formatBeaconDateDelta(lastCheckin, now, true, false),
formatBeaconDateDelta(nextCheckin, now, true, true),
)
}
// Build the row struct
row := table.Row{}
for _, entry := range rowEntries {
row = append(row, entry)
}
tw.AppendRow(row)
// Apply filters if any
if filter == "" && filterRegex == nil {
tw.AppendRow(row)
} else {
for _, rowEntry := range rowEntries {
if filter != "" {
if strings.Contains(rowEntry, filter) {
tw.AppendRow(row)
break
}
}
if filterRegex != nil {
if filterRegex.MatchString(rowEntry) {
tw.AppendRow(row)
break
}
}
}
}
}
return tw
}
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
if filter == "" && filterRegex == nil {
return true
}
for _, entry := range entries {
if filter != "" && strings.Contains(entry, filter) {
return true
}
if filterRegex != nil && filterRegex.MatchString(entry) {
return true
}
}
return false
}
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
date := timestamp.Format(time.UnixDate)
interval := ""
if timestamp.Before(now) {
delta := now.Sub(timestamp).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (%s ago)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldRed.Render(interval)
}
} else {
delta := timestamp.Sub(now).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (in %s)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldGreen.Render(interval)
}
}
return interval
}
-257
View File
@@ -1,257 +0,0 @@
package beacons
import (
"regexp"
"strings"
"testing"
"time"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
beacon := testWindowsBeacon()
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("full process path missing from table:\n%s", rendered)
}
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
tests := []struct {
name string
beacon *clientpb.Beacon
hasIntegrity bool
wantProcess string
}{
{
name: "windows path",
beacon: testWindowsBeacon(),
hasIntegrity: true,
wantProcess: "winterfell.exe (2020)",
},
{
name: "posix path",
beacon: testLinuxBeacon(),
hasIntegrity: false,
wantProcess: "winterfell-agent (2020)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
beacons := []*clientpb.Beacon{tt.beacon}
fullLayout := targettable.Layout{
IncludeIntegrity: tt.hasIntegrity,
IncludeLocale: true,
}
basenameLayout := fullLayout
basenameLayout.ProcessBasename = true
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
if basenameWidth >= fullWidth {
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
if !strings.Contains(rendered, tt.wantProcess) {
t.Fatalf("process basename missing from table:\n%s", rendered)
}
if strings.Contains(rendered, tt.beacon.Filename) {
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, basenameWidth)
})
}
}
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
withAll := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
withoutIntegrity := withAll
withoutIntegrity.IncludeIntegrity = false
withoutLocale := withoutIntegrity
withoutLocale.IncludeLocale = false
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
}
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
}
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
beacon := testLinuxBeacon()
beacons := []*clientpb.Beacon{beacon}
layout := targettable.Layout{IncludeLocale: true}
width := beaconCandidateWidth(t, beacons, layout, false)
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
assertHeaderPresence(t, rendered, "Integrity", false)
assertHeaderPresence(t, rendered, "Locale", true)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
basenameLayout := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
withoutLocale := basenameLayout
withoutLocale.IncludeIntegrity = false
withoutLocale.IncludeLocale = false
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
t.Run("full process path survives basename projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
}
if strings.Contains(rendered, `AppData\Local\Temp`) {
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
}
})
t.Run("hidden integrity survives column projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
}
assertHeaderPresence(t, rendered, "Integrity", false)
})
}
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
beacon := testWindowsBeacon()
beacon.Integrity = ""
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if beacon.Integrity != "" {
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
}
}
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
if compactWidth >= withoutOptionalWidth {
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
assertHeaderPresence(t, rendered, omitted, false)
}
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
}
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, compactWidth)
}
func testWindowsBeacon() *clientpb.Beacon {
return &clientpb.Beacon{
ID: "0c9f6b27-1111-2222-3333-444444444444",
Name: "winterfell-session",
TasksCount: 3,
TasksCountCompleted: 2,
Transport: "mtls",
RemoteAddress: "10.13.37.11:49760",
Hostname: "winterfell",
Username: `winterfell\vagrant`,
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
PID: 2020,
Integrity: "High",
OS: "windows",
Arch: "amd64",
Locale: "en-US",
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
}
}
func testLinuxBeacon() *clientpb.Beacon {
beacon := testWindowsBeacon()
beacon.OS = "linux"
beacon.Hostname = "winterfell-linux"
beacon.Username = "vagrant"
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
beacon.Integrity = ""
return beacon
}
func testBeaconConsole() *console.SliverClient {
return &console.SliverClient{
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
}
}
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
t.Helper()
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
return text.LongestLineLen(tw.Render())
}
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
t.Helper()
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
return text.StripEscape(tw.Render())
}
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
t.Helper()
if got := text.LongestLineLen(rendered); got > width {
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
}
}
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
t.Helper()
headerLine := strings.SplitN(rendered, "\n", 2)[0]
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
}
}
-157
View File
@@ -1,157 +0,0 @@
package beacons
import (
"context"
"fmt"
"strings"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/commonpb"
)
// Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
beaconsCmd := &cobra.Command{
Use: consts.BeaconsStr,
Short: "Manage beacons",
Long: help.GetHelpFor([]string{consts.BeaconsStr}),
GroupID: consts.SliverHelpGroup,
Run: func(cmd *cobra.Command, args []string) {
BeaconsCmd(cmd, con, args)
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
f.BoolP("kill-all", "K", false, "kill all beacons")
f.StringP("interact", "i", "", "interact with a beacon")
f.BoolP("force", "F", false, "force killing the beacon")
f.StringP("filter", "f", "", "filter beacons by substring")
f.StringP("filter-re", "e", "", "filter beacons by regular expression")
})
flags.BindFlagCompletions(beaconsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = BeaconIDCompleter(con)
(*comp)["interact"] = BeaconIDCompleter(con)
})
registerBeaconIDFlagCompletion(beaconsCmd, "interact", con)
beaconsRmCmd := &cobra.Command{
Use: consts.RmStr,
Short: "Remove a beacon",
Long: help.GetHelpFor([]string{consts.BeaconsStr, consts.RmStr}),
Run: func(cmd *cobra.Command, args []string) {
BeaconsRmCmd(cmd, con, args)
},
}
carapace.Gen(beaconsRmCmd).PositionalCompletion(BeaconIDCompleter(con))
beaconsCmd.AddCommand(beaconsRmCmd)
beaconsWatchCmd := &cobra.Command{
Use: consts.WatchStr,
Short: "Watch your beacons",
Long: help.GetHelpFor([]string{consts.BeaconsStr, consts.WatchStr}),
Run: func(cmd *cobra.Command, args []string) {
BeaconsWatchCmd(cmd, con, args)
},
}
beaconsCmd.AddCommand(beaconsWatchCmd)
beaconsPruneCmd := &cobra.Command{
Use: consts.PruneStr,
Short: "Prune stale beacons automatically",
Long: help.GetHelpFor([]string{consts.BeaconsStr, consts.PruneStr}),
Run: func(cmd *cobra.Command, args []string) {
BeaconsPruneCmd(cmd, con, args)
},
}
flags.Bind("beacons", false, beaconsPruneCmd, func(f *pflag.FlagSet) {
f.StringP("duration", "d", "1h", "duration to prune beacons that have missed their last checkin")
})
beaconsCmd.AddCommand(beaconsPruneCmd)
return []*cobra.Command{beaconsCmd}
}
// BeaconIDCompleter completes beacon IDs
func BeaconIDCompleter(con *console.SliverClient) carapace.Action {
callback := func(_ carapace.Context) carapace.Action {
return carapace.ActionValuesDescribed(beaconCompletionPairs(con)...).Tag("beacons")
}
return carapace.ActionCallback(callback)
}
func registerBeaconIDFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
if cmd == nil {
return
}
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
return
}
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
return
}
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
values := describedValuesToTabs(beaconCompletionPairs(con))
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
})
}
func beaconCompletionPairs(con *console.SliverClient) []string {
results := make([]string, 0)
if con == nil || con.Rpc == nil {
return results
}
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
if err != nil {
return results
}
for _, b := range beacons.Beacons {
link := fmt.Sprintf("[%s <- %s]", b.ActiveC2, b.RemoteAddress)
id := fmt.Sprintf("%s (%d)", b.Name, b.PID)
userHost := fmt.Sprintf("%s@%s", b.Username, b.Hostname)
desc := strings.Join([]string{id, userHost, link}, " ")
results = append(results, b.ID[:8], desc)
}
return results
}
func describedValuesToTabs(values []string) []string {
tabbed := make([]string, 0, len(values)/2)
for i := 0; i+1 < len(values); i += 2 {
tabbed = append(tabbed, fmt.Sprintf("%s\t%s", values[i], values[i+1]))
}
return tabbed
}
func filterCompletionValues(values []string, prefix string) []string {
if prefix == "" {
return values
}
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
}
}
return filtered
}
+10 -5
View File
@@ -26,8 +26,9 @@ import (
"strings"
"text/tabwriter"
"github.com/AlecAivazis/survey/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
)
@@ -42,7 +43,7 @@ var (
)
// SelectBeacon - Interactive menu for the user to select an session, optionally only display live sessions
func SelectBeacon(con *console.SliverClient) (*clientpb.Beacon, error) {
func SelectBeacon(con *console.SliverConsoleClient) (*clientpb.Beacon, error) {
grpcCtx, cancel := con.GrpcContext(nil)
defer cancel()
beacons, err := con.Rpc.GetBeacons(grpcCtx, &commonpb.Empty{})
@@ -82,8 +83,12 @@ func SelectBeacon(con *console.SliverClient) (*clientpb.Beacon, error) {
options := strings.Split(outputBuf.String(), "\n")
options = options[:len(options)-1] // Remove the last empty option
prompt := &survey.Select{
Message: "Select a beacon:",
Options: options,
}
selected := ""
_ = forms.Select("Select a beacon:", options, &selected)
survey.AskOne(prompt, &selected)
if selected == "" {
return nil, ErrNoSelection
}
@@ -97,7 +102,7 @@ func SelectBeacon(con *console.SliverClient) (*clientpb.Beacon, error) {
return nil, ErrNoSelection
}
func GetBeacon(con *console.SliverClient, beaconID string) (*clientpb.Beacon, error) {
func GetBeacon(con *console.SliverConsoleClient, beaconID string) (*clientpb.Beacon, error) {
grpcCtx, cancel := con.GrpcContext(nil)
defer cancel()
beacons, err := con.Rpc.GetBeacons(grpcCtx, &commonpb.Empty{})
@@ -115,7 +120,7 @@ func GetBeacon(con *console.SliverClient, beaconID string) (*clientpb.Beacon, er
return nil, ErrBeaconNotFound
}
func GetBeacons(con *console.SliverClient) (*clientpb.Beacons, error) {
func GetBeacons(con *console.SliverConsoleClient) (*clientpb.Beacons, error) {
grpcCtx, cancel := con.GrpcContext(nil)
defer cancel()
beacons, err := con.Rpc.GetBeacons(grpcCtx, &commonpb.Empty{})
+4 -3
View File
@@ -21,16 +21,16 @@ package beacons
import (
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
)
// BeaconsPruneCmd - Prune stale beacons automatically
func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
duration, _ := cmd.Flags().GetString("duration")
pruneDuration, err := time.ParseDuration(duration)
if err != nil {
@@ -71,7 +71,8 @@ func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
}
con.Println()
confirm := false
_ = forms.Confirm("Prune these beacons?", &confirm)
prompt := &survey.Confirm{Message: "Prune these beacons?"}
survey.AskOne(prompt, &confirm)
if !confirm {
return
}
+1 -1
View File
@@ -25,7 +25,7 @@ import (
)
// BeaconsRmCmd - Display/interact with beacons
func BeaconsRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func BeaconsRmCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
beacon, err := SelectBeacon(con)
if err != nil {
con.PrintErrorf("%s\n", err)
+1 -1
View File
@@ -31,7 +31,7 @@ import (
)
// BeaconsWatchCmd - Watch your beacons in real-ish time
func BeaconsWatchCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func BeaconsWatchCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
done := waitForInput()
defer func() {
con.Printf(console.UpN+console.Clearln+"\r", 1)
-10
View File
@@ -1,10 +0,0 @@
# client/command/builders
## Overview
Implements the 'builders' command group for the Sliver client console.
## Go Files
- `builders.go` – Fetches external builder metadata and formats it for console display.
- `commands.go` – Registers the builders CLI command with its flags and completion helpers.
+6 -5
View File
@@ -23,16 +23,17 @@ import (
"fmt"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
// BuildersCmd - List external builders.
func BuildersCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// BuildersCmd - List external builders
func BuildersCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
builders, err := con.Rpc.Builders(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("%s", err)
@@ -45,7 +46,7 @@ func BuildersCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
}
}
func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverClient) {
func PrintBuilders(externalBuilders []*clientpb.Builder, con *console.SliverConsoleClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.AppendHeader(table.Row{
-28
View File
@@ -1,28 +0,0 @@
package builders
import (
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
buildersCmd := &cobra.Command{
Use: consts.BuildersStr,
Short: "List external builders",
Long: help.GetHelpFor([]string{consts.BuildersStr}),
Run: func(cmd *cobra.Command, args []string) {
BuildersCmd(cmd, con, args)
},
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("builders", false, buildersCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
return []*cobra.Command{buildersCmd}
}
-10
View File
@@ -1,10 +0,0 @@
# client/command/c2profiles
## Overview
Implements the 'c2profiles' command group for the Sliver client console. Handlers map Cobra invocations to c2profiles workflows such as C2 profiles.
## Go Files
- `c2profiles.go` – Implements listing, import/export, and generation routines for HTTP C2 profiles, including protobuf/JSON conversion helpers.
- `commands.go` – Binds the c2profiles command suite with flag handling and completions for profile management operations.
+166 -355
View File
@@ -21,34 +21,25 @@ package c2profiles
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
)
var (
ErrNoSelection = errors.New("no selection")
)
// C2ProfileCmd list available http profiles
func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func C2ProfileCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
profileName, _ := cmd.Flags().GetString("name")
if profileName == constants.DefaultC2Profile {
@@ -58,11 +49,7 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
return
}
if len(httpC2Profiles.Configs) != 1 {
profileName, err = selectC2Profile(httpC2Profiles.Configs)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
profileName = selectC2Profile(httpC2Profiles.Configs)
}
}
@@ -74,7 +61,8 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
PrintC2Profiles(profile, con)
}
func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) {
protocols := []string{constants.HttpStr, constants.HttpsStr}
profileName, _ := cmd.Flags().GetString("name")
if profileName == "" {
con.PrintErrorf("Invalid c2 profile name\n")
@@ -110,216 +98,29 @@ func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
con.PrintErrorf("%s\n", err)
return
}
}
func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
filepath, _ := cmd.Flags().GetString("file")
if filepath == "" {
con.PrintErrorf("Missing file path\n")
return
}
profileName, _ := cmd.Flags().GetString("name")
if profileName == "" {
con.PrintErrorf("Invalid c2 profile name\n")
return
}
if profileName == constants.DefaultC2Profile {
httpC2Profiles, err := con.Rpc.GetHTTPC2Profiles(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("failed to fetch HTTP C2 profiles: %s", err.Error())
return
}
if len(httpC2Profiles.Configs) != 1 {
profileName, err = selectC2Profile(httpC2Profiles.Configs)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
}
}
profile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: profileName})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
config, err := C2ConfigToJSON(profileName, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
jsonProfile, err := json.Marshal(config)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
err = os.WriteFile(filepath, jsonProfile, 0644)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
con.Println(profileName, "C2 profile exported to ", filepath)
}
func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// load template to use as starting point
template, err := cmd.Flags().GetString("template")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
profileName, _ := cmd.Flags().GetString("name")
if profileName == "" {
con.PrintErrorf("Invalid c2 profile name\n")
return
}
profile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: template})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
config, err := C2ConfigToJSON(profileName, profile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
// read urls files and replace segments
filepath, err := cmd.Flags().GetString("file")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
urlsFile, err := os.Open(filepath)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
fileContent, err := io.ReadAll(urlsFile)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
urls := strings.Split(string(fileContent), "\n")
jsonProfile, err := updateC2Profile(config, urls)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
// save or display config
importC2Profile, err := cmd.Flags().GetBool("import")
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
if importC2Profile {
httpC2ConfigReq := clientpb.HTTPC2ConfigReq{C2Config: C2ConfigToProtobuf(profileName, jsonProfile)}
_, err = con.Rpc.SaveHTTPC2Profile(context.Background(), &httpC2ConfigReq)
confirm := false
prompt := &survey.Confirm{Message: "Restart HTTP/S jobs?"}
survey.AskOne(prompt, &confirm)
if confirm {
var restartJobReq clientpb.RestartJobReq
jobs, err := con.Rpc.GetJobs(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
con.Println("C2 profile generated and saved as ", profileName)
} else {
PrintC2Profiles(C2ConfigToProtobuf(profileName, jsonProfile), con)
}
}
// reload jobs to include new profile
for _, job := range jobs.Active {
if job != nil && slices.Contains(protocols, job.Name) {
restartJobReq.JobIDs = append(restartJobReq.JobIDs, job.ID)
}
}
// convert protobuf to json
func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets.HTTPC2Config, error) {
implantConfig := assets.HTTPC2ImplantConfig{
UserAgent: profile.ImplantConfig.UserAgent,
ChromeBaseVersion: int(profile.ImplantConfig.ChromeBaseVersion),
MacOSVersion: profile.ImplantConfig.MacOSVersion,
NonceQueryArgChars: profile.ImplantConfig.NonceQueryArgChars,
NonceQueryLength: int(profile.ImplantConfig.NonceQueryLength),
NonceMode: profile.ImplantConfig.NonceMode,
MaxFileGen: int(profile.ImplantConfig.MaxFileGen),
MinFileGen: int(profile.ImplantConfig.MinFileGen),
MaxPathGen: int(profile.ImplantConfig.MaxPathGen),
MinPathGen: int(profile.ImplantConfig.MinFileGen),
MaxPathLength: int(profile.ImplantConfig.MaxPathLength),
MinPathLength: int(profile.ImplantConfig.MinPathLength),
Extensions: profile.ImplantConfig.Extensions,
}
var headers []assets.NameValueProbability
for _, header := range profile.ImplantConfig.Headers {
headers = append(headers, assets.NameValueProbability{
Name: header.Name,
Value: header.Value,
Probability: int(header.Probability),
Method: header.Method,
})
}
implantConfig.Headers = headers
var urlParameters []assets.NameValueProbability
for _, urlParameter := range profile.ImplantConfig.ExtraURLParameters {
urlParameters = append(urlParameters, assets.NameValueProbability{
Name: urlParameter.Name,
Value: urlParameter.Value,
Probability: int(urlParameter.Probability),
})
}
implantConfig.URLParameters = urlParameters
var (
files []string
paths []string
)
for _, pathSegment := range profile.ImplantConfig.PathSegments {
if pathSegment.IsFile {
files = append(files, pathSegment.Value)
} else {
paths = append(paths, pathSegment.Value)
_, err = con.Rpc.RestartJobs(context.Background(), &restartJobReq)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
}
implantConfig.Files = files
implantConfig.Paths = paths
var serverHeaders []assets.NameValueProbability
for _, header := range profile.ServerConfig.Headers {
serverHeaders = append(serverHeaders, assets.NameValueProbability{
Name: header.Name,
Value: header.Value,
Probability: int(header.Probability),
Method: header.Method,
})
}
var serverCookies []string
for _, cookie := range profile.ServerConfig.Cookies {
serverCookies = append(serverCookies, cookie.Name)
}
serverConfig := assets.HTTPC2ServerConfig{
RandomVersionHeaders: profile.ServerConfig.RandomVersionHeaders,
Headers: serverHeaders,
Cookies: serverCookies,
}
config := assets.HTTPC2Config{
ImplantConfig: implantConfig,
ServerConfig: serverConfig,
}
return &config, nil
}
// convert json to protobuf
@@ -330,66 +131,101 @@ func C2ConfigToProtobuf(profileName string, config *assets.HTTPC2Config) *client
pathSegments := []*clientpb.HTTPC2PathSegment{}
// files
for _, file := range config.ImplantConfig.Files {
for _, poll := range config.ImplantConfig.PollFiles {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: true,
Value: file,
IsFile: true,
SegmentType: 0,
Value: poll,
})
}
for _, path := range config.ImplantConfig.Paths {
for _, session := range config.ImplantConfig.SessionFiles {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: false,
Value: path,
IsFile: true,
SegmentType: 1,
Value: session,
})
}
for _, clientHeader := range config.ImplantConfig.Headers {
httpC2Headers = append(httpC2Headers, &clientpb.HTTPC2Header{
Method: clientHeader.Method,
Name: clientHeader.Name,
Value: clientHeader.Value,
Probability: int32(clientHeader.Probability),
for _, close := range config.ImplantConfig.CloseFiles {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: true,
SegmentType: 2,
Value: close,
})
}
for _, urlParameter := range config.ImplantConfig.URLParameters {
httpC2UrlParameters = append(httpC2UrlParameters, &clientpb.HTTPC2URLParameter{
Method: urlParameter.Method,
Name: urlParameter.Name,
Value: urlParameter.Value,
Probability: int32(urlParameter.Probability),
for _, stager := range config.ImplantConfig.StagerFiles {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: true,
SegmentType: 3,
Value: stager,
})
}
// paths
for _, poll := range config.ImplantConfig.PollPaths {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: false,
SegmentType: 0,
Value: poll,
})
}
for _, session := range config.ImplantConfig.SessionPaths {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: false,
SegmentType: 1,
Value: session,
})
}
for _, close := range config.ImplantConfig.ClosePaths {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: false,
SegmentType: 2,
Value: close,
})
}
for _, stager := range config.ImplantConfig.StagerPaths {
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
IsFile: false,
SegmentType: 3,
Value: stager,
})
}
implantConfig := &clientpb.HTTPC2ImplantConfig{
UserAgent: config.ImplantConfig.UserAgent,
ChromeBaseVersion: int32(config.ImplantConfig.ChromeBaseVersion),
MacOSVersion: config.ImplantConfig.MacOSVersion,
NonceQueryArgChars: config.ImplantConfig.NonceQueryArgChars,
NonceQueryLength: int32(config.ImplantConfig.NonceQueryLength),
NonceMode: config.ImplantConfig.NonceMode,
ExtraURLParameters: httpC2UrlParameters,
Headers: httpC2Headers,
MaxFileGen: int32(config.ImplantConfig.MaxFileGen),
MinFileGen: int32(config.ImplantConfig.MinFileGen),
MaxPathGen: int32(config.ImplantConfig.MaxPathGen),
MinPathGen: int32(config.ImplantConfig.MinPathGen),
MaxPathLength: int32(config.ImplantConfig.MaxPathLength),
MinPathLength: int32(config.ImplantConfig.MinPathLength),
Extensions: config.ImplantConfig.Extensions,
PathSegments: pathSegments,
UserAgent: config.ImplantConfig.UserAgent,
ChromeBaseVersion: int32(config.ImplantConfig.ChromeBaseVersion),
MacOSVersion: config.ImplantConfig.MacOSVersion,
NonceQueryArgChars: config.ImplantConfig.NonceQueryArgChars,
ExtraURLParameters: httpC2UrlParameters,
Headers: httpC2Headers,
MaxFiles: int32(config.ImplantConfig.MaxFiles),
MinFiles: int32(config.ImplantConfig.MinFiles),
MaxPaths: int32(config.ImplantConfig.MaxPaths),
MinPaths: int32(config.ImplantConfig.MinFiles),
StagerFileExtension: config.ImplantConfig.StagerFileExt,
PollFileExtension: config.ImplantConfig.PollFileExt,
StartSessionFileExtension: config.ImplantConfig.StartSessionFileExt,
SessionFileExtension: config.ImplantConfig.SessionFileExt,
CloseFileExtension: config.ImplantConfig.CloseFileExt,
PathSegments: pathSegments,
}
// Server Config
serverHeaders := []*clientpb.HTTPC2Header{}
for _, serverHeader := range config.ServerConfig.Headers {
serverHeaders = append(serverHeaders, &clientpb.HTTPC2Header{
Method: serverHeader.Method,
Name: serverHeader.Name,
Value: serverHeader.Value,
Probability: int32(serverHeader.Probability),
})
for _, method := range serverHeader.Methods {
serverHeaders = append(serverHeaders, &clientpb.HTTPC2Header{
Method: method,
Name: serverHeader.Name,
Value: serverHeader.Value,
Probability: int32(serverHeader.Probability),
})
}
}
serverCookies := []*clientpb.HTTPC2Cookie{}
@@ -412,7 +248,7 @@ func C2ConfigToProtobuf(profileName string, config *assets.HTTPC2Config) *client
}
// PrintImplantBuilds - Print the implant builds on the server
func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient) {
func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverConsoleClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
@@ -432,7 +268,7 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
var serverHeaders []string
for _, header := range profile.ServerConfig.Headers {
serverHeaders = append(serverHeaders, header.Name)
serverHeaders = append(serverHeaders, header.Value)
}
tw.AppendRow(table.Row{
"Server Headers",
@@ -457,7 +293,7 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
var clientHeaders []string
for _, header := range profile.ImplantConfig.Headers {
clientHeaders = append(clientHeaders, header.Name)
clientHeaders = append(clientHeaders, header.Value)
}
tw.AppendRow(table.Row{
"Client Headers",
@@ -488,139 +324,114 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
"Nonce query arg chars",
profile.ImplantConfig.NonceQueryArgChars,
})
tw.AppendRow(table.Row{
"Nonce query length",
profile.ImplantConfig.NonceQueryLength,
})
tw.AppendRow(table.Row{
"Nonce mode",
profile.ImplantConfig.NonceMode,
})
tw.AppendRow(table.Row{
"Max files",
profile.ImplantConfig.MaxFileGen,
profile.ImplantConfig.MaxFiles,
})
tw.AppendRow(table.Row{
"Min files",
profile.ImplantConfig.MinFileGen,
profile.ImplantConfig.MinFiles,
})
tw.AppendRow(table.Row{
"Max paths",
profile.ImplantConfig.MaxPathGen,
profile.ImplantConfig.MaxPaths,
})
tw.AppendRow(table.Row{
"Min paths",
profile.ImplantConfig.MinPathGen,
profile.ImplantConfig.MinPaths,
})
tw.AppendRow(table.Row{
"Max path length",
profile.ImplantConfig.MaxPathLength,
"Stager file extension",
profile.ImplantConfig.StagerFileExtension,
})
tw.AppendRow(table.Row{
"Min path length",
profile.ImplantConfig.MinPathLength,
"Start session file extension",
profile.ImplantConfig.StartSessionFileExtension,
})
tw.AppendRow(table.Row{
"File extensions",
strings.Join(profile.ImplantConfig.Extensions, ","),
"Session file extension",
profile.ImplantConfig.SessionFileExtension,
})
tw.AppendRow(table.Row{
"Poll file extension",
profile.ImplantConfig.PollFileExtension,
})
tw.AppendRow(table.Row{
"Close file extension",
profile.ImplantConfig.CloseFileExtension,
})
var (
paths []string
files []string
pollPaths []string
pollFiles []string
sessionPaths []string
sessionFiles []string
closePaths []string
closeFiles []string
)
for _, segment := range profile.ImplantConfig.PathSegments {
if segment.IsFile {
files = append(files, segment.Value)
switch segment.SegmentType {
case 0:
pollFiles = append(pollFiles, segment.Value)
case 1:
sessionFiles = append(sessionFiles, segment.Value)
case 2:
closeFiles = append(closeFiles, segment.Value)
}
} else {
paths = append(paths, segment.Value)
switch segment.SegmentType {
case 0:
pollPaths = append(pollPaths, segment.Value)
case 1:
sessionPaths = append(sessionPaths, segment.Value)
case 2:
closePaths = append(closePaths, segment.Value)
}
}
}
tw.AppendRow(table.Row{
"Paths",
strings.Join(paths[:], ",")[:50] + "...",
"Poll paths",
strings.Join(pollPaths[:], ","),
})
tw.AppendRow(table.Row{
"Files",
strings.Join(files[:], ",")[:50] + "...",
"Poll files",
strings.Join(pollFiles[:], ","),
})
tw.AppendRow(table.Row{
"Session paths",
strings.Join(sessionPaths[:], ","),
})
tw.AppendRow(table.Row{
"Session files",
strings.Join(sessionFiles[:], ","),
})
tw.AppendRow(table.Row{
"Close paths",
strings.Join(closePaths[:], ","),
})
tw.AppendRow(table.Row{
"Close files",
strings.Join(closeFiles[:], ","),
})
con.Println(tw.Render())
con.Println("\n")
}
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) (string, error) {
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
c2profile := ""
var choices []string
for _, c2profile := range c2profiles {
choices = append(choices, c2profile.Name)
}
_ = forms.Select("Select a c2 profile", choices, &c2profile)
if c2profile == "" {
return "", ErrNoSelection
prompt := &survey.Select{
Message: "Select a c2 profile",
Options: choices,
}
survey.AskOne(prompt, &c2profile, nil)
return c2profile, nil
}
func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
// update the template with the urls
var (
paths []string
filenames []string
extensions []string
)
for _, urlPath := range urls {
parsedURL, err := url.Parse(urlPath)
if err != nil {
fmt.Println("Error parsing URL:", err)
continue
}
dir, file := path.Split(parsedURL.Path)
dir = strings.Trim(dir, "/")
if dir != "" {
paths = append(paths, strings.Split(dir, "/")...)
}
if file != "" {
fileName := strings.TrimSuffix(file, filepath.Ext(file))
filenames = append(filenames, fileName)
ext := strings.TrimPrefix(filepath.Ext(file), ".")
if ext != "" {
extensions = append(extensions, ext)
}
}
}
slices.Sort(extensions)
extensions = slices.Compact(extensions)
slices.Sort(paths)
paths = slices.Compact(paths)
slices.Sort(filenames)
filenames = slices.Compact(filenames)
// 5 is arbitrarily used as a minimum value
if len(paths) < 5 {
return nil, fmt.Errorf("got %d paths need at least 5", len(paths))
}
if len(filenames) < 5 {
return nil, fmt.Errorf("got %d paths need at least 5", len(filenames))
}
template.ImplantConfig.Extensions = extensions
template.ImplantConfig.Paths = paths
template.ImplantConfig.Files = filenames
return template, nil
return c2profile
}
-95
View File
@@ -1,95 +0,0 @@
package c2profiles
import (
"github.com/bishopfox/sliver/client/command/completers"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/command/generate"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Commands returns the “ command and its subcommands.
func Commands(con *console.SliverClient) []*cobra.Command {
importC2ProfileCmd := &cobra.Command{
Use: consts.ImportC2ProfileStr,
Short: "Import HTTP C2 profile",
Long: help.GetHelpFor([]string{consts.ImportC2ProfileStr}),
Run: func(cmd *cobra.Command, args []string) {
ImportC2ProfileCmd(cmd, con, args)
},
}
flags.Bind(consts.ImportC2ProfileStr, false, importC2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
f.StringP("file", "f", "", "Path to C2 configuration file to import")
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
})
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
exportC2ProfileCmd := &cobra.Command{
Use: consts.ExportC2ProfileStr,
Short: "Export HTTP C2 profile",
Long: help.GetHelpFor([]string{consts.ExportC2ProfileStr}),
Run: func(cmd *cobra.Command, args []string) {
ExportC2ProfileCmd(cmd, con, args)
},
}
flags.Bind(consts.ExportC2ProfileStr, false, exportC2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
})
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con)
})
C2ProfileCmd := &cobra.Command{
Use: consts.C2ProfileStr,
Short: "Display C2 profile details",
Long: help.GetHelpFor([]string{consts.C2ProfileStr}),
Run: func(cmd *cobra.Command, args []string) {
C2ProfileCmd(cmd, con, args)
},
GroupID: consts.NetworkHelpGroup,
}
flags.Bind(consts.C2ProfileStr, true, C2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile to display")
})
flags.BindFlagCompletions(C2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["name"] = generate.HTTPC2Completer(con)
})
generateC2ProfileCmd := &cobra.Command{
Use: consts.C2GenerateStr,
Short: "Generate a C2 Profile from a list of urls",
Long: help.GetHelpFor([]string{consts.C2ProfileStr + "." + consts.C2GenerateStr}),
Run: func(cmd *cobra.Command, args []string) {
GenerateC2ProfileCmd(cmd, con, args)
},
}
flags.Bind(consts.GenerateStr, false, generateC2ProfileCmd, func(f *pflag.FlagSet) {
f.StringP("file", "f", "", "Path to file containing URL list, /hello/there.txt one per line")
f.BoolP("import", "i", false, "Import the generated profile after creation")
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
})
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
(*comp)["template"] = generate.HTTPC2Completer(con)
})
C2ProfileCmd.AddCommand(generateC2ProfileCmd)
C2ProfileCmd.AddCommand(importC2ProfileCmd)
C2ProfileCmd.AddCommand(exportC2ProfileCmd)
return []*cobra.Command{
C2ProfileCmd,
}
}
-12
View File
@@ -1,12 +0,0 @@
# client/command/certificates
## Overview
Implements the 'certificates' command group for the Sliver client console.
## Go Files
- `authorities.go` – Retrieves certificate authority metadata from the server and prints it in a table.
- `completions.go` – Provides tab completion helpers for certificate common names.
- `certificates.go` – Retrieves certificate metadata from the server, formats expiration status, and prints results to the console.
- `commands.go` – Registers the certificates command and binds the flags needed to query certificate inventories.
@@ -1,89 +0,0 @@
package certificates
import (
"context"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/term"
)
func CertificateAuthoritiesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
authorityInfo, err := con.Rpc.GetCertificateAuthorityInfo(context.Background(), &commonpb.Empty{})
if err != nil {
con.PrintErrorf("could not get certificate authority information from database: %s", err.Error())
return
}
printCertificateAuthorityInfo(con, authorityInfo.Info)
}
func printCertificateAuthorityInfo(con *console.SliverClient, authData []*clientpb.CertificateAuthorityData) {
width, _, err := term.GetSize(0)
if err != nil {
width = 999
}
if len(authData) == 0 {
con.PrintWarnf("There are no certificate authorities in the database.\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
wideTermWidth := con.Settings.SmallTermWidth < width
if wideTermWidth {
tw.AppendHeader(table.Row{
"ID",
"Common Name",
"Creation Time",
"Authority Type",
"Key Algorithm",
"Validity Start",
"Expires",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Common Name",
"Expires",
})
}
for _, authority := range authData {
rowStyle := console.StyleNormal
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
if err == nil {
rowStyle = checkCertExpiry(expiry)
}
if wideTermWidth {
tw.AppendRow(table.Row{
rowStyle.Render(authority.ID),
rowStyle.Render(authority.CN),
rowStyle.Render(authority.CreationTime),
rowStyle.Render(authority.Type),
rowStyle.Render(authority.KeyAlgorithm),
rowStyle.Render(authority.ValidityStart),
rowStyle.Render(authority.ValidityExpiry),
})
} else {
tw.AppendRow(table.Row{
rowStyle.Render(authority.ID),
rowStyle.Render(authority.CN),
rowStyle.Render(authority.ValidityExpiry),
})
}
}
tw.SortBy([]table.SortBy{{Name: "Expires", Mode: table.Dsc}})
con.Printf("%s\n", tw.Render())
}
-180
View File
@@ -1,180 +0,0 @@
package certificates
import (
"context"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"golang.org/x/term"
)
/*
Sliver Implant Framework
Copyright (C) 2024 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/>.
*/
const (
timeFormat = "2006-01-02 15:04:05 UTC-0700"
)
// Defining the transport filters
const (
MTLSTransport uint32 = 1 << iota
HTTPSTransport
AllTransports
)
// Defining the role filters
const (
// Provide some separation between the options so that we do not have duplicate combinations
ServerRole uint32 = 8 << iota
ImplantRole
AllRoles
)
func CertificateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// Since we are sending this value in a protobuf, we will give it a fixed bit size
// 32 is the smallest we can go
var chosenOptions uint32
if cmd.Flags().Changed("mtls") {
if cmd.Flags().Changed("https") {
chosenOptions = AllTransports
} else {
chosenOptions = MTLSTransport
}
} else if cmd.Flags().Changed("https") {
chosenOptions = HTTPSTransport
} else {
chosenOptions = AllTransports
}
if cmd.Flags().Changed("server") {
if cmd.Flags().Changed("implant") {
chosenOptions |= AllRoles
} else {
chosenOptions |= ServerRole
}
} else if cmd.Flags().Changed("implant") {
chosenOptions |= ImplantRole
} else {
chosenOptions |= AllRoles
}
request := &clientpb.CertificatesReq{
CategoryFilters: chosenOptions,
}
request.CN, _ = cmd.Flags().GetString("cn")
// Ask the server for information about certificates
certificateInfo, err := con.Rpc.GetCertificateInfo(context.Background(), request)
if err != nil {
con.PrintErrorf("could not get certificate information from database: %s", err.Error())
return
}
printCertificateInfo(con, certificateInfo.Info)
}
func checkCertExpiry(expiryTime time.Time) console.TextStyle {
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
return console.StyleBoldRed
}
// One week is 168 hours - this is bad
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
return console.StyleBoldRed
}
// One month is approximately 730 hours - this is a warning
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
return console.StyleBoldOrange
}
return console.StyleNormal
}
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
// Get the terminal width
width, _, err := term.GetSize(0)
if err != nil {
width = 999
}
if len(certData) == 0 {
con.PrintWarnf("There are no certificates in the database matching the given parameters.\n")
return
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
wideTermWidth := con.Settings.SmallTermWidth < width
if wideTermWidth {
tw.AppendHeader(table.Row{
"ID",
"Common Name",
"Creation Time",
"Certificate Type",
"Key Algorithm",
"Validity Start",
"Expires",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Common Name",
"Expires",
})
}
for _, cert := range certData {
rowStyle := console.StyleNormal
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
// This should not error out, but if it does, the row will not be colored
if err == nil {
rowStyle = checkCertExpiry(expiry)
}
if wideTermWidth {
tw.AppendRow(table.Row{
rowStyle.Render(cert.ID),
rowStyle.Render(cert.CN),
rowStyle.Render(cert.CreationTime),
rowStyle.Render(cert.Type),
rowStyle.Render(cert.KeyAlgorithm),
rowStyle.Render(cert.ValidityStart),
rowStyle.Render(cert.ValidityExpiry),
})
} else {
tw.AppendRow(table.Row{
rowStyle.Render(cert.ID),
rowStyle.Render(cert.CN),
rowStyle.Render(cert.ValidityExpiry),
})
}
}
tw.SortBy([]table.SortBy{{Name: "Expires", Mode: table.Dsc}})
con.Printf("%s\n", tw.Render())
}
-65
View File
@@ -1,65 +0,0 @@
package certificates
/*
Sliver Implant Framework
Copyright (C) 2024 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/flags"
"github.com/bishopfox/sliver/client/command/help"
"github.com/bishopfox/sliver/client/console"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func Commands(con *console.SliverClient) []*cobra.Command {
certificatesCmd := &cobra.Command{
Use: consts.CertificatesStr,
Short: "Certificate management",
Long: help.GetHelpFor([]string{consts.CertificatesStr}),
Run: func(cmd *cobra.Command, args []string) {
CertificateInfoCmd(cmd, con, args)
},
GroupID: consts.GenericHelpGroup,
}
flags.Bind(consts.CertificatesStr, false, certificatesCmd, func(f *pflag.FlagSet) {
f.BoolP("mtls", "m", false, "Show MTLS certificates")
f.BoolP("https", "p", false, "Show HTTPS certificates")
f.BoolP("implant", "i", false, "Show implant certificates")
f.BoolP("server", "s", false, "Show server certificates")
f.StringP("cn", "c", "", "Show certificate information for a provided common name")
})
flags.BindFlagCompletions(certificatesCmd, func(comp *carapace.ActionMap) {
(*comp)["cn"] = CertificateCommonNameCompleter(con)
})
registerCertificateCNFlagCompletion(certificatesCmd, "cn", con)
authoritiesCmd := &cobra.Command{
Use: "authorities",
Short: "List certificate authorities",
Run: func(cmd *cobra.Command, args []string) {
CertificateAuthoritiesCmd(cmd, con, args)
},
}
certificatesCmd.AddCommand(authoritiesCmd)
return []*cobra.Command{
certificatesCmd,
}
}
@@ -1,96 +0,0 @@
package certificates
import (
"context"
"sort"
"strings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// CertificateCommonNameCompleter completes common names for stored certificates.
func CertificateCommonNameCompleter(con *console.SliverClient) carapace.Action {
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
if con == nil || con.Rpc == nil {
return carapace.ActionMessage("not connected")
}
values, err := certificateCommonNameValues(con)
if err != nil {
return carapace.ActionMessage("failed to list certificates %s", err)
}
if len(values) == 0 {
return carapace.ActionMessage("no certificates")
}
return carapace.ActionValues(values...).Tag("common name").Usage("certificate common name")
})
}
func registerCertificateCNFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
if cmd == nil {
return
}
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
return
}
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
return
}
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
values, err := certificateCommonNameValues(con)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
})
}
func certificateCommonNameValues(con *console.SliverClient) ([]string, error) {
if con == nil || con.Rpc == nil {
return nil, nil
}
certInfo, err := con.Rpc.GetCertificateInfo(context.Background(), &clientpb.CertificatesReq{
CategoryFilters: AllTransports | AllRoles,
})
if err != nil {
return nil, err
}
unique := map[string]struct{}{}
for _, cert := range certInfo.Info {
if cert.CN == "" {
continue
}
unique[cert.CN] = struct{}{}
}
values := make([]string, 0, len(unique))
for name := range unique {
values = append(values, name)
}
sort.Strings(values)
return values, nil
}
func filterCompletionValues(values []string, prefix string) []string {
if prefix == "" {
return values
}
filtered := make([]string, 0, len(values))
for _, value := range values {
candidate := value
if before, _, ok := strings.Cut(value, "\t"); ok {
candidate = before
}
if strings.HasPrefix(candidate, prefix) {
filtered = append(filtered, value)
}
}
return filtered
}
-9
View File
@@ -1,9 +0,0 @@
# client/command/clean
## Overview
Implements the 'clean' command group for the Sliver client console.
## Go Files
- `clean.go` – Defines the clean command and its helpers that purge local build artifacts, cached profiles, and stale sessions.
-140
View File
@@ -1,140 +0,0 @@
package clean
/*
Sliver Implant Framework
Copyright (C) 2023 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"
"github.com/bishopfox/sliver/client/command/flags"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/spf13/cobra"
)
// CleanCmd - Remove all profiles, beacons, sessions and implant builds (Builds and logs will still exist on disk in .sliver)
func CleanCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
con.Printf("This command will kill and remove all sessions, beacons and profiles \n")
confirm := false
_ = forms.Confirm("Are you sure you want to destroy everything?", &confirm)
if !confirm {
return
}
err := removeSessionsAndBeacons(con)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
err = removeImplantBuilds(con)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
err = removeProfiles(con)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
con.Printf("All done !\n")
}
func removeImplantBuilds(con *console.SliverClient) error {
builds, err := con.Rpc.ImplantBuilds(context.Background(), &commonpb.Empty{})
if err != nil {
return err
}
for name, _ := range builds.Configs {
_, err := con.Rpc.DeleteImplantBuild(context.Background(), &clientpb.DeleteReq{
Name: name,
})
if err != nil {
return err
}
}
return nil
}
func removeProfiles(con *console.SliverClient) error {
profiles, err := con.Rpc.ImplantProfiles(context.Background(), &commonpb.Empty{})
if err != nil {
return err
}
for _, profile := range profiles.Profiles {
_, err := con.Rpc.DeleteImplantProfile(context.Background(), &clientpb.DeleteReq{
Name: profile.Name,
})
if err != nil {
return err
}
}
return nil
}
func removeSessionsAndBeacons(con *console.SliverClient) error {
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
if err != nil {
return err
}
for _, session := range sessions.Sessions {
_, err := con.Rpc.Kill(context.Background(), &sliverpb.KillReq{
Request: &commonpb.Request{
SessionID: session.ID,
Timeout: flags.DefaultTimeout,
},
Force: false,
})
if err != nil {
return err
}
}
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
if err != nil {
return err
}
for _, beacon := range beacons.Beacons {
_, err = con.Rpc.RmBeacon(context.Background(), &clientpb.Beacon{ID: beacon.ID})
if err != nil {
return err
}
}
return nil
}
// Commands returns the `exit` command.
func Command(con *console.SliverClient) []*cobra.Command {
return []*cobra.Command{{
Use: "clean",
Short: "Remove all profiles, beacons, sessions, implant builds and HTTP profiles (Builds and logs will still exist on disk in .sliver)",
Run: func(cmd *cobra.Command, args []string) {
CleanCmd(cmd, con, args)
},
GroupID: constants.GenericHelpGroup,
}}
}
+12 -69
View File
@@ -21,18 +21,19 @@ package command
import (
"strings"
client "github.com/bishopfox/sliver/client/console"
"github.com/reeflective/console"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Bind is a convenience function to bind flags to a given command.
const defaultTimeout = 60
// Flags is a convenience function to bind flags to a given command.
// name - The name of the flag set (can be empty).
// cmd - The command to which the flags should be bound.
// flags - A function exposing the flag set through which flags are declared.
func Bind(name string, persistent bool, cmd *cobra.Command, flags func(f *pflag.FlagSet)) {
func Flags(name string, persistent bool, cmd *cobra.Command, flags func(f *pflag.FlagSet)) {
flagSet := pflag.NewFlagSet(name, pflag.ContinueOnError) // Create the flag set.
flags(flagSet) // Let the user bind any number of flags to it.
@@ -43,21 +44,22 @@ func Bind(name string, persistent bool, cmd *cobra.Command, flags func(f *pflag.
}
}
// BindFlagCompletions is a convenience function for adding completions to a command's flags.
// FlagComps is a convenience function for adding completions to a command's flags.
// cmd - The command owning the flags to complete.
// bind - A function exposing a map["flag-name"]carapace.Action.
func BindFlagCompletions(cmd *cobra.Command, bind func(comp *carapace.ActionMap)) {
func FlagComps(cmd *cobra.Command, bind func(comp *carapace.ActionMap)) {
comps := make(carapace.ActionMap)
bind(&comps)
carapace.Gen(cmd).FlagCompletion(comps)
}
// RestrictTargets generates a cobra annotation map with a single console.CommandHiddenFilter key
// to a comma-separated list of filters to use in order to expose/hide commands based on requirements.
// Ex: cmd.Annotations = RestrictTargets("windows") will only show the command if the target is Windows.
// Ex: cmd.Annotations = RestrictTargets("windows", "beacon") show the command if target is a beacon on Windows.
func RestrictTargets(filters ...string) map[string]string {
// hideCommand generates a cobra annotation map with a single
// console.CommandHiddenFilter key, which value is a comma-separated list
// of filters to use in order to expose/hide commands based on requirements.
// Ex: cmd.Annotations = hideCommand("windows") will hide the cmd
// if the target session/beacon is not a Windows host.
func hideCommand(filters ...string) map[string]string {
if len(filters) == 0 {
return nil
}
@@ -74,62 +76,3 @@ func RestrictTargets(filters ...string) map[string]string {
console.CommandFilterKey: filts,
}
}
// makeBind returns a commandBinder helper function
// @menu - The command menu to which the commands should be bound (either server or implant menu).
func makeBind(cmd *cobra.Command, con *client.SliverClient) func(group string, cmds ...func(con *client.SliverClient) []*cobra.Command) {
return func(group string, cmds ...func(con *client.SliverClient) []*cobra.Command) {
found := false
// Ensure the given command group is available in the menu.
if group != "" {
for _, grp := range cmd.Groups() {
if grp.Title == group {
found = true
break
}
}
if !found {
cmd.AddGroup(&cobra.Group{
ID: group,
Title: group,
})
}
}
// Bind the command to the root
for _, command := range cmds {
cmd.AddCommand(command(con)...)
}
}
}
// commandBinder is a helper used to bind commands to a given menu, for a given "command help group".
//
// @group - Name of the group under which the command should be shown. Preferably use a string in the constants package.
// @ cmds - A list of functions returning a list of root commands to bind. See any package's `commands.go` file and function.
// type commandBinder func(group string, cmds ...func(con *client.SliverClient) []*cobra.Command)
// [ Core ]
// [ Sessions ]
// [ Execution ]
// [ Filesystem ]
// [ Info ]
// [ Network (C2)]
// [ Network tools ]
// [ Payloads ]
// [ Privileges ]
// [ Processes ]
// [ Aliases ]
// [ Extensions ]
// Commands not to bind in CLI:
// - portforwarders
// - Socks (and wg-socks ?)
// - shell ?
// Take care of:
// - double bind help command
// - double bind session commands
// - don't bind readline command in CLI.
Loaded 100 of 13177 files, more files were not shown because too many files have changed in this diff. Show more