Compare commits

..
Author SHA1 Message Date
moloch-- 436ec30c78 Reduce WASM extension gzip dependency in implants 2026-10-22 12:07:05 -07:00
Joe 9fdc66e4a8 Merge pull request #2414 from BishopFox/feat/go-garble-1.27.3
Update embedded Go and Garble to 1.27.3
2026-10-22 10:43:57 -07:00
moloch-- ab960fd4ed Make SOCKS receive test shutdown deterministic 2026-10-22 08:42:19 -07:00
moloch-- 34fc1c5ad6 Update embedded Go and Garble to 1.27.3 2026-10-22 08:23:51 -07:00
Joe ac65aa3708 Merge pull request #2412 from BishopFox/feat/fix-initial-prompt
Fix update reminder corrupting the initial console prompt
2026-10-21 15:30:21 -07:00
moloch-- 73bf782656 Avoid shadowing print in startup regression test 2026-10-21 08:19:39 -07:00
moloch-- a0a303ae89 Fix update reminder corrupting the initial console prompt 2026-10-21 08:16:22 -07:00
Joe 5383bd67b5 Merge pull request #2408 from skinnypuppy/fix/mcp-keepalive
client/cli: keep the MCP bridge alive across idle gRPC channels
2026-10-19 12:50:42 -07:00
Joe 73ea63fd44 Merge pull request #2405 from rob-cshub/feat/non-inter-stage
Add non-interactive support for 'implants stage'
2026-10-19 12:03:33 -07:00
Joe 3ef04241a1 Merge pull request #2410 from BishopFox/dependabot/github_actions/actions/setup-go-7
build(deps): bump actions/setup-go from 6 to 7
2026-10-19 12:03:07 -07:00
dependabot[bot] 402b5459e6 build(deps): bump actions/setup-go from 6 to 7
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6 to 7.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 18:38:45 +00:00
Joe 7c56553372 Merge pull request #2409 from BishopFox/dependabot/github_actions/actions/configure-pages-6
build(deps): bump actions/configure-pages from 5 to 6
2026-10-19 11:36:44 -07:00
dependabot[bot] a40754eaac build(deps): bump actions/configure-pages from 5 to 6
Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6.
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-19 03:03:00 +00:00
skinnypuppy 042bb6bccc client/cli: keep the MCP bridge alive across idle gRPC channels
gRPC-Go closes client transports after its default 30-minute idle
timeout (no RPCs in flight) and moves the channel to Idle; the next
RPC transparently reconnects. The MCP bridge installed the
connection-lost watchdog, which treats any Idle transition as a lost
connection and exits the process - so `sliver-client mcp` reliably
died ~30 minutes after connecting, forcing the MCP host to reconnect.

Do not install the watchdog for the MCP bridge (an idle channel simply
reconnects on the next tool call; a down server surfaces as an RPC
error to the MCP host instead of a dead bridge process) and enable the
client keepalive pings that #2273 added but never wired up, so dead
connections are detected quickly while the channel is in use.

Client keepalive pings do not reset the idle timer, so this cannot be
fixed by configuration alone.
2026-10-18 14:44:56 -06:00
Robert Pretschner 8bc7273cad Add non-interactive mode to 'implants stage' 2026-10-15 14:11:36 +02:00
Joe 8e5d2f9dbe Merge pull request #2406 from BishopFox/feat/go-garble-1.27.2
Update embedded Go and Garble to 1.27.2
2026-10-14 08:51:22 -07:00
moloch-- 52864c481f Fix lint toolchain for Go 1.27.2 2026-10-14 07:04:43 -07:00
moloch-- 2f6fca1eac Update embedded Go and Garble to 1.27.2 2026-10-13 21:58:13 -07:00
Joe 481b5b0534 Merge pull request #2397 from BishopFox/dependabot/github_actions/actions/checkout-7
build(deps): bump actions/checkout from 6 to 7
2026-10-12 12:48:13 -07:00
Joe 65efd3f2fd Merge pull request #2398 from BishopFox/dependabot/github_actions/actions/upload-pages-artifact-5
build(deps): bump actions/upload-pages-artifact from 4 to 5
2026-10-12 12:47:57 -07:00
Joe 58c5f33cf5 Merge pull request #2399 from BishopFox/dependabot/github_actions/actions/cache-6
build(deps): bump actions/cache from 5 to 6
2026-10-12 12:47:43 -07:00
Joe 3e20660ab0 Merge pull request #2400 from BishopFox/dependabot/github_actions/actions/setup-node-7
build(deps): bump actions/setup-node from 6 to 7
2026-10-12 12:47:28 -07:00
Joe acb6cd2d77 Merge pull request #2401 from BishopFox/dependabot/github_actions/actions/deploy-pages-5
build(deps): bump actions/deploy-pages from 4 to 5
2026-10-12 12:47:13 -07:00
dependabot[bot] 7f82085467 build(deps): bump actions/deploy-pages from 4 to 5
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:24 +00:00
dependabot[bot] 1ac72de74d build(deps): bump actions/setup-node from 6 to 7
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:20 +00:00
dependabot[bot] 94df7a3e58 build(deps): bump actions/cache from 5 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:15 +00:00
dependabot[bot] 329e44dfd9 build(deps): bump actions/upload-pages-artifact from 4 to 5
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:08 +00:00
dependabot[bot] 5d303fca79 build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-12 07:24:01 +00:00
Joe 90f759dac8 Merge pull request #2395 from BishopFox/feat/defensive-security-fixes
Random Bug Fixes
2026-10-10 10:05:26 -07:00
Joe 1fc9e256c0 Merge pull request #2396 from zwanski2019/feature/docs-search-relevance
Improve embedded docs search relevance
2026-10-10 09:48:26 -07:00
moloch-- 13935b2e68 fix(cli): preserve configuration import exit codes 2026-10-10 09:34:17 -07:00
moloch-- 1db7b99685 fix(websites): preserve content in metadata-only requests 2026-10-10 09:34:08 -07:00
zwanski2019andCopilot b48e3fc537 Improve embedded docs search relevance
Search document content using normalized multi-term queries and rank title matches above body matches. Add focused tests for content search and ranking.

Co-authored-by: Copilot <[email protected]>
2026-10-09 11:30:59 +01:00
moloch-- 10ec67d274 fix(logging): omit SQLite connection strings from startup logs 2026-10-08 09:07:43 -07:00
moloch-- 52f5517c2a fix(tests): make regression assertions portable to Windows 2026-10-08 08:47:37 -07:00
moloch-- c4ed103182 fix(ci): resolve lint findings in defensive fixes 2026-10-08 08:47:37 -07:00
moloch-- c04b0884a5 fix(tests): run discovered docs and protobuf packages 2026-10-08 08:47:37 -07:00
moloch-- 893fe4ccda fix(client): propagate configuration import failures 2026-10-08 08:47:37 -07:00
moloch-- b43ef0fabe fix(client): keep diagnostic log open through command execution 2026-10-08 08:47:37 -07:00
moloch-- 25d2bffb5a fix(update): select Windows executable release assets 2026-10-08 08:47:37 -07:00
moloch-- 534763f087 fix(websites): persist zero-byte content replacements 2026-10-08 08:47:37 -07:00
moloch-- ec733974de fix(websites): target MIME updates by existing content path 2026-10-08 08:47:37 -07:00
moloch-- d9811baa7e fix(update): preserve prior download until verified replacement 2026-10-08 08:47:37 -07:00
moloch-- d8b360ecea fix(config): preserve database connection values in driver DSNs 2026-10-08 08:47:37 -07:00
moloch-- 12c55be611 fix(config): propagate failed saves and atomically replace files 2026-10-08 08:47:37 -07:00
moloch-- 3a147f1e01 fix(logging): redact administrative secrets from audit and RPC logs 2026-10-08 08:47:36 -07:00
moloch-- 5b03320a32 fix(auth): complete operator revocation after cleanup errors 2026-10-06 21:47:06 -07:00
moloch-- 1cc6d35dc6 fix(websites): restrict recursive removal to selected path 2026-10-06 21:34:23 -07:00
moloch-- 1c5d8ab5a9 Add Sliver GUI download to docs landing page 2026-10-05 17:50:28 -07:00
moloch-- 04a71e6d7d Merge branch 'feat/docs-npm-updates' 2026-10-03 21:07:03 -07:00
moloch-- bb8b90be56 Update docs 2026-10-03 21:06:48 -07:00
Joe 7a2b58ade8 Merge pull request #2391 from BishopFox/feat/docs-npm-updates
build(docs): update npm dependencies
2026-10-03 13:45:51 -07:00
moloch-- 854143b593 build(docs): update npm dependencies 2026-10-03 13:32:35 -07:00
Joe d3068394bf Merge pull request #2390 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/next-16.3.6
build(deps): bump next from 16.3.3 to 16.3.6 in /docs/sliver-docs
2026-10-03 13:30:04 -07:00
Joe d80db8a932 Merge pull request #2388 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/dompurify-3.4.16
build(deps): bump dompurify from 3.4.13 to 3.4.16 in /docs/sliver-docs
2026-10-03 13:29:56 -07:00
Joe 179ff728dd Merge pull request #2389 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/multi-05c505ab13
build(deps): bump brace-expansion in /docs/sliver-docs
2026-10-03 13:29:45 -07:00
moloch-- 422044af0d fix(ci): preserve pinned CNA fixture bytes on Windows 2026-10-03 10:17:29 -07:00
dependabot[bot] 7dbdd1931c build(deps): bump next from 16.3.3 to 16.3.6 in /docs/sliver-docs
Bumps [next](https://github.com/vercel/next.js) from 16.3.3 to 16.3.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.3...v16.3.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:56:48 +00:00
dependabot[bot] 8b7e1b63ab build(deps): bump brace-expansion in /docs/sliver-docs
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 1.1.16 to 1.1.21
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.16...v1.1.21)

Updates `brace-expansion` from 5.0.8 to 5.0.12
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.16...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 5.0.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:55:54 +00:00
dependabot[bot] ed5c89253c build(deps): bump dompurify from 3.4.13 to 3.4.16 in /docs/sliver-docs
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.13 to 3.4.16.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.13...3.4.16)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-03 00:55:48 +00:00
Joe 06b5a56e74 Merge pull request #2387 from BishopFox/feat/console-multiline-piped-commands
fix(console): run pasted and piped commands in sequence
2026-10-02 17:53:01 -07:00
moloch-- 704912d25a fix(console): resolve CLI lint findings 2026-10-01 20:52:56 -07:00
moloch-- ea1fe95408 fix(console): run pasted and piped commands in sequence 2026-10-01 20:28:48 -07:00
moloch-- 8fe54f243e Tweak readme 2026-09-26 11:19:40 -07:00
moloch-- 4fa6fe86fe Fix doc names 2026-09-26 11:08:55 -07:00
moloch-- 23c5a0b164 Update docs with details on cross-platform bof/coff support 2026-09-26 11:04:41 -07:00
Joe 7b8dafcf55 Merge pull request #2380 from MKMithun2806/fix/client-console-non-tty-oom
fix(client): refuse interactive console on non-TTY stdin
2026-09-20 14:14:35 -07:00
moloch-- 4e6531fa98 test: wait for tunnel deadline cleanup 2026-09-20 11:10:07 -07:00
moloch-- 6e9b2a9820 fix(client): skip interactive loop after non-TTY rc 2026-09-20 10:48:23 -07:00
MKMithun2806 b11f85d4d7 Merge remote-tracking branch 'upstream/master' into fix/client-console-non-tty-oom
# Conflicts:
#	client/console/console.go
2026-09-20 10:17:07 +00:00
MKMithun2806 23f1341684 test(client): add unit tests for non-TTY interactive console guard
Exercises the nonTTYGuard helper introduced in the previous commit
to verify:
- error returned when stdin is not a TTY and no --rc is provided
- nil returned when --rc content is supplied (rc already ran)
- nil returned when stdin is a TTY (normal interactive path)
2026-09-20 09:49:28 +00:00
MKMithun2806 e68e163456 fix(client): refuse interactive console on non-TTY stdin (#2379)
When stdin is not a terminal, the interactive console run-loop
spins with ENOTTY from readline, rebuilding the full command tree
every iteration and consuming memory unboundedly (300-400 MB/s RSS).

Fail closed before entering the loop:
- Non-TTY without --rc: return a clear error.
- Non-TTY with --rc: rc script already ran, return nil without
  calling App.Start() so --rc remains usable for automation.

Uses golang.org/x/term (already vendored) for the TTY check.
No vendor, server, or command-tree changes.
2026-09-20 09:49:02 +00:00
Joe f8430cecf7 Merge pull request #2378 from BishopFox/feat/rportfwd-first-frame-master
fix: preserve reverse port forward first frames
2026-09-19 21:27:44 -07:00
moloch-- 276a30fbaa test: normalize Windows corpus line endings 2026-09-19 19:54:18 -07:00
moloch-- 3aa8a9f93c fix: satisfy reverse precreate lint checks 2026-09-19 19:35:41 -07:00
moloch-- fc3fb7386a fix: preserve reverse port forward first frames
(cherry picked from commit a89c03bb87)
2026-09-19 19:26:43 -07:00
moloch-- 6edea618e9 Remove .gitattributes 2026-09-19 18:33:37 -07:00
moloch-- e920f2f8ea Add RegistryType to read resp 2026-09-19 18:24:15 -07:00
Joe 5b4a03c9c3 Merge pull request #2375 from BishopFox/feat/dependabot-consolidated-2026-09-15
build(deps): consolidate nine Dependabot updates
2026-09-15 15:36:28 -07:00
moloch-- e93fabecd6 build(deps): consolidate open Dependabot updates 2026-09-15 14:35:45 -07:00
Joe b2cae511e6 Merge pull request #2372 from jimmexploit/add-service-name-flag
Add --service-name flag to generate for Windows service implants
2026-09-14 07:34:13 -07:00
Joe 2e8e53789f Merge pull request #2374 from BishopFox/crackstation/top2
Redesign crack top as a real-time dashboard
2026-09-12 17:38:53 -07:00
moloch-- 6a08fb2f7c Fix crack top lint findings 2026-09-12 17:11:48 -07:00
moloch-- 76d38d4677 Improve 'crack top' view 2026-09-12 16:53:33 -07:00
moloch-- 3f87964397 Redesign crack top 2026-09-12 15:39:43 -07:00
moloch-- bb19ba0665 Implement crack cancel/pause/resume 2026-09-12 12:37:01 -07:00
Joe 262851373d Merge pull request #2373 from BishopFox/crackstation/top
Add crack top dashboard and benchmark cache policy
2026-09-12 10:31:15 -07:00
moloch-- 4afcd5bc9d Close crack top polling test database 2026-09-12 09:58:58 -07:00
moloch-- c746a5262c Fix crack top lint findings 2026-09-12 09:32:58 -07:00
moloch-- e91cdf117f Add crack top and more hashcat benchmark options 2026-09-12 09:18:37 -07:00
moloch-- 1fb671f71b crack top 2026-09-12 09:03:48 -07:00
Ubuntu 40df2701d1 Add --service-name flag for Windows service implants 2026-09-12 05:12:33 +00:00
moloch-- c43347886b Update hash types 2026-09-11 16:48:42 -07:00
Joe 4ef33f389c Merge pull request #2371 from BishopFox/crackstation/v1
Crackstation Updates
2026-09-11 15:22:59 -07:00
moloch-- acd52520e4 Fix crackstation lint findings 2026-09-11 14:56:19 -07:00
moloch-- 9a373e20f0 Update hash types to latest version of hashcat 2026-09-11 14:19:19 -07:00
moloch-- aad186a632 improved crackstation support 2026-09-11 12:47:58 -07:00
moloch-- ec9d9b2cc1 improved crackstation support 2026-09-11 09:18:48 -07:00
moloch-- 05d3b2604b crackstation implemented 2026-09-10 11:22:06 -07:00
Joe c3e8a97f66 Merge pull request #2354 from BishopFox/dependabot/go_modules/github.com/tetratelabs/wazero-1.12.0
build(deps): bump github.com/tetratelabs/wazero from 1.11.0 to 1.12.0
2026-09-04 11:27:42 -07:00
dependabot[bot] 547a56696d build(deps): bump github.com/tetratelabs/wazero from 1.11.0 to 1.12.0
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-03 23:42:44 +00:00
moloch-- 84bb2397c7 Merge pull request #2357 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/browserslist-4.28.8 2026-09-03 16:26:41 -07:00
moloch-- 503d040885 Merge pull request #2355 from BishopFox/dependabot/go_modules/github.com/alecthomas/chroma/v2-2.27.0 2026-09-03 16:26:37 -07:00
moloch-- 801452eeff Merge pull request #2341 from BishopFox/dependabot/go_modules/google.golang.org/protobuf-1.36.12 2026-09-03 16:26:03 -07:00
moloch-- f6c7d73014 Merge pull request #2343 from BishopFox/dependabot/go_modules/golang.org/x/crypto-0.55.0 2026-09-03 16:25:12 -07:00
moloch-- 228757f656 Merge pull request #2358 from BishopFox/dependabot/go_modules/google.golang.org/grpc-1.83.1 2026-09-03 16:24:09 -07:00
dependabot[bot] 738010e990 build(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.1
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.82.1 to 1.83.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.1)

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-31 03:03:42 +00:00
Joe 67c6d0d356 Merge branch 'master' into fix/shell-win 2026-08-30 10:06:50 -07:00
Joe 5e3b432b9b Merge pull request #2352 from BishopFox/feat/opfor-cna-client
feat(client): add OPFOR CNA BOF scripting
2026-08-30 10:06:32 -07:00
moloch-- fc4d9a1c36 Improvements to windows/shell handling 2026-08-30 10:06:00 -07:00
moloch-- 55b97df9f7 fix(client): repair OPFOR CI portability 2026-08-29 18:47:04 -07:00
moloch-- 227328fa87 docs: document OPFOR CNA scripting 2026-08-29 14:05:08 -07:00
moloch-- 071106d0bf test(e2e): add OPFOR native BOF coverage 2026-08-29 14:04:50 -07:00
moloch-- 8c326e6af9 feat(client): add OPFOR CNA BOF scripting 2026-08-29 14:04:27 -07:00
moloch-- 5e9ba47bf4 feat(opfor): preserve typed BOF output 2026-08-29 14:04:05 -07:00
moloch-- 0537e285fc build(deps): vendor OPFOR v0.0.1 and Reflektor v0.0.8 2026-08-29 14:03:42 -07:00
moloch-- ca685f5eed Require e2e tests for release workflow 2026-08-28 11:27:28 -07:00
Joe 902999493b Merge pull request #2351 from BishopFox/feat/wireguard-opt-in-auto-client
Make multiplayer WireGuard explicitly opt-in
2026-08-28 10:10:58 -07:00
moloch-- 14363c5d2a Make multiplayer WireGuard explicitly opt-in 2026-08-28 09:06:37 -07:00
Joe 926b7ae1a7 Merge pull request #2350 from BishopFox/feat/sgn-v0.1.0-stability
Fix SGN x64 stability and strengthen shellcode E2E coverage
2026-08-28 07:22:05 -07:00
moloch-- bf815b36ed Fix asset downloader lint findings 2026-08-27 18:04:14 -07:00
moloch-- 637853e8c2 Retry transient asset downloads safely 2026-08-27 14:48:38 -07:00
moloch-- 0c022ec722 Update SGN for x64 safe-mode stack alignment 2026-08-27 13:09:36 -07:00
moloch-- 7c83b458fa Fix SGN stability and expand shellcode E2E coverage 2026-08-27 10:39:32 -07:00
moloch-- f447a170c6 Close UUID test database on Windows 2026-08-26 14:00:49 -07:00
moloch-- d565592773 Fix Go 1.27 CI validation 2026-08-26 13:37:58 -07:00
moloch-- c21da4dceb Modernize host code for Go 1.27 2026-08-26 13:13:50 -07:00
moloch-- b08e2ff5c0 Merge branch 'master' into feat/go-1.27-server-client-build 2026-08-26 12:32:17 -07:00
Joe 09b0540e75 Merge pull request #2349 from BishopFox/feat/armory-package-identity
Fix Armory extension updates and archive installs
2026-08-26 12:07:49 -07:00
moloch-- b4005efda8 Fix Armory PR lint failures 2026-08-26 11:03:58 -07:00
moloch-- 6d086fd7a7 Fix Armory package resolution and archive installs 2026-08-26 09:21:38 -07:00
Joe ccebd505b0 Merge pull request #2348 from BishopFox/fix/extensions-rm-external-path
Fix extensions rm deleting external directories
2026-08-25 16:21:38 -07:00
rkervella 94c5022dd0 fix(client): preserve external extension directories 2026-08-25 10:46:13 -07:00
Joe b19e383111 Merge pull request #2347 from BishopFox/feat/reflektor-default-bof-execution
Use Reflektor for default BOF execution
2026-08-25 06:56:20 -07:00
moloch-- 02ebd99c68 Fix Reflektor BOF lint failures 2026-08-24 21:30:08 -07:00
moloch-- 83d82e7161 feat: use Reflektor for default BOF execution 2026-08-24 21:13:51 -07:00
moloch-- c481a4f5db Tweaks for go v1.27 2026-08-24 17:31:01 -07:00
moloch-- eb5ac1b309 Only run reflektor tests on e2e 2026-08-24 16:49:00 -07:00
Joe e73618e0a5 Merge pull request #2339 from BishopFox/e2e/comprehensive
test: add comprehensive server and implant E2E coverage
2026-08-24 16:25:55 -07:00
moloch-- 0017fd2fe8 test: reduce extension callback test complexity 2026-08-24 09:53:49 -07:00
moloch-- e44111b6b4 fix: defer native extension rpc responses 2026-08-24 09:47:09 -07:00
moloch-- aa2dce510a test: run darwin shellcode e2e on macos 26 2026-08-24 06:45:15 -07:00
moloch-- 5e6acde3c7 test: surface darwin shellcode loader failures 2026-08-23 23:03:26 -07:00
moloch-- 99a8d3f859 fix: reduce xor dynamic stub complexity 2026-08-23 19:41:18 -07:00
moloch-- fc15a47b1e fix: satisfy shellcode encoder lint 2026-08-23 19:38:16 -07:00
moloch-- 8b32b6525f fix: stabilize shellcode encoders 2026-08-23 19:35:30 -07:00
moloch-- 9e0fafb785 fix: keep shellcode runner alive after return 2026-08-23 18:58:11 -07:00
moloch-- 12ebffb16c test: add shellcode execution e2e matrix 2026-08-23 18:47:39 -07:00
moloch-- b539762de7 test: cover chmod and chown on darwin 2026-08-23 15:39:14 -05:00
moloch-- e71af809d4 fix: support chmod and chown on darwin 2026-08-23 15:39:14 -05:00
moloch-- f3ffa3323b ci: fix cross-platform lint checks 2026-08-23 15:39:14 -05:00
moloch-- 778331d474 ci: export command coverage markdown 2026-08-23 15:39:14 -05:00
moloch-- 8d259577af ci: restore manual-only comprehensive trigger 2026-08-23 15:39:14 -05:00
moloch-- aa108a66a8 ci: restore comprehensive E2E matrix 2026-08-23 15:39:13 -05:00
moloch-- 44e5464950 fix Windows arm64 WireGuard reconnects 2026-08-23 15:39:13 -05:00
moloch-- 1381200b33 fix WireGuard key exchange peer readiness 2026-08-23 15:39:13 -05:00
moloch-- f81b250a04 ci: focus Windows arm64 WireGuard diagnostics 2026-08-23 15:39:13 -05:00
moloch-- 8b4ee5f5c5 test: add opt-in implant debug logging 2026-08-23 15:39:13 -05:00
moloch-- 68cb1363e8 test: preserve E2E command failure diagnostics 2026-08-23 15:39:12 -05:00
moloch-- 2344a3ae50 fix overlapping beacon check-ins 2026-08-23 15:39:12 -05:00
moloch-- 8cec5164b7 fix implant WireGuard device teardown 2026-08-23 15:39:12 -05:00
moloch-- be50c25cb2 fix WireGuard beacon result delivery receipt 2026-08-23 15:39:12 -05:00
moloch-- ee2cc83aa1 test: accept partial Windows service inventories 2026-08-23 15:39:12 -05:00
moloch-- 8e62e2dcd9 ci: temporarily run comprehensive E2E on branch pushes 2026-08-23 15:39:11 -05:00
moloch-- 1347059639 ci: add comprehensive E2E workflow 2026-08-23 15:39:11 -05:00
moloch-- 29efea561c test: add comprehensive implant command E2E suite 2026-08-23 15:39:11 -05:00
moloch-- 82deca2b0f test beacon empty check-in receive path 2026-08-23 15:39:11 -05:00
moloch-- 808b3b6408 fix beacon result delivery across reconnects 2026-08-23 15:39:11 -05:00
moloch-- eba10ebbfc fix Windows arm64 process architecture detection 2026-08-23 15:39:11 -05:00
moloch-- 13903e46a8 fix Linux arm64 memfd creation 2026-08-23 15:39:10 -05:00
moloch-- 8aaaa583bd fix Linux arm64 process architecture detection 2026-08-23 15:39:10 -05:00
moloch-- 4b428065e2 fix WireGuard key exchange response parsing 2026-08-23 15:39:10 -05:00
moloch-- 578f9a28d0 fix UDP-only netstat responses 2026-08-23 15:39:10 -05:00
moloch-- ce2f494f96 fix directory upload overwrite truncation 2026-08-23 15:39:10 -05:00
moloch-- c1c36491a6 fix cd handler error reporting 2026-08-23 15:39:09 -05:00
Joe a5c3482c6d Merge pull request #2335 from kyzzniko-lang/fix/mcp-auth-token
fix(mcp): make `mcp start` usable without the interactive confirmation
2026-08-19 14:12:23 -07:00
Joe b428274515 Merge pull request #2336 from BishopFox/reflektor-ext
Support native extensions on macOS and Linux
2026-08-19 13:24:59 -07:00
moloch-- 5dc6adae47 Pin Reflektor v0.0.6 2026-08-19 13:33:44 -05:00
moloch-- 264b00f3c7 Pin Reflektor v0.0.5 2026-08-19 12:22:06 -05:00
moloch-- ada79c8ecd Use Reflektor native loader for Unix extensions 2026-08-19 11:06:49 -05:00
moloch-- edd4c3d097 Support native extensions on macOS and Linux
Replace the Darwin universal loader with Reflektor, add Linux extension handlers and a shared Unix callback ABI, fix client registration ordering and target visibility, and extend the Reflektor integration lifecycle across supported platforms.
2026-08-19 09:36:44 -05:00
kyzzniko-lang 38f84a4f6f fix(mcp): make mcp start usable without the interactive confirmation
`mcp start` gated the whole startup behind an interactive huh confirmation
("Do you know what prompt injection is and are you an adult?"). On terminals
where those interactive forms break (#2133) the form error was discarded,
the prompt silently evaluated to false, and users were stopped with an
unrelated "not qualified" message before the server ever started: no auth
token in the output and no ~/.sliver-client/mcp.yaml, which is what #2294
reports.

- add --yes/-y to `mcp start` to skip the confirmation (the same
  flags-over-menus escape hatch recommended in #2133), and point the
  rejection message at it and at `settings autoadult`
- print the auth config path next to the token in `mcp start` and `mcp`
  status output so users can locate mcp.yaml
- surface the confirmation form error in IsUserAnAdultWithPrompt instead
  of swallowing it, so a broken terminal reports why it failed

Closes #2294
2026-08-19 19:29:47 +08:00
Joe aee0958e99 Merge pull request #2329 from BishopFox/dependabot/go_modules/charm.land/lipgloss/v2-2.0.6
chore(deps): bump charm.land/lipgloss/v2 from 2.0.4 to 2.0.6
2026-08-18 13:36:26 -07:00
Joe 23c067982a Merge pull request #2315 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/mermaid-11.16.1
chore(deps): bump mermaid from 11.15.0 to 11.16.1 in /docs/sliver-docs
2026-08-18 13:36:12 -07:00
dependabot[bot] 7e917da3ac chore(deps): bump charm.land/lipgloss/v2 from 2.0.4 to 2.0.6
Bumps [charm.land/lipgloss/v2](https://github.com/charmbracelet/lipgloss) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/charmbracelet/lipgloss/releases)
- [Commits](https://github.com/charmbracelet/lipgloss/compare/v2.0.4...v2.0.6)

---
updated-dependencies:
- dependency-name: charm.land/lipgloss/v2
  dependency-version: 2.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-18 03:08:02 +00:00
Joe 674b49f41f Merge pull request #2303 from BishopFox/dependabot/go_modules/github.com/jedib0t/go-pretty/v6-6.8.3
chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.7.8 to 6.8.3
2026-08-17 20:05:17 -07:00
moloch-- 4ba04df621 Only run reflektor tests on changes to implant/... 2026-08-17 20:02:29 -07:00
Joe 3274382bc7 Merge pull request #2333 from BishopFox/docs-refresh
Docs refresh
2026-08-17 19:58:50 -07:00
moloch-- 069d57b5e5 Polish docs landing and section content 2026-08-17 19:57:11 -07:00
moloch-- e023211c02 Polish docs branding and landing terminal 2026-08-17 19:35:23 -07:00
moloch-- a48006640d Fix docs scrolling and client navigation 2026-08-17 19:26:35 -07:00
moloch-- 764fd0eddf Refresh docs site design and navigation 2026-08-17 18:47:32 -07:00
dependabot[bot] 9c9943c94c chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.7.8 to 6.8.3
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.7.8 to 6.8.3.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.7.8...v6.8.3)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  dependency-version: 6.8.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-18 00:10:18 +00:00
Joe 2e9b52371e Merge pull request #2332 from BishopFox/reflektor-tests
Test Sliver shared libraries with Reflektor
2026-08-17 17:07:02 -07:00
moloch-- 6d6df68972 Update Reflektor to v0.0.3
Synchronize the root and implant module graphs and vendor trees with the all-platform Go c-shared loader fixes.
2026-08-17 00:29:26 -07:00
moloch-- 7f863d3172 Fix cross-platform Reflektor integration failures
Close rendered implant files before import renames, use native GCC for Linux/386 Go shared libraries, and preserve actionable integration logs.
2026-08-17 00:28:49 -07:00
moloch-- 6ecb379bce Test Sliver shared libraries with Reflektor 2026-08-16 22:09:22 -07:00
Joe db45c4aaf9 Merge pull request #2328 from BishopFox/reflektor
Use Reflektor for implant extensions
2026-08-16 18:57:56 -07:00
moloch-- 198913d6f8 Use Reflektor for implant extensions 2026-08-15 21:54:40 -07:00
Joe cd85824185 Merge pull request #2326 from kyzzniko-lang/fix/alias-arg-passthrough
fix(alias): keep alias args intact without "--"
2026-08-15 15:45:27 -07:00
kyzzniko-lang 63797ac79c fix(alias): split splitAliasArgs up, gocyclo was at 26
golangci-lint caps cyclomatic complexity at 15. Break the token matcher,
value consumption and numeric validation into their own functions; the
main loop only dispatches now. No behaviour change, tests unchanged.
2026-08-15 13:37:23 +08:00
kyzzniko-lang 4633409623 fix(alias): keep alias args intact without "--"
Alias commands had it worse than extensions: pflag hard-errors on flags it
does not know, so a payload with its own arguments (amsi-bypass -pid 6969)
could only be invoked through cobra's "--" separator, and every alias help
text pointed to a wiki page explaining the trick (#2264).

Same treatment extensions got in #2323: alias commands now run with
DisableFlagParsing and runAliasCommand pulls the Sliver-owned flags out of
the raw slice itself (args.go). The flag table owns --process,
--process-arguments, --ppid, --save, --timeout and --help for every alias,
plus the .NET assembly set (--method, --class, --app-domain, --arch,
--in-process, --runtime, --amsi-bypass, --etw-bypass) for assembly aliases.
Everything else is handed to the payload verbatim, including glued
shorthands: "-pid" is deliberately NOT read as -p with value "id", which is
exactly the mangling behind #2264. Parsed values are written back onto the
flag set so Request()'s --timeout and the Get* readers keep working, and
"--" still forces everything after it to the payload for name collisions.

Drops the wiki workaround warning from the generated help text.

Closes #2264
2026-08-15 12:00:09 +08:00
Joe 13c2d33449 Merge pull request #2323 from kyzzniko-lang/fix/extension-bof-arg-passthrough
fix(extensions): keep BOF/extension args intact without "--"
2026-08-14 09:48:43 -07:00
kyzzniko-lang 8cf478d7b0 fix(extensions): remove unused flag-name maps flagged by golangci-lint
splitExtensionArgs matches Sliver-owned flags with literal comparisons, so the
extensionOwnedBoolFlags/extensionOwnedIntFlags package vars were never read.
golangci-lint's unused linter rejected them, failing CI on darwin/linux/windows.
Drop the maps and fold the design notes into splitExtensionArgs's doc comment.
2026-08-14 11:44:21 +08:00
Joe f55215ff93 Merge pull request #2324 from MKMithun2806/feat/profiles-generate-external-builder
Feat/profiles generate external builder
2026-08-13 11:32:29 -07:00
Ronan Kervella b5d3d0bad6 Merge branch 'master' into dependabot/npm_and_yarn/docs/sliver-docs/mermaid-11.16.1 2026-08-13 09:41:06 -07:00
Mitchaster a32d81252c Merge branch 'BishopFox:master' into feat/profiles-generate-external-builder 2026-08-13 21:44:34 +05:30
MKMithun2806 a1537535c4 test: cover external-builder flag on profiles generate
Verify the profiles generate subcommand exposes the -E/--external-builder
flag with the same semantics as generate, so the external builder path is
wired and discoverable.

Signed-off-by: MKMithun2806 <[email protected]>
2026-08-13 16:11:22 +00:00
MKMithun2806 61dd34e989 feat: use external builder for profiles generate
Mirror the generate command: when --external-builder is set, call the
existing externalBuild() helper instead of the local compile() path, and
handle ErrNoExternalBuilder / ErrNoValidBuilders with the same user-facing
messages as generate.

Signed-off-by: MKMithun2806 <[email protected]>
2026-08-13 16:09:17 +00:00
MKMithun2806 2b67cb917b feat: add --external-builder flag to profiles generate
Add the same -E/--external-builder flag already used by generate and
generate beacon so profiles generate can offload builds to an external
builder.

Signed-off-by: MKMithun2806 <[email protected]>
2026-08-13 16:08:39 +00:00
kyzzniko-lang ceead92ec5 fix(extensions): keep BOF/extension args intact without "--"
Extension commands were registered with pflag's
ParseErrorsWhitelist.UnknownFlags, which silently drops every unknown flag
together with its value. The extension's own flags (--type, --fqdn, ...) are
unknown to pflag, so they were stripped before reaching the BOF parser, and
operators had to insert cobra's "--" separator as a workaround (#2309).

Run extension commands with DisableFlagParsing and split the Sliver-owned
flags (--save, --timeout, --help) out of the raw argument slice ourselves,
then pass the rest untouched to the BOF/DLL parser. The parsed values are
written back onto the flag set so Request()'s --timeout (and any other
cmd.Flags() reader) still observes them.

Named extension flags now work with and without "--", quoted values and
positional arguments are preserved, and single/double-dash forms still reach
the BOF parser.

Closes #2309
2026-08-13 22:03:00 +08:00
Joe df9ed94635 Merge pull request #2322 from nkbeast/fix/nil-session-deref-crash
fix(server): prevent nil session dereference crash in tunnel/socks/pivot paths
2026-08-12 22:00:48 -07:00
Joe e891613f6f Merge pull request #2318 from MKMithun2806/fix/httpc2-header-probability
fix: default missing HTTPC2Header probability to 100
2026-08-12 22:00:31 -07:00
dependabot[bot] 528b04427a chore(deps): bump mermaid from 11.15.0 to 11.16.1 in /docs/sliver-docs
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.15.0 to 11.16.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/[email protected]@11.16.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.16.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-13 04:00:22 +00:00
Joe f60a0e91d3 Merge pull request #2317 from BishopFox/dependabot/npm_and_yarn/docs/sliver-docs/dompurify-3.4.13
build(deps): bump dompurify from 3.4.12 to 3.4.13 in /docs/sliver-docs
2026-08-12 20:57:38 -07:00
Joe 2308314529 Merge pull request #2319 from BishopFox/dependabot/go_modules/gorm.io/driver/postgres-1.6.2
chore(deps): bump gorm.io/driver/postgres from 1.6.0 to 1.6.2
2026-08-12 20:57:20 -07:00
NK 04bc82bca3 fix(server): prevent nil session dereference crash in tunnel/socks/pivot paths
Sessions.Get() can return nil when an implant session is removed (e.g. the
implant dies or times out) between the RPC handler's nil check and the
tunnel creation call. Both tunnels.Create() and SocksTunnels.Create()
dereferenced the session unconditionally, and since the gRPC server has no
panic recovery interceptor, the panic took down the entire server and all
active sessions (see #1969).

Also guard the tunnel data goroutines (resend, to-implant sender) and the
pivot graph ToProtobuf() against a vanished session, logging and skipping
instead of panicking.

Adds core.ErrInvalidSessionID mapped to codes.InvalidArgument in rpcError().
2026-08-12 09:42:26 +05:30
dependabot[bot] acdfa33231 chore(deps): bump gorm.io/driver/postgres from 1.6.0 to 1.6.2
Bumps [gorm.io/driver/postgres](https://github.com/go-gorm/postgres) from 1.6.0 to 1.6.2.
- [Commits](https://github.com/go-gorm/postgres/compare/v1.6.0...v1.6.2)

---
updated-dependencies:
- dependency-name: gorm.io/driver/postgres
  dependency-version: 1.6.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-10 03:03:31 +00:00
Mitchaster a03592de81 Merge branch 'master' into fix/httpc2-header-probability 2026-08-09 22:57:55 +05:30
Joe 5cf9d088e8 Merge pull request #2316 from MKMithun2806/docs/remove-generate-stager
docs: note the generate stager command was removed
2026-08-09 09:35:03 -07:00
MKMithun2806 db3294b926 fix: default missing HTTPC2Header probability to 100
When probability is omitted from http-c2.json headers it stayed at the
protobuf zero value (0) and was never applied by the implant. Coerce any
header with a zero probability to 100 during config normalization so the
header is always sent.

Signed-off-by: MKMithun2806 <[email protected]>
2026-08-09 16:04:52 +00:00
dependabot[bot] 1847b8c24b build(deps): bump dompurify from 3.4.12 to 3.4.13 in /docs/sliver-docs
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.12 to 3.4.13.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-09 09:20:01 +00:00
MKMithun2806 79e4259081 docs: note the generate stager command was removed
The Sliver generate stager subcommand was removed in recent releases. Update
the Stagers documentation to make clear stagers must now be generated with
Metasploit's msfvenom or custom tooling, and correct the HTTP(S) stager
section heading to reflect supported schemes.
2026-08-08 17:39:12 +00:00
Joe 9ff9b55352 Merge pull request #2305 from Luppa90/fix/mtls-wg-frame-bounds
fix(implant): bound mTLS and WireGuard envelope allocation from length prefix
2026-08-05 09:42:30 -07:00
Joe 9767e24edd Merge pull request #2312 from BishopFox/dependabot/go_modules/golang.org/x/text-0.40.0
Bump golang.org/x/text from 0.37.0 to 0.40.0
2026-08-05 09:42:13 -07:00
dependabot[bot] bb572f3846 Bump golang.org/x/text from 0.37.0 to 0.40.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.37.0 to 0.40.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.37.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-03 03:03:48 +00:00
Joe 01c5b5cee4 Merge pull request #2306 from Luppa90/fix/canary-nil-connection-panic
fix(server): guard Session.ToProtobuf against nil Connection
2026-07-31 15:52:17 -07:00
Luppa 1049dc4a98 fix(lint): add package comment to wireguard test file
Satisfies the revive package-comments rule that was failing CI on all
three platforms.
2026-07-31 21:55:49 +01:00
Luppa d1b0ba13c4 fix(server): guard Session.ToProtobuf against nil Connection
When a DNS canary fires, handleCanary publishes an event containing a
Session with only the Name field set (Connection is nil). The RPC event
stream calls ToProtobuf() on that Session, which dereferences Connection
for Transport, RemoteAddress, LastCheckin, and IsDead — causing a nil
pointer panic that crashes the server.

Guard the Connection-dependent fields so ToProtobuf returns zero values
for transport metadata when Connection is nil, instead of panicking.

Fixes BishopFox/sliver#1435
2026-07-31 10:49:07 +01:00
Luppa 2d98f14149 fix(implant): bound mTLS and WireGuard envelope allocation from length prefix
Same vulnerability fixed for pivots in #2292: a corrupted or desynced
4-byte length prefix could trigger an allocation up to ~4 GiB, crashing
the implant. Cap at 512 MiB (matching the pivot transport limit) and
return a descriptive error instead of allocating.

Also fixes stale "[pivot]" log tags in both transports' zero-length
error paths.
2026-07-31 08:55:30 +01:00
Joe 19e82064b1 Merge pull request #2292 from Luppa90/fix/pivot-frame-bounds
fix(implant): bound pivot frame allocation from length prefix
2026-07-30 14:34:54 -07:00
Joe 3b72374685 Merge pull request #2291 from Luppa90/fix/log-rotation
fix(server): rotate server logs to bound disk usage
2026-07-30 14:34:36 -07:00
Luppa 20fb8c4975 fix(implant): handle Close errors in pivot tests (errcheck) 2026-07-30 11:07:01 +01:00
Luppa 68829c0a40 fix(server): handle Close error in rotation test (errcheck) 2026-07-30 10:53:46 +01:00
Joe 2c18062f4d Merge branch 'master' into fix/log-rotation 2026-07-29 17:38:56 -07:00
Joe f17ce0afbe Merge pull request #2302 from BishopFox/dependabot/go_modules/charm.land/glamour/v2-2.0.1
Bump charm.land/glamour/v2 from 2.0.0 to 2.0.1
2026-07-29 17:38:16 -07:00
dependabot[bot] 116b12885c Bump charm.land/glamour/v2 from 2.0.0 to 2.0.1
Bumps [charm.land/glamour/v2](https://github.com/charmbracelet/glamour) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/charmbracelet/glamour/releases)
- [Commits](https://github.com/charmbracelet/glamour/compare/v2.0.0...v2.0.1)

---
updated-dependencies:
- dependency-name: charm.land/glamour/v2
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-28 00:31:47 +00:00
Joe 3d9860db6c Merge pull request #2301 from BishopFox/dependabot/go_modules/gorm.io/gorm-1.31.2
Bump gorm.io/gorm from 1.31.0 to 1.31.2
2026-07-27 17:29:18 -07:00
dependabot[bot] 1a6579e954 Bump gorm.io/gorm from 1.31.0 to 1.31.2
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.31.0 to 1.31.2.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.31.0...v1.31.2)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-version: 1.31.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-27 03:03:55 +00:00
Joe 2659d848d7 Merge pull request #2298 from BishopFox/feat/golangci-lint
Replace Go Report Card with golangci-lint
2026-07-26 17:08:34 -07:00
moloch-- df6571f63f Fix linting without generated assets 2026-07-26 15:18:21 -07:00
moloch-- aab1470e08 Replace Go Report Card with golangci-lint 2026-07-26 14:49:42 -07:00
Joe 19d43fdc37 Merge pull request #2295 from ph0neh0me/docs/go-archive-validation
docs: add go archive validation examples
2026-07-26 14:47:25 -07:00
Joe 3b09e5ea80 Merge pull request #2297 from BishopFox/feat/dependabot-follow-up
Fix remaining Dependabot advisories
2026-07-26 14:10:38 -07:00
moloch-- b904d29b25 Update implant security dependencies 2026-07-26 08:57:27 -07:00
moloch-- 8e2b069a7d Fix remaining Dependabot advisories 2026-07-26 08:08:26 -07:00
Joe 096c418d27 Merge pull request #2296 from BishopFox/feat/dependabot-security-fixes
Fix Dependabot security alerts
2026-07-25 18:47:29 -07:00
moloch-- d9e241ccb5 Fix flaky website content test 2026-07-25 14:19:06 -07:00
moloch-- 086da95525 Fix Dependabot security alerts 2026-07-24 16:48:14 -07:00
Peter Georgiev 27ed557e55 docs: add go archive validation examples 2026-07-25 01:46:46 +03:00
Joe af7c823508 Merge pull request #2290 from ph0neh0me/feature/go-archive-output
Add Go c-archive output format
2026-07-23 17:54:34 -07:00
Joe f2a3d8ee6d Merge pull request #2293 from ecapuano/docs/update-soc-analyst-link
Docs: update stale SOC analyst blog post link
2026-07-22 21:49:00 -07:00
Eric Capuano de7215d2c0 docs: update stale SOC analyst blog link in Community Guides 2026-07-20 22:09:20 -05:00
Luppa 2d555b16d9 fix(implant): bound pivot frame allocation from length prefix
Both pivot readers (NetConnPivot and NetConnPivotClient) allocated
make([]byte, dataLength) directly from a peer's 4-byte length prefix with only
a lower-bound (<= 0) check. A corrupted or desynced prefix -- e.g. from a
framing desync during a transfer over a pivot -- can therefore be read as a
value up to ~4GiB and trigger an allocation large enough to crash the implant
(reported in #1452 with a 300KiB download over an http -> named-pipe pivot).

Add an upper bound (MaxFrameLength, 512MiB) and reject oversized frames with
ErrFrameTooLarge before allocating. The constant and error live in the pivots
package and are reused by pivotclients, which already imports it.

Adds tests covering rejection of an oversized prefix (with a timeout guard so a
regression fails fast instead of hanging) and a valid-frame round-trip.
2026-07-18 17:33:03 +01:00
Luppa 597a2571fc fix(server): rotate server logs to bound disk usage
The server log sinks (sliver.json, sliver.log, audit.json) were opened as
plain append-only file handles with no rotation, so a sustained error
condition (e.g. a hot RPC/accept error path) can grow them without bound and
exhaust the host disk, locking out the operator.

Route each sink through lumberjack. Rotation starts with safe built-in
defaults at logger init and is retuned from LogConfig after the server config
loads, mirroring the existing SetLevel pattern (the log package cannot import
server/configs -- import cycle). Configs that predate the new fields keep the
built-in defaults, so rotation is never accidentally disabled.

Adds LogConfig knobs (max_size_mb, max_backups, max_age_days, compress) and
unit tests covering the defaults, override/zero-value semantics, and real
on-disk rotation. New log files are created 0600 (more restrictive than the
previous 0644 for sliver.json/sliver.log).
2026-07-18 17:16:18 +01:00
Luppa 409b4c8fdb chore: vendor gopkg.in/natefinch/lumberjack.v2
Dependency for bounded server log rotation. Kept in a distinct commit from
the functional change per CONTRIBUTING.md ("Any changes to vendor/ should be
in a distinct commit").
2026-07-18 17:16:17 +01:00
Peter Georgiev 2e72bc96b9 Add Go c-archive output format 2026-07-18 15:09:09 +03:00
Joe fa9557348e Merge pull request #2286 from Functionhx/fix/sliver-1775-proxy-ua
fix: set ProxyConnectHeader to prevent UA leak on proxy CONNECT
2026-07-17 15:18:03 -07:00
Functionhx 4768e36074 fix: set ProxyConnectHeader to use spoofed User-Agent
Set http.Transport.ProxyConnectHeader with the generated User-Agent so
HTTPS CONNECT requests through a proxy use the spoofed UA instead of
leaking the default Go-http-client identifier.

Fixes BishopFox/sliver#1775

Signed-off-by: Yuchen Fan <[email protected]>
2026-07-17 14:00:07 +08:00
Joe e53f66de72 Merge pull request #2288 from BishopFox/feat/pr-2280-ci-fix
fix(dns): handle SplitBuffer varint boundaries and restore CI
2026-07-14 22:34:12 -07:00
moloch-- 1d33b89ec5 test: clean Go cache before generation suite 2026-07-14 19:14:37 -07:00
moloch-- 7b0f420290 Fix DNS splitting and Windows implant builds 2026-07-14 18:31:17 -07:00
moloch-- 00824d1b83 Merge remote-tracking branch 'origin/master' into feat/pr-2280-ci-fix 2026-07-14 07:50:18 -07:00
Joe 99ad05b7b7 Merge pull request #2287 from BishopFox/fix/png-decoder
Fix unbounded PNG decoding
2026-07-14 07:47:32 -07:00
moloch-- 80273462bd Fix unbounded PNG decoding 2026-07-13 22:07:42 -07:00
Luppa 2cdefc7369 fix(dns): step back on varint-boundary overshoot in SplitBuffer
The inner loop guard (subdataSpace-1) only accounts for a +2 base32
jump from a pure encoding boundary. When msg.Data length crosses a
proto3 varint boundary (e.g. 127->128 bytes), the serialized message
grows by 2 bytes, pushing the base32 encoding 3-4 chars over the
budget. This caused errMsgTooLong on specific parent domain length
parities. Add a step-back loop to correct overshoots without losing
the fast-path acceleration.

Fixes #2279
2026-06-28 08:50:31 +01:00
Ronan Kervella 80c752a422 Merge pull request #2273 from MKMithun2806/feature/configurable-keepalive
Feature/configurable keepalive
2026-06-03 12:35:42 -07:00
Mitchaster 8a8a9dc498 Merge branch 'BishopFox:master' into feature/configurable-keepalive 2026-06-03 06:14:57 +00:00
Mithun 5f8540b3c4 Standardize formatting with gofmt 2026-06-03 06:12:54 +00:00
Mithun 4eef868047 Add unit tests for rtunnels KeepAlive state management 2026-06-03 06:12:40 +00:00
Mithun 5ebfecd822 Use configurable KeepAlive in createReverseTunnelHandler 2026-06-03 06:12:26 +00:00
Mithun 9762560228 Pass KeepAlive to TrackListener in StartRportFwdListener 2026-06-03 06:12:12 +00:00
Mithun 6310c7d6a0 Store KeepAlive in rtunnels state manager 2026-06-03 06:11:58 +00:00
moloch-- fafe360672 go mod vendor 2026-05-31 14:25:51 -07:00
moloch-- d9b6f41940 Bump pge dep 2026-05-30 11:49:46 -07:00
moloch-- f1ccf0eac3 Docs site version bumps 2026-05-30 09:50:58 -07:00
Joe d2aa924bc4 Merge pull request #2258 from SAY-5/fix/install-detect-existing-minisign-2239
install: detect existing minisign before apt-get install
2026-05-07 08:01:04 -07:00
Joe 46861cd428 Merge pull request #2259 from cyberguerilla68/patch-1
Duplicate HTML Fix
2026-05-07 07:46:17 -07:00
ju li 1c1825f8f0 Duplicate HTML Fix
stageHandler function was missing a return statement at line 780, causing the server to serve duplicate HTML blocks when using the websites feature, causing things to render twice, and breaking embedded scripts like WAS stagers.

Signed-off-by: ju li <[email protected]>
2026-05-05 15:40:48 -04:00
SAY-5 a943995040 install: detect existing minisign before apt-get install
Signed-off-by: SAY-5 <[email protected]>
2026-05-05 03:48:27 -07:00
Joe f0d2a2cf24 Merge pull request #2257 from BishopFox/go/v1.26.2
Go v1.26.2
2026-05-03 17:47:00 -07:00
Joe 27ac612c82 Merge pull request #2255 from SAY-5/fix/event-broker-closed-channel-2252
core/events: don't panic on send to closed subscriber
2026-05-03 09:19:06 -07:00
moloch-- baedb563f3 Go v1.26.2 2026-05-03 09:17:04 -07:00
SAY-5 45c7914602 core/events: don't panic on send to closed subscriber
Closes #2252.
2026-05-02 01:05:02 -07:00
moloch-- 92d85cb8ff Tweak styling 2026-05-01 07:17:02 -07:00
moloch-- f5e831844a Fix various docs ui quirks in hero v3 2026-04-30 18:21:06 -07:00
moloch-- ab2fc7dc4a Initial update to heroui v3 2026-04-30 17:59:11 -07:00
Joe 5e377019e4 Merge pull request #2254 from smeukinou/fix-sliverpy
fix: safely close event broker subscription channels
2026-04-30 11:37:01 -07:00
smeukinou 88a4bebca0 fix: safely close event broker subscription channels
Moves the responsibility of closing the event subscription channel to the broker.Start() goroutine when processing an unsubscribe request. This prevents potential data races and panics caused by closing a channel concurrently while the broker might still be attempting to publish events to it.
2026-04-29 23:07:40 +02:00
Joe 2daccb5bd3 Merge pull request #2250 from BishopFox/wg/opt-in
Disable wg wrapper on multiplayer by default
2026-04-21 21:24:17 -07:00
Joe e2a009d4ac Merge pull request #2249 from Moriz82/fix/services-windows-nil-deref
fix(implant): services list panics on nil error on Windows (#1989)
2026-04-21 21:03:40 -07:00
moloch-- eda51f826a Disable wg wrapper on multiplayer by default 2026-04-21 20:51:06 -07:00
Will DeDominico f83dc42a64 fix(implant): services list panics on nil error (#1989)
servicesListHandler called err.Error() unconditionally after
service.ListServices(). On a healthy Windows host ListServices returns
nil error, causing a nil pointer dereference that crashed the implant.

serviceDetailHandler in the same file already guards correctly with
if err != nil. Apply the same pattern to servicesListHandler by
extracting response construction into a buildServicesResp helper with
a proper nil check.

Verified on Windows 11 VM: unfixed helper panics with "runtime error:
invalid memory address or nil pointer dereference" on the nil-error
path; fixed helper passes.

Fixes #1989
2026-04-20 20:13:46 -05:00
Joe 17e8c996a6 Merge pull request #2244 from Moriz82/fix/localtask-process-isolation
fix(implant): run LocalTask shellcode in forked child (#2237)
2026-04-19 08:44:56 -07:00
Will DeDominico 2b667f0e7e fix(implant): run LocalTask shellcode in forked child (#2237)
The previous implementation ran shellcode in a goroutine inside the
beacon; payloads that terminate via exit/execve killed the whole
beacon. Fork a child instead and let the parent wait for it.

Linux uses SYS_CLONE(SIGCHLD) since linux/arm64 has no SYS_FORK.
Darwin uses SYS_FORK, with the isChild check routed through the r1==1
convention that libSystem's fork() wrapper normally hides.

Post-fork the child runs only raw syscalls: runtime_BeforeFork poisons
stackguard0 and AfterForkInChild clears inherited signal handlers.
Mprotect happens in the child so the parent's page table stays
writable, inherited FDs are closed to avoid leaking C2 sockets into
exec'd payloads, and the child exits via SYS_EXIT_GROUP (Linux) or
SYS_EXIT (Darwin).

Tests under implant/sliver/taskrunner/ cover parent survival and
non-zero-exit error reporting on both Linux and Darwin.
2026-04-16 20:52:38 -05:00
Ronan Kervella 73bcf40bfd Merge pull request #2240 from BishopFox/CLD-10806/update-codeowners-replace-red-team
chore(doc): replace red-team with bf-red-team in CODEOWNERS (CLD-10806)
2026-04-10 09:02:55 -07:00
George Robbins b635adcb3c chore(doc): replace red-team with bf-red-team in CODEOWNERS (CLD-10806) 2026-04-10 10:56:01 -05:00
Joe 2b92397e07 Merge pull request #2236 from BishopFox/codex/pr2234-wg-ci
Fix WG client Windows CI regressions
2026-04-09 10:11:57 -07:00
moloch-- c23067a0e0 Wait for AI RPC event streams to subscribe 2026-04-02 09:17:33 -07:00
moloch-- 3aefc39c08 Stabilize AI RPC event stream tests 2026-04-02 08:50:19 -07:00
moloch-- abd8c83df8 Stabilize Windows WireGuard reconnects 2026-04-02 08:22:51 -07:00
Joe 6cce9426f2 Merge pull request #2235 from skoveit/fix/pkg-cache-delete-wrong-argument
fix: pkgCache.Delete(value) should be pkgCache.Delete(key)
2026-04-01 17:30:20 -07:00
skove 2e742863a1 fix: pass key instead of value to pkgcache.delete() 2026-04-01 14:47:39 +02:00
moloch-- dc13a5bb93 Add connecting animation 2026-03-31 13:33:19 -07:00
moloch-- 1f2946eb87 Revert changes to netstack 2026-03-31 09:45:40 -07:00
moloch-- 5eeda4c02b console now keeps the long-lived background WG streams on one connection 2026-03-31 09:36:05 -07:00
moloch-- 55a3386313 Improve reliabilty of multiplayer/wg setup 2026-03-30 20:02:55 -07:00
moloch-- b34589a002 Merge branch 'master' of github.com:BishopFox/sliver 2026-03-30 15:56:54 -07:00
moloch-- 4486b17b49 Tweak release trigger 2026-03-30 15:56:45 -07:00
Joe 377ee5ca7c Merge pull request #2229 from BishopFox/docs/multiplayer-wg
Update docs for wg+grpc/mtls multiplayer
2026-03-30 12:13:43 -07:00
moloch-- 4765f40854 v1.7.4 2026-03-30 11:51:15 -07:00
moloch-- e239d85821 v1.7.4 2026-03-30 11:48:34 -07:00
Joe dec419b284 Merge pull request #2232 from BishopFox/fix/linux-ver
Implement Detailed Linux Version Info
2026-03-30 09:52:39 -07:00
moloch-- 6d51a23cc9 Implement linux detailed version info 2026-03-29 19:11:48 -07:00
moloch-- 3d6eec47be Implement linux detailed version info 2026-03-29 19:11:40 -07:00
moloch-- 464fb7008c Tweak ai.yaml config schema 2026-03-29 18:41:06 -07:00
moloch-- ffa5f89f5f Tweak styling of system prompt 2026-03-29 09:00:15 -07:00
moloch-- f19cbdc957 Give agent encoder/decoder tools 2026-03-29 08:43:14 -07:00
moloch-- 83bfc8d8e6 Add default prompt to ai.yaml config 2026-03-27 21:36:58 -07:00
moloch-- 365bb3596e Add aliases and extensions to agent tools 2026-03-27 20:22:15 -07:00
moloch-- 9ee96c2ce6 Add ctx window usage to tui 2026-03-27 19:43:42 -07:00
moloch-- e776e266d0 Stack toast 2026-03-27 18:46:08 -07:00
moloch-- d080c8a646 Implement more tools for the ai to use 2026-03-27 18:15:46 -07:00
moloch-- cb6402245e Implement more tools for the ai to use 2026-03-27 18:15:37 -07:00
moloch-- e4357277ec Add web search to oai models 2026-03-27 17:33:15 -07:00
moloch-- 5dc2ab1490 Render reasoning text as md 2026-03-27 17:13:39 -07:00
moloch-- 9f034a9956 Implement better reasoning ui 2026-03-27 17:10:24 -07:00
moloch-- f700535997 Implement selectable text 2026-03-27 16:59:08 -07:00
moloch-- 31bca034d8 Remove layout text 2026-03-27 16:48:08 -07:00
moloch-- fac4ad5fb7 Add mouse support to ai tui 2026-03-27 16:40:33 -07:00
moloch-- c128d33514 Add toast notifications to tui 2026-03-27 13:01:34 -07:00
moloch-- 4a37262bcf Implement scroll wheel support for conversation pane 2026-03-27 11:56:58 -07:00
moloch-- a2295112bd Clean up various styling in ai tui 2026-03-27 11:47:39 -07:00
moloch-- 834dff0f6e Add lic header 2026-03-27 08:38:41 -07:00
Joe f4a0431fdd Merge pull request #2231 from BishopFox/fix/pivots-dos
Fix/pivots dos
2026-03-26 21:40:23 -07:00
moloch-- d072b328c0 Fix pivot handler test helper collision 2026-03-26 19:44:18 -07:00
moloch-- 435bbcbd7e Merge master 2026-03-26 18:51:05 -07:00
moloch-- 84dc28e584 Fix nil deref in pivots 2026-03-26 18:50:13 -07:00
Joe 639592cb02 Merge pull request #2230 from BishopFox/fix/tunnel-dos
Fix/tunnel dos
2026-03-26 18:03:16 -07:00
moloch-- 8c7a1a6b40 Fix dos/panic in tunnel closed 2026-03-26 17:40:57 -07:00
moloch-- 06274c4061 Fix dos/panic in tunnel closed 2026-03-26 17:40:46 -07:00
moloch-- 593925b4a5 Update docs for wg+grpc/mtls multiplayer 2026-03-26 17:30:33 -07:00
Joe 255477e572 Merge pull request #2228 from BishopFox/fix/mcp
Fix/mcp
2026-03-26 17:21:49 -07:00
moloch-- 0acc6911d8 Tweak ai mode init 2026-03-26 16:59:27 -07:00
moloch-- 4ca026906b Merge master 2026-03-26 15:54:00 -07:00
moloch-- 97a249e28e Implement http auth for mcp 2026-03-26 15:32:26 -07:00
moloch-- 9f331687d3 wip on agentic actions 2026-03-24 19:06:47 -07:00
Joe 14d68d6fcd Merge pull request #2225 from BishopFox/ai/tui2
Ai/tui
2026-03-22 08:36:46 -07:00
moloch-- 05c5334fea Switch to openai sdk 2026-03-21 09:23:49 -07:00
moloch-- d277a14c57 Add examples dir 2026-03-21 08:29:27 -07:00
moloch-- a7b5dbe6ee Tweak colors for user blocks 2026-03-20 20:29:28 -07:00
moloch-- c188dc1e69 Make messages copy-able 2026-03-20 20:19:02 -07:00
moloch-- 9fc9d1762e Merge branch 'master' into ai/tui2 2026-03-20 20:00:05 -07:00
Joe 4ef86448d0 Merge pull request #2224 from BishopFox/clean-makefile
Cleanup makefile vars
2026-03-20 08:22:50 -07:00
moloch-- 02117a6072 Cleanup makefile vars 2026-03-20 07:51:48 -07:00
moloch-- c272c8abac Update mcp dep 2026-03-19 09:26:17 -07:00
moloch-- cdf2412b39 Update mcp dep 2026-03-19 09:25:56 -07:00
moloch-- 40f52791d3 Update gprc dep 2026-03-19 08:55:25 -07:00
moloch-- a5d8bd16b9 Merge branch 'master' of github.com:BishopFox/sliver 2026-03-19 08:53:40 -07:00
moloch-- bf113f2151 npm update 2026-03-19 08:53:29 -07:00
moloch-- 4cd010afb9 wip on copy/paste 2026-03-19 08:52:46 -07:00
Joe 04b0bdd199 Merge pull request #2222 from skoveit/fix/alias-save-beacon-hostname
fix: populate hostname from beacon when using --save flag
2026-03-16 19:05:11 -07:00
Joe 9682db93b3 Merge branch 'master' into fix/alias-save-beacon-hostname 2026-03-16 18:47:06 -07:00
Joe 1d7ffb4ac2 Merge pull request #2221 from BishopFox/ai/tui
Ai/tui
2026-03-16 18:46:53 -07:00
0xOral c5e7cd6354 fix: use beacon.GetHostname() instead of direct field access 2026-03-17 03:16:19 +02:00
0xOral b20ba7284b fix: use beacon hostname when saving output with --save flag 2026-03-17 02:59:00 +02:00
moloch-- 49498210f8 Fix WireGuard generate test IP collisions 2026-03-16 17:20:33 -07:00
moloch-- 14e9121302 Go 1.25.8 2026-03-15 20:25:02 -07:00
moloch-- 8bd0257dd9 Go 1.25.8 2026-03-15 20:14:18 -07:00
Joe 93fdfb3add Merge branch 'master' into ai/tui 2026-03-15 20:03:27 -07:00
moloch-- e02b03cc70 Go 1.25.7 2026-03-15 20:03:01 -07:00
Joe 6831fe50a6 Merge pull request #2220 from skoveit/feat/multi-protocol-beacons
feat: multi-protocol implant builds via TUI
2026-03-15 20:02:36 -07:00
moloch-- 405108866d Update gh action workflows to 1.26.1 2026-03-15 15:34:34 -07:00
moloch-- 4d551b71e7 Fix huh form rendering 2026-03-15 14:11:33 -07:00
moloch-- a7edd9838d Sync prompt to stdout 2026-03-15 13:50:04 -07:00
moloch-- 9ef54ca6b1 Add mouse support to ai tui 2026-03-15 12:48:43 -07:00
moloch-- 4622c50288 Add ai chat sync 2026-03-15 12:26:03 -07:00
moloch-- b1ee0355af isolate multiplayer wg from c2 wg 2026-03-15 11:51:09 -07:00
moloch-- 01d9b213f2 fix grpc/mtls+wg for multiplayer 2026-03-15 11:11:27 -07:00
moloch-- f56886bce5 Merge grpc/mtls+wg branch 2026-03-15 10:55:50 -07:00
moloch-- 88176dfd00 Improve docs command 2026-03-15 10:31:41 -07:00
moloch-- 45dc3ea09b Improve doc file selector 2026-03-15 10:25:38 -07:00
moloch-- 682b3c2409 Upgrade to charm v2 libs 2026-03-15 09:47:29 -07:00
moloch-- 3a93f312b4 Add optional Wireguard multiplayer 2026-03-15 09:38:54 -07:00
moloch-- 2ca95f7a7c Add docs command 2026-03-15 09:21:24 -07:00
moloch-- d2319432ab Create docs go pkg 2026-03-15 08:53:52 -07:00
moloch-- 253f1a8806 Create docs go pkg 2026-03-15 08:52:05 -07:00
moloch-- 2c025d5970 Add new conversation modal 2026-03-15 07:56:11 -07:00
moloch-- f0034683f9 Tweak tui borders 2026-03-15 07:16:45 -07:00
moloch-- 8e8398b654 Tweak border 2026-03-14 23:20:07 -07:00
moloch-- aa02293f05 Fix borders 2026-03-14 22:34:58 -07:00
moloch-- f967a32308 Add scrollbar 2026-03-14 22:22:46 -07:00
moloch-- 99ec812455 Improve scroll of conversation pane, move context info to modal 2026-03-14 22:08:28 -07:00
moloch-- eeef542eeb Make conversation scrollable 2026-03-14 21:48:54 -07:00
moloch-- 6d7a10b392 Switch to fantasy to call models 2026-03-14 21:22:01 -07:00
Joe ca40c0a1e7 Merge pull request #2219 from BishopFox/fix/windows-ci
Fix Windows CI
2026-03-14 20:48:12 -07:00
moloch-- 4a5ad6ea92 Redesign delete modal 2026-03-14 20:13:02 -07:00
moloch-- 266dabf6c3 Redesign modals 2026-03-14 20:03:49 -07:00
moloch-- a4c8968539 Resize tui on window resize 2026-03-14 18:54:09 -07:00
moloch-- 613c030ea3 Add markdown rendering 2026-03-14 15:38:54 -07:00
moloch-- 9f6bfb6714 Working animation 2026-03-14 15:27:35 -07:00
0xOral a764814a8e ux: refine reconfig command warnings and safety prompts 2026-03-14 23:16:30 +02:00
0xOral 0c9ce138a0 allow runtime protocol switching 2026-03-14 22:54:25 +02:00
0xOral 6c1dcf6a06 add flags to include multiple transport protocols at build time 2026-03-14 22:53:33 +02:00
moloch-- fffe93819d Improve server-side ai grpc handlers 2026-03-14 13:31:32 -07:00
moloch-- 635ee421e3 Add animation 2026-03-14 12:54:20 -07:00
moloch-- 3ce6d69474 Add server-side ai-config form 2026-03-14 12:00:28 -07:00
moloch-- cd523c7f77 Wire tui to ai grpc 2026-03-14 11:43:21 -07:00
moloch-- 1f1b759914 Merge branch 'codex/add-bubbletea-ai-tui-layout' into ai/tui 2026-03-14 11:15:14 -07:00
moloch-- b78915fcbd Basic db layout 2026-03-14 11:14:54 -07:00
moloch-- fa0e13a5cc Basic tui layout 2026-03-14 11:14:03 -07:00
moloch-- 924bcddfd2 Fix gogo test on Windows 2026-03-14 10:25:34 -07:00
moloch-- affbf61452 Fix unit test on windows 2026-03-14 09:57:52 -07:00
moloch-- 7c6b2f70e4 Add windows ci runner 2026-03-14 08:42:57 -07:00
moloch-- c46f2938de Fix cross-platform issues with running ci on windows 2026-03-14 08:37:31 -07:00
moloch-- b355fe70d6 Add Windows makefile 2026-03-13 16:14:11 -07:00
moloch-- 10b1079286 Tweak agents.md 2026-03-13 15:29:45 -07:00
Joe 9c042e6977 Merge pull request #2217 from BishopFox/fix/cert-revoke
Improve operator cert revocation
2026-03-13 12:04:26 -07:00
Joe 62583cac8f Merge branch 'master' into fix/cert-revoke 2026-03-13 11:02:34 -07:00
moloch 95764945b8 Improve operator cert revocation 2026-03-13 10:59:00 -07:00
Joe 26abda1c15 Merge pull request #2216 from skoveit/fix/timeout-int64p-mismatch
fix: timeout flag ignored in aliases and extensions due to type mismatch
2026-03-13 07:38:21 -07:00
moloch 01dd3cac72 Go mod tidy 2026-03-13 07:02:06 -07:00
0xOral f8faa1f767 fix: normalize all command timeout flags to Int64P across client codebase 2026-03-13 02:49:01 +02:00
0xOral a9e48e84cd fix: extension timeout flag ignored due to IntP/Int64P mismatch 2026-03-13 02:30:55 +02:00
0xOral 9d6704d925 fix: alias -t flag always capped at 30s due to IntP/Int64P type mismatch (#2183) 2026-03-13 02:04:35 +02:00
Joe ce5157e2e9 Merge pull request #2213 from skoveit/fix/wininet-handle-leak
fix(wininet): Fix handle leak in WinINet driver
2026-03-11 08:29:42 -07:00
0xOral 7d19051df8 fix(wininet): close connection and request handles after each request 2026-03-11 01:32:27 +02:00
Joe a0d3bd579f Merge pull request #2211 from skoveit/feat/configurable-portfwd-keepalive
feat: make TCP keepalive period configurable for portfwd and rportfwd
2026-03-08 10:36:42 -07:00
0xOral 82c309632a default keepalive value 30s for both rportfwd and portfwd 2026-03-08 17:27:27 +02:00
0xOral 5c0b8ec460 disable keepalive with --keepalive -1 value 2026-03-08 17:25:06 +02:00
0xOral 7791a86eb4 configurable keepalive: portfwd --keepalive 2026-03-08 17:20:35 +02:00
Joe 7ac4db3fa0 Merge pull request #2207 from BishopFox/fix/beacon-getsystem
Fix for getsystem in beacon mode
2026-03-05 18:31:39 -07:00
moloch-- 512bc822ef Implement fix for getsystem in beacon mode 2026-03-05 08:22:42 -08:00
Joe f2bb9c3733 Merge pull request #2205 from BishopFox/feature/lazy-lg-msg
Lazy spool large messages to disk
2026-03-04 15:40:56 -07:00
moloch-- 0bd4f100fd Lazy spool large messages to disk 2026-03-04 14:21:07 -08:00
Joe 3230322fd6 Merge pull request #2201 from BishopFox/feature/exec-shellcode
Add support for wasm-donut options in windows `execute-shellcode`
2026-03-02 08:23:49 -07:00
moloch-- 82d979436b Add support for additional wasm-donute options in windows execute-shellcode 2026-03-01 22:35:37 -08:00
Joe d2632e75bd Merge pull request #2199 from BishopFox/feature/go-tests2
Refactor unit tests to not write to main db
2026-03-01 09:47:23 -07:00
moloch-- a55831fb44 Go v1.25.7 2026-02-28 22:39:46 -08:00
moloch-- 39d4edec43 Tweak go-tests.sh 2026-02-28 22:08:59 -08:00
moloch-- bbdb08e7a4 Refactor go-tests.sh 2026-02-28 14:12:37 -08:00
moloch-- 39b488bd25 Refactor unit tests to not write to main db 2026-02-28 13:11:14 -08:00
Joe e7b8a24534 Merge pull request #2198 from BishopFox/feature/handler-recover
Feature/handler recover
2026-02-28 11:39:00 -07:00
moloch-- 17b9d611f8 Add panic/recover to mtls, dns, and wg handlers 2026-02-28 09:56:48 -08:00
moloch-- ed9075d065 Add better nil checks to various pb handlers 2026-02-28 09:52:40 -08:00
Joe 1f7221933d Merge pull request #2194 from BishopFox/crush/v2
Upgrade crush libs to v2
2026-02-25 09:22:24 -07:00
moloch-- 6333e0691a Upgrade crush libs to v2 2026-02-24 17:40:13 -08:00
moloch-- 3bbaf80510 v1.7.3 2026-02-23 20:49:28 -08:00
moloch-- 25726d8105 Revert wasm-donut to v0.0.3 2026-02-23 20:48:53 -08:00
moloch-- 67e9a7f37f Update wasm-donut to v0.0.6 2026-02-23 19:33:26 -08:00
moloch-- a5b6935da6 Tweak AGENTS.md 2026-02-23 07:10:28 -08:00
moloch-- 2e36f45bdf v1.7.2 2026-02-22 17:33:08 -08:00
Joe 3e06a9d5ee Merge pull request #2191 from BishopFox/feature/file-tab-completer
Feature/file tab completer
2026-02-22 18:32:27 -07:00
moloch-- 952b2df49d Update beignet 2026-02-22 16:42:32 -08:00
moloch-- 9769f1bb57 Fix mod vendor 2026-02-22 16:41:16 -08:00
moloch-- f5f2c80ad3 Update malasada to v0.0.3 2026-02-22 16:38:41 -08:00
moloch-- bb2edf63cf Add file tab completer to more commands 2026-02-22 16:37:30 -08:00
moloch-- cd711afe8a Add tab completer to generate --save 2026-02-22 16:26:32 -08:00
Joe f9bbbb34bb Merge pull request #2190 from BishopFox/skoveit-feature/PE-Metadata-Spoofing
Skoveit feature/pe metadata spoofing
2026-02-21 23:29:33 -07:00
moloch-- 64b91c56c0 Fix unit tests 2026-02-21 15:14:26 -08:00
moloch-- a6c7867a26 Fix unit tests 2026-02-21 14:22:18 -08:00
moloch-- 7f25c3bb06 Expand pe metadata spoofing feature 2026-02-21 12:36:47 -08:00
moloch-- 153cf29551 Fix unit tests 2026-02-21 06:30:01 -08:00
jjensn b85161e797 Declare vars before use 2026-02-21 06:29:59 -08:00
jjensn ed906fcffe typo: Add yamux client constructor 2026-02-21 06:29:58 -08:00
jjensn 3bbcf5697a Yamux logging for debug implants 2026-02-21 06:29:56 -08:00
moloch-- 4a425d2fd6 Fix 'websites rm' add tab completer issue #2185 2026-02-21 06:29:55 -08:00
Adam d0f752e118 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:54 -08:00
Adam 7167621577 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-21 06:29:47 -08:00
Joe c3582fc203 Merge pull request #2189 from BishopFox/jjensn-fix/yamux-disable-logging
Jjensn fix/yamux disable logging
2026-02-19 22:29:46 -07:00
moloch-- 07c95b203f Fix unit tests 2026-02-19 18:05:57 -08:00
moloch-- 958e44deef Fix 'websites rm' add tab completer issue #2185 2026-02-19 17:54:55 -08:00
Adam 836395a2d6 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:53 -08:00
Adam 1a7a1c9b2d Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 17:54:27 -08:00
Joe 4ba285796f Merge pull request #2188 from BishopFox/fix/websites-rm
Fix 'websites rm' add tab completer issue #2185
2026-02-19 18:51:41 -07:00
moloch-- a05dfad100 Fix 'websites rm' add tab completer issue #2185 2026-02-19 17:30:33 -08:00
Joe 0eb29ad9b8 Merge pull request #2187 from Aptimex/execute-fix
Fix: Execute default env inheritance issues
2026-02-19 18:06:43 -07:00
Adam ad8b329f85 Fix execute infinite spin
Signed-off-by: Adam <[email protected]>
2026-02-19 16:45:39 -07:00
Adam c6206257b6 Change env-inheritance flag default to false
Signed-off-by: Adam <[email protected]>
2026-02-19 16:20:59 -07:00
Ronan Kervella 8bbf06c156 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-19 12:42:31 -08:00
jjensn 4d4eab998f Declare vars before use 2026-02-18 13:34:24 -05:00
jjensn c081f17380 typo: Add yamux client constructor 2026-02-17 13:17:44 -05:00
jjensn fafbac3fa6 Yamux logging for debug implants 2026-02-17 12:59:34 -05:00
moloch-- d9c89c81cf Fix typo in docs 2026-02-16 13:14:40 -08:00
moloch-- 061221905b Add discord button to upper right 2026-02-16 13:09:03 -08:00
moloch-- d0600e2be7 Tweak docs site loading 2026-02-16 10:44:51 -08:00
moloch-- 26abbe3860 Update wasm-donut to v0.0.5 2026-02-16 09:58:57 -08:00
Anas 55a386ae1c Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 19:54:15 +02:00
moloch-- 2bfdb218ff Update wasm-donut to v0.0.5 2026-02-16 09:44:22 -08:00
moloch-- 715b84e799 Update readme 2026-02-16 09:26:49 -08:00
moloch-- faea2ff639 Add link buttons to home page 2026-02-16 08:58:39 -08:00
moloch-- 1b253f8dec Add community talks section 2026-02-16 08:46:12 -08:00
moloch-- eb38724322 Add mtls and wg c2 docs 2026-02-16 08:35:01 -08:00
moloch-- 05dedf4473 Add mermaid chart component to md renderer 2026-02-16 08:06:36 -08:00
moloch-- 28c6b82a79 Update config file docs 2026-02-16 07:46:18 -08:00
Anas 1f553496e3 Merge branch 'master' into feature/PE-Metadata-Spoofing 2026-02-16 15:50:07 +02:00
0xOral cd5db464e0 fix protoc version 2026-02-16 15:25:21 +02:00
moloch-- 0bf0ac6c7b Update config file md 2026-02-15 21:52:57 -08:00
Joe 0cf5a47cfd Merge pull request #2181 from BishopFox/fix/gzip-bomb
Add limit to gzip size
2026-02-15 22:27:56 -07:00
moloch-- 8d6759924b Add limit to gzip size 2026-02-15 21:13:54 -08:00
Joe 8d5dbd2050 Merge pull request #2180 from BishopFox/feature/shell-mgmt
Feature/Shell Management
2026-02-15 22:12:08 -07:00
moloch-- 804e149113 Add shell kill option 2026-02-15 20:58:07 -08:00
moloch-- 9ced4cfd50 Add shell mgmt features 2026-02-15 20:51:18 -08:00
0xOral c8be5a5b1b add --spoof feature 2026-02-16 02:27:59 +02:00
moloch-- b7abf98c95 Tweak yt header 2026-02-15 07:30:51 -08:00
moloch-- 918ac1bb91 Tweak payload compatibility tables 2026-02-14 16:24:44 -08:00
moloch-- 1e57818312 Tweak cross-compile docs 2026-02-14 11:34:11 -08:00
moloch-- c44c14dd46 Tweak cross-compile docs 2026-02-14 11:33:48 -08:00
moloch-- 35f41cd62d Tweak compile from source instructions 2026-02-14 11:29:35 -08:00
moloch-- 4eff1cc053 Add getting started workshop 2026-02-14 11:22:12 -08:00
moloch-- 16981da431 upgrade docs to nextjs v16 2026-02-14 11:16:38 -08:00
moloch-- 3830c76292 npm update 2026-02-14 09:43:13 -08:00
moloch-- 73de93d6fb Fix content scrolling 2026-02-14 09:42:04 -08:00
moloch-- c7770c2f6e Add more yt links 2026-02-14 09:31:02 -08:00
moloch-- 21db908ce4 Hide footer on yt play 2026-02-14 09:23:11 -08:00
moloch-- b7b0247652 Add basic yt embeds 2026-02-14 08:52:15 -08:00
moloch-- 3dc7dc03d0 Fix moving sidebar 2026-02-14 08:40:26 -08:00
moloch-- 229452d7ed Fix moving sidebar 2026-02-14 08:39:23 -08:00
moloch-- d29ce9a08b Add payload compatibility table to docs 2026-02-14 08:19:18 -08:00
Joe 46f2b2b402 Merge pull request #2177 from BishopFox/skoveit-feature/dynamic-c2-endpoint
Skoveit feature/dynamic c2 endpoint
2026-02-13 11:50:36 -07:00
Joe 6ee2006387 Merge pull request #2170 from xtle0o0/fix/external-builder-authz-scope
Restrict external builder RPCs to assigned build jobs
2026-02-13 11:50:22 -07:00
moloch-- 2fd13ee2bc Regenerate pb 2026-02-13 07:22:06 -08:00
0xOral 2697568336 fix-export-ca-error-message 2026-02-13 07:21:54 -08:00
Joe 5013048b0b Merge pull request #2176 from skoveit/fix-export-ca-error-message
fix-export-ca-error-message
2026-02-13 08:13:46 -07:00
Joe 627b15572c Merge branch 'master' into feature/dynamic-c2-endpoint 2026-02-13 08:11:56 -07:00
0xOral 42c40c9b8d fix-export-ca-error-message 2026-02-12 19:45:52 +02:00
0xOral 405b282fed Dynamic C2 Endpoint Switching 2026-02-12 18:47:26 +02:00
Joe e32cd36e01 Merge pull request #2172 from skoveit/fix-off-by-one-naming
Fix: server panic on single-word lists and off-by-one in codename generation
2026-02-10 22:30:32 -07:00
Joe 492a2aeed6 Merge pull request #2171 from loresuso/loresuso/fix-zombie-shell
fix: wait system shell to cleanup zombie processes
2026-02-10 22:29:53 -07:00
Lorenzo Susini d6dafb0391 fix: wait system shell to cleanup zombie processes
Signed-off-by: Lorenzo Susini <[email protected]>
2026-02-10 18:01:39 +01:00
0xOral 49a1bcfd7d fix(codenames): fix off-by-one error and prevent panic on single-word lists 2026-02-10 17:05:13 +02:00
xtle0o0 a8d1bf6fb1 Restrict external builder RPCs to assigned builds 2026-02-10 12:00:24 +01:00
moloch-- 2f386b7e09 v1.7.1 2026-02-08 14:19:47 -08:00
Joe d7dce58397 Merge pull request #2168 from BishopFox/feature/mutli-console
Feature/mutli console
2026-02-08 07:47:05 -07:00
moloch-- 15114138a8 Save interactive settings to disk 2026-02-08 06:32:17 -08:00
moloch-- 2716090410 Add custom prompt configurations 2026-02-08 06:28:45 -08:00
moloch-- a805161245 Add themes and console switcher 2026-02-07 15:46:17 -08:00
Joe afd57891cd Merge pull request #2166 from BishopFox/linux/shellcode
Linux/shellcode
2026-02-07 14:32:19 -07:00
moloch--andCodex bdf4a6ed0d Codex changes
Co-authored-by: Codex
2026-02-07 12:54:03 -08:00
moloch-- 87b1efaa5f Make sure zig builds get stripped 2026-02-07 12:25:04 -08:00
moloch-- 009e505a54 Update shellcode unit tests 2026-02-07 10:46:01 -08:00
moloch-- c0dd2d5b51 Update docs, wire linux shellcode into client/ code 2026-02-07 10:33:13 -08:00
moloch-- 6dab9dc6c5 Update malasada to v0.0.2 2026-02-07 10:11:15 -08:00
moloch-- 854249fa25 Add linux shellcode for amd64/arm64 to rpc 2026-02-07 09:33:42 -08:00
moloch-- 7db573a03e Tweak file header 2026-02-07 07:32:48 -08:00
moloch-- 0bef5491dc Fix linux --format shared builds 2026-02-07 00:26:33 -08:00
moloch-- 1c50e5a87b Remove unused tests 2026-02-06 17:53:50 -08:00
moloch-- 8dae2d0d52 v1.7.0 2026-02-06 17:50:35 -08:00
Joe 023776f7e3 Merge pull request #2165 from BishopFox/crack/basic
Update gRPC APIs
2026-02-06 18:48:21 -07:00
moloch-- b7566a3b09 Update client grpc to modern apis 2026-02-06 17:31:08 -08:00
moloch-- b889e2eb6f Fix merge 2026-02-06 16:30:53 -08:00
moloch-- 4466ace279 Merge master 2026-02-06 16:29:39 -08:00
Joe 2b65089b27 Merge pull request #2164 from BishopFox/fix/dns-dos
Fix/dns dos
2026-02-06 15:41:17 -07:00
moloch-- 87d5fd53a8 Add e2e dns c2 tests 2026-02-06 13:54:07 -08:00
moloch-- cf79a2d284 Merge patch for oversized msg size checks 2026-02-06 12:16:03 -08:00
moloch-- 6d0276ff67 Add more tests for dns gc 2026-02-06 11:56:55 -08:00
moloch-- 9b001ab88e Remove totp from dns c2, implement pending msg gc 2026-02-06 11:40:00 -08:00
Joe 503218f3f4 Merge pull request #2163 from BishopFox/feature/exec-bg
Feature/exec bg
2026-02-06 10:44:20 -07:00
moloch-- 495dce0987 Improve error and nil checks in mtls comms 2026-02-06 08:22:26 -08:00
moloch-- a7ff35c229 Tweak selection form height 2026-02-06 07:58:17 -08:00
moloch-- 162fc4fb8a Merge branch 'feature/background-execute-flag' into feature/exec-bg 2026-02-06 07:31:10 -08:00
moloch-- 636ea0a9b2 Cleanup execute children output 2026-02-06 07:31:01 -08:00
moloch-- 84ed9bc4e7 Merge branch 'feature/background-execute-flag' into feature/exec-bg 2026-02-06 07:27:57 -08:00
moloch-- b985d8ff9f Fix cmd group help 2026-02-06 07:27:50 -08:00
moloch-- 5a43705e1d Merge branch 'feature/background-execute-flag' into feature/exec-bg 2026-02-06 07:13:34 -08:00
Joe 76af248850 Merge pull request #2162 from BishopFox/darwin/shellcode
Darwin/arm64 Shellcode
2026-02-06 07:59:56 -07:00
moloch-- 8ea7eb4236 Remove old test 2026-02-05 21:30:45 -08:00
Joe 8f3c3ec1e0 Merge branch 'master' into darwin/shellcode 2026-02-05 22:19:10 -07:00
moloch-- 8d4bf9c759 Fixes to xor_dynamic arm64 2026-02-05 21:18:17 -08:00
moloch-- 628757250e Add execute background flag 2026-02-05 20:56:07 -08:00
moloch-- 85094fc695 Port xor encoders to arm64 2026-02-05 20:41:00 -08:00
moloch-- 0a88ad77f7 Port xor encoders to arm64 2026-02-05 19:15:34 -08:00
moloch-- 4d04f08421 Port xor encoders to arm64 2026-02-05 19:14:25 -08:00
moloch-- 4a01971f7d Allow specifying multiple shellcode encoders by name 2026-02-05 18:39:07 -08:00
moloch-- 977c63dc16 Add shellcode support for darwin/arm64 2026-02-05 17:57:08 -08:00
Joe 2d65d7d6a5 Merge pull request #2161 from BishopFox/c2/mux
C2/mux
2026-02-05 18:36:17 -07:00
moloch-- 4f5ca51cfe Implement generate.darwinShellcode() 2026-02-05 17:16:14 -08:00
moloch-- 83208eeab4 Fix io import for named pipe build 2026-02-05 16:43:53 -08:00
moloch-- 21674a9f0a Merge branch 'codex/fix-client-wg-lport-flag' into c2/mux 2026-02-05 15:50:58 -08:00
moloch-- a18bda14e2 Fix wg lport check 2026-02-05 15:50:47 -08:00
moloch-- 37f9670783 Add mux to wg c2 2026-02-05 15:50:13 -08:00
moloch-- 1745886622 Add mux and signing to wg c2 2026-02-05 14:54:32 -08:00
moloch-- bd7361825c v1.6.11 2026-02-05 12:59:32 -08:00
Joe 32af81c132 Merge pull request #2160 from BishopFox/fix/wepath
Use correct path construction in website.ToProtobuf()
2026-02-05 13:47:35 -07:00
moloch-- 818127349c Use correct path construction in website.ToProtobuf() 2026-02-05 12:29:59 -08:00
moloch-- a712bead88 Tweak mux ids 2026-02-05 12:10:24 -08:00
moloch-- aa86a1d12c Add more signing unit tests 2026-02-05 11:38:37 -08:00
moloch-- afb58f7155 Add envelope signing over mtls 2026-02-05 11:10:35 -08:00
moloch-- 97d5adccd7 Add beacon tests 2026-02-05 09:28:55 -08:00
moloch-- 83cc755388 Add multiplexing to mtls comms 2026-02-05 07:17:25 -08:00
moloch-- 4dc1fe145c Fix missing import 2026-02-04 09:46:38 -08:00
Joe 5e27b23bde Merge pull request #2158 from BishopFox/fix/shell
Fix `shell` pty resizing
2026-02-03 17:47:44 -07:00
Joe 6011d23a9f Merge pull request #2159 from JakubStanior/fix/2129
Use lport from server config in daemon mode  / fix #2129
2026-02-03 17:42:18 -07:00
Jakub Stankiewicz 976d720043 Add default lport from server config in daemon mode 2026-02-03 23:31:08 +01:00
moloch-- 86f41e9abc Fix shell pty resizing 2026-02-02 19:19:14 -08:00
Joe 8b9fc399fa Merge pull request #2156 from BishopFox/fix/armory-win
Ensure armory temp files cross‑plat
2026-02-02 20:12:49 -07:00
moloch-- 27c75c14e6 Ensure armory temp files cross‑plat 2026-02-02 18:17:28 -08:00
moloch-- 11d6af8aa0 Fix duplicated 'unknown cmd' output 2026-02-01 10:44:38 -08:00
moloch-- 54dc6dcec3 v1.6.10 2026-01-31 17:45:38 -08:00
Joe c7412d4d5b Merge pull request #2154 from BishopFox/go/v1.25.6
Go/v1.25.6
2026-01-31 09:44:33 -07:00
moloch-- c3817dc175 Pin tag patch version Go 1.25.6 2026-01-31 08:20:09 -08:00
moloch-- 09cedf588e Update to go 1.25.6 2026-01-31 08:18:39 -08:00
moloch-- 66aed89ebd Merge master 2026-01-31 08:03:24 -08:00
moloch-- 16397e7a0f Update default runtime arg 2026-01-31 07:53:18 -08:00
moloch-- 5062b483b7 Update wasm-donut 2026-01-31 07:43:21 -08:00
moloch-- b268295c8d Switch to wasm-donut 2026-01-30 22:23:56 -08:00
moloch-- 2b3f23cec9 Move donut options to generate flags 2026-01-30 17:19:57 -08:00
moloch-- 25d242230b Rengerate pb 2026-01-30 15:10:02 -08:00
Joe 110658ec39 Merge pull request #2152 from BishopFox/feature/shellcode-encoders
Feature/shellcode encoders
2026-01-30 16:09:46 -07:00
moloch-- b4345226c1 Merge branch 'feature/shellcode-encoders' into crack/basic 2026-01-30 15:09:21 -08:00
moloch-- e99999ebb6 Cleanup shellcode encoder list output 2026-01-30 14:20:48 -08:00
moloch-- 24795e7cba Update shellcode encode rpc to support new encoders 2026-01-30 14:14:45 -08:00
moloch-- 32e3386a3e Add sgn to unified ShellcodeEncoders 2026-01-30 13:40:16 -08:00
moloch-- 7feea8bef3 Add more shellcode encoders 2026-01-30 13:02:14 -08:00
moloch-- d182434d54 Implement amd64/xor_dynamic shellcode encoder 2026-01-30 12:45:22 -08:00
moloch-- 0237af0c6e Implement amd64/xor_dynamic shellcode encoder 2026-01-30 12:45:08 -08:00
moloch-- 1fa36e46c4 Implement amd64/xor_dynamic shellcode encoder 2026-01-30 12:44:49 -08:00
moloch-- 4d9bae0778 Port basic xor shellcode encoder to golang 2026-01-30 12:28:15 -08:00
moloch-- 9a9b40e400 Move sng to server/encoders/shellcode/sgn 2026-01-30 11:51:57 -08:00
moloch-- d784b305ef Move sng to server/encoders/shellcode/sgn 2026-01-30 11:50:24 -08:00
moloch-- 7a224b4757 Update year 2026-01-30 11:40:58 -08:00
Joe 76ae704ce4 Merge pull request #2151 from BishopFox/feature/xxd
Feature/xxd
2026-01-30 12:06:11 -07:00
moloch-- 71cd8fa610 Skip generate tests if there's no edits to related code 2026-01-30 10:35:16 -08:00
moloch-- e9fdcc1285 Improve hex-edit ux 2026-01-30 10:30:15 -08:00
moloch-- 870c995ae2 Add hexedit feature 2026-01-30 10:20:56 -08:00
Joe 15d7e7c4d1 Merge pull request #2150 from BishopFox/feature/chtimes
Expand time formats supported by chtimes
2026-01-30 10:50:56 -07:00
moloch-- 750cdfce0a Expand time formats supported by chtimes 2026-01-30 09:41:55 -08:00
Joe 0f128a22b3 Merge pull request #1807 from navsec/socks5-stability
SOCKS5 Stability Improvements for HTTP/mTLS Transports
2026-01-30 10:07:13 -07:00
Joe 3296a8f483 Merge branch 'master' into socks5-stability
Signed-off-by: Joe <[email protected]>
2026-01-30 10:07:05 -07:00
Joe b77ade2bb9 Merge pull request #2148 from BishopFox/fix/info
Fix/info
2026-01-30 08:47:32 -07:00
moloch-- 41dc54d736 Implement mount for darwin 2026-01-29 22:22:59 -08:00
moloch-- 8a0694cb38 Add mount on darwin 2026-01-29 22:08:28 -08:00
moloch-- 52c0618bc3 Add os version implementations for linux/darwin 2026-01-29 21:50:16 -08:00
moloch-- e2e1912628 Add search to edit cmd 2026-01-29 21:01:16 -08:00
moloch-- 36794f1b07 Implement skip to line number 2026-01-29 20:50:11 -08:00
moloch-- ccd8f75b03 v1.6.9 2026-01-29 20:36:45 -08:00
moloch-- dc89afe404 Add optional line numbers 2026-01-29 20:32:15 -08:00
moloch-- 9db1f3a967 Add syntax highlighting 2026-01-29 20:22:23 -08:00
moloch-- a2ab3aede6 Implement 'edit' command 2026-01-29 20:07:51 -08:00
moloch-- c9f4970e4b Add crack db models 2026-01-29 19:35:39 -08:00
moloch-- e72fa3dbd0 Switch crack.yaml to snake case 2026-01-29 16:30:51 -08:00
moloch-- 2e93ce5684 Merge branch 'master' into crack/basic 2026-01-29 16:16:43 -08:00
Joe 545fc27d03 Merge pull request #2147 from BishopFox/feature/certs-db
Feature/certs db
2026-01-29 09:59:10 -07:00
moloch-- 5bf65aa2b0 Fix notification indentation 2026-01-29 08:51:01 -08:00
Joe ebaf07ff1f Merge branch 'master' into feature/certs-db 2026-01-29 09:16:35 -07:00
moloch-- fc95d3e354 Fix notifications build on windows 2026-01-29 08:15:32 -08:00
moloch-- 88dca9337b Fix notifications docs anchors 2026-01-29 08:11:17 -08:00
moloch-- 44d9f8d5aa Update cli to work with db CAs 2026-01-29 08:01:35 -08:00
moloch-- f76e9e367d Move CAs to db 2026-01-29 07:56:20 -08:00
moloch-- 69fa4143f6 Improve logging for invalid configs 2026-01-29 07:08:34 -08:00
moloch-- dcbda0c64e Improve logging for notifications 2026-01-29 06:50:18 -08:00
moloch-- 3db48408af Initial notifications implementation 2026-01-29 06:44:11 -08:00
moloch-- 828838cddf Re-add new gprc server config fields 2026-01-29 05:52:41 -08:00
moloch-- 16c1dc21fd Merge master 2026-01-29 05:47:18 -08:00
moloch-- 3c8edb7294 v1.6.8 2026-01-28 20:14:36 -08:00
moloch-- 2e2206e0ae Fix emtpy prompt after event 2026-01-28 19:58:08 -08:00
Joe a43648a24a Merge pull request #2146 from BishopFox/fix/websites-css
Fix/websites css
2026-01-28 19:50:20 -07:00
moloch-- f65f98bf65 Improve websites content-type sniffing issue #2033 2026-01-28 18:14:27 -08:00
Joe 26d14e15ad Merge pull request #2144 from BishopFox/fix/update
Fix/update
2026-01-28 19:07:58 -07:00
moloch-- cee2cc76c4 Convert tui-settings to yaml 2026-01-28 18:06:41 -08:00
moloch-- daa33b6301 Convert server configs to yaml syntax, with backwards compatibility 2026-01-28 17:59:34 -08:00
moloch-- ef52973536 Convert configs to yml 2026-01-28 17:39:14 -08:00
moloch-- 6a7bd0803a Update --save help 2026-01-28 17:21:35 -08:00
moloch-- 6c70c1c1e6 Add signature validation logic 2026-01-28 17:20:24 -08:00
moloch-- ddc222b8a2 Improvements to update 2026-01-28 17:07:47 -08:00
moloch-- 5506a582ac Add SliverPublicKey 2026-01-28 16:45:32 -08:00
Joe 63d872c98b Merge pull request #2142 from BishopFox/fix/sgn
Fix/sgn
2026-01-28 17:31:46 -07:00
moloch-- ef9f5dde0d Fix unit tests 2026-01-28 07:21:38 -08:00
moloch-- e35b7fdb97 Update sgn to v0.0.5 2026-01-27 21:28:21 -08:00
moloch-- efbbf382eb Merge branch 'master' into fix/sgn 2026-01-27 21:26:42 -08:00
moloch-- 0b8d00900d v1.6.7 2026-01-27 21:06:39 -08:00
moloch-- 9077596f25 Damn it Claude 2026-01-27 21:04:30 -08:00
moloch-- 0a8104a32e Damn it Claude 2026-01-27 21:03:25 -08:00
moloch-- 15a0ba4c1e Damn it Claude 2026-01-27 21:01:55 -08:00
moloch-- d452c8a676 Write .bin even if sgn fails 2026-01-27 20:49:28 -08:00
Joe fb74b491fa Merge pull request #2141 from BishopFox/fix/console-inputs
Update readline/console deps
2026-01-27 21:38:20 -07:00
moloch-- 2af6e11adc Update readline/console deps 2026-01-27 19:55:18 -08:00
moloch-- 33c656d082 Implement first part of crack cmd 2026-01-24 18:28:07 -08:00
moloch-- 99a44e340f Improve crack benchmark output 2026-01-24 11:50:20 -08:00
moloch-- de5fa3458b Allow keep alives 2026-01-23 17:05:53 -08:00
Joe a3a98858fd Merge pull request #2135 from BishopFox/fix/sqlite
Add sqlite pragmas to db config
2026-01-21 22:28:56 -07:00
moloch-- d0652f2fd3 Add sqlite pragmas to db config 2026-01-21 16:08:07 -08:00
Joe 936f067363 Merge pull request #2130 from BishopFox/feature/website-info
Feature/website info
2026-01-21 10:25:56 -07:00
moloch-- 041c3c7048 Improve tab completer for websites cmd 2026-01-20 21:29:39 -08:00
moloch-- b48f74af58 Add original file name and sha256 to websites cmd 2026-01-20 19:13:42 -08:00
moloch-- 6ec8849b20 v1.6.6 2026-01-16 09:18:39 -08:00
Joe db7cf1ed62 Merge pull request #2125 from BishopFox/fix/winlsdir
Improved `err` and `nil` checks in `dirListHandler`
2026-01-16 10:17:01 -07:00
moloch-- 16e1e77068 Implement better err and nil checks in dirListHandler issue #2122 2026-01-16 07:06:43 -08:00
moloch-- 1cfa694aa2 Merge branch 'update/deps' into tui/adv-forms 2026-01-16 06:57:06 -08:00
moloch-- 5898877136 Fix --rc scripts 2026-01-16 06:56:41 -08:00
moloch-- a7d8cac565 Update generate form 2026-01-16 06:41:58 -08:00
moloch-- 0e73afca47 Update deps 2026-01-16 06:07:10 -08:00
moloch-- 432941d694 Tab complete parent commands from submenu 2026-01-16 05:58:35 -08:00
moloch-- ac521bf53c v1.6.4 2026-01-15 16:10:06 -08:00
Joe 9720c9f14f Merge pull request #2121 from BishopFox/feature/flag-completers
Improved flag completers
2026-01-15 17:09:30 -07:00
moloch-- 4a224fdb9c Improve completers for --interact flags 2026-01-15 15:14:20 -08:00
moloch-- 671eb4d777 Add tab completer to 'use' cmd 2026-01-15 15:03:21 -08:00
moloch-- 16cf816610 Implement os and arch completers 2026-01-15 13:54:21 -08:00
moloch-- 879f576c6e Add MCP to docs 2026-01-15 08:05:02 -08:00
moloch-- 2df78cdbf5 Update readme 2026-01-15 07:45:49 -08:00
moloch-- a05ffc0ae1 Fix footer/card z-index issue in docs site 2026-01-15 07:44:30 -08:00
moloch-- 6b4b218f09 Tweak docs 2026-01-15 07:33:40 -08:00
Joe 4d0d46afd6 Merge pull request #2118 from BishopFox/exec/env
Updates to `execute`
2026-01-13 22:45:02 -07:00
moloch-- f24c057503 Implement --env and --env-inheritance for 'execute' to allow greater control over subprocess env 2026-01-13 20:55:20 -08:00
moloch-- 7e3c1f1549 v1.6.3 2026-01-13 16:28:27 -08:00
Joe 1d4c0ec8a5 Merge pull request #2106 from cmprmsd/feature/extension-alias-autocompletion
Add argument completion for aliases and extensions
2026-01-13 17:25:11 -07:00
Joe 92dda6aa4f Merge pull request #2112 from BishopFox/6b37dependabot/go_modules/google.golang.org/protobuf-1.36.11
Bump google.golang.org/protobuf from 1.36.10 to 1.36.11
2026-01-13 17:24:30 -07:00
Joe 9a605dfda6 Merge pull request #2113 from BishopFox/0f1edependabot/go_modules/golang.org/x/sys-0.40.0
Bump golang.org/x/sys from 0.39.0 to 0.40.0
2026-01-13 17:24:14 -07:00
Joe e918885556 Merge pull request #2114 from Gubarz/getsystem
Fix: getsystem shellcode generation
2026-01-13 17:24:00 -07:00
Joe 263702594f Merge pull request #2115 from Gubarz/execute-assembly
Revert go-donut from 2022 patch to 2021
2026-01-13 17:23:17 -07:00
Joe 72b8dfb454 Merge pull request #2116 from BishopFox/fix/armory-all
Fix armory install all cmd
2026-01-13 17:23:00 -07:00
moloch-- aa2383ac24 Fix armory install all cmd 2026-01-12 21:45:21 -08:00
dependabot[bot] b6f0ea013d Bump golang.org/x/sys from 0.39.0 to 0.40.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.39.0 to 0.40.0.
- [Commits](https://github.com/golang/sys/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-13 05:34:38 +00:00
dependabot[bot] eb82e18a52 Bump google.golang.org/protobuf from 1.36.10 to 1.36.11
Bumps google.golang.org/protobuf from 1.36.10 to 1.36.11.

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-13 05:34:34 +00:00
Joe 40ea51b685 Merge pull request #2111 from BishopFox/3a38dependabot/go_modules/golang.org/x/net-0.48.0
Bump golang.org/x/net from 0.47.0 to 0.48.0
2026-01-12 22:33:30 -07:00
Joe 1285b44c50 Merge pull request #2110 from BishopFox/d73edependabot/go_modules/github.com/miekg/dns-1.1.70
Bump github.com/miekg/dns from 1.1.69 to 1.1.70
2026-01-12 22:33:18 -07:00
Joe cb63a3378f Merge pull request #2109 from BishopFox/dca1dependabot/go_modules/tailscale.com-1.92.5
Bump tailscale.com from 1.90.6 to 1.92.5
2026-01-12 22:33:02 -07:00
Gubarz 4f90bfdcda revert go-donut from 2022 to 2021 2026-01-12 13:48:53 -07:00
Gubarz 5bb1dcc137 Fix getsystem rpc errors 2026-01-12 12:25:00 -07:00
dependabot[bot] 57c417c48f Bump golang.org/x/net from 0.47.0 to 0.48.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.47.0 to 0.48.0.
- [Commits](https://github.com/golang/net/compare/v0.47.0...v0.48.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-12 03:05:52 +00:00
dependabot[bot] 55fc59f6f5 Bump github.com/miekg/dns from 1.1.69 to 1.1.70
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.69 to 1.1.70.
- [Commits](https://github.com/miekg/dns/compare/v1.1.69...v1.1.70)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-version: 1.1.70
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-12 03:05:44 +00:00
dependabot[bot] 332852d095 Bump tailscale.com from 1.90.6 to 1.92.5
Bumps [tailscale.com](https://github.com/tailscale/tailscale) from 1.90.6 to 1.92.5.
- [Release notes](https://github.com/tailscale/tailscale/releases)
- [Commits](https://github.com/tailscale/tailscale/compare/v1.90.6...v1.92.5)

---
updated-dependencies:
- dependency-name: tailscale.com
  dependency-version: 1.92.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-12 03:05:34 +00:00
cmprmsd a903d2f596 Add support for predefined argument completion choices. in extensions and aliases
Example:
{
      "name": "browser",
      "desc": "target browser (chrome, edge, brave, all)",
      "type": "string",
      "optional": true,
      "choices": ["chrome", "edge", "brave", "all"]
    },
2026-01-11 22:11:40 +01:00
cmprmsd 4a4bb683e3 Add argument completion for aliases and extensions
- Introduced `AliasArgument` struct to define arguments for alias commands.
- Enhanced `LoadAlias` function to build usage and help messages including argument details.
- Implemented argument completion for alias commands using Carapace.
- Updated `makeAliasArgCompleter` to support positional and flag-style argument completions.
- Refactored `makeExtensionArgCompleter` to include similar completion features for extension commands.
- Improved user experience by displaying argument descriptions in completion lists one argument per line (instead of grouped).
2026-01-09 15:46:32 +01:00
moloch-- 016299c756 v1.6.2 2026-01-08 12:56:41 -08:00
Joe 075caaccda Merge pull request #2104 from BishopFox/rc/flush
Flush output on exit
2026-01-08 13:54:54 -07:00
Joe 729be253d3 Merge branch 'master' into rc/flush 2026-01-08 13:12:05 -07:00
moloch-- 13bd48182f Cleanup exit msg 2026-01-08 12:11:50 -08:00
moloch-- 67188dabea Flush output on exit 2026-01-08 12:10:30 -08:00
Joe 6e4c38c10e Merge pull request #2103 from BishopFox/fix-rportfwd
Fix rportfwd
2026-01-08 12:48:28 -07:00
Joe 00ecb1f4fd Merge pull request #2094 from BishopFox/5032dependabot/go_modules/github.com/spf13/cobra-1.10.2
Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
2026-01-08 12:21:04 -07:00
Joe bac047fedd Merge pull request #2102 from 13621/revertpr2070
Fix DNS beacon closing immediately after registering
2026-01-08 12:20:48 -07:00
rkervella 10c16be351 Fix rportfwd 2026-01-08 10:48:57 -08:00
13621 d51ea9dbab fix: revert pr2070
Signed-off-by: 13621 <[email protected]>
2026-01-07 18:35:09 +00:00
dependabot[bot] 0eb41094de Bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-05 06:05:01 +00:00
Joe a219a97cb4 Merge pull request #2092 from BishopFox/3cabdependabot/go_modules/filippo.io/age-1.3.1
Bump filippo.io/age from 1.2.1 to 1.3.1
2026-01-04 23:04:08 -07:00
Joe b617bb4cf4 Merge pull request #2095 from BishopFox/21cddependabot/go_modules/github.com/ncruces/go-sqlite3-0.30.4
Bump github.com/ncruces/go-sqlite3 from 0.29.1 to 0.30.4
2026-01-04 23:03:54 -07:00
Joe 613b53430b Merge pull request #2093 from BishopFox/eb28dependabot/go_modules/github.com/miekg/dns-1.1.69
Bump github.com/miekg/dns from 1.1.68 to 1.1.69
2026-01-04 23:03:39 -07:00
Joe 472de54757 Merge pull request #2096 from BishopFox/b1e7dependabot/go_modules/golang.org/x/mod-0.31.0
Bump golang.org/x/mod from 0.30.0 to 0.31.0
2026-01-04 23:03:26 -07:00
Joe bdbc3bd7b5 Merge pull request #2090 from AnvithLobo/fix-forms
Fix interactive prompt cancellation handling
2026-01-04 23:03:11 -07:00
dependabot[bot] 87db61bcfb Bump github.com/miekg/dns from 1.1.68 to 1.1.69
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.68 to 1.1.69.
- [Commits](https://github.com/miekg/dns/compare/v1.1.68...v1.1.69)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-version: 1.1.69
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-05 05:13:43 +00:00
dependabot[bot] 095eb2722b Bump golang.org/x/mod from 0.30.0 to 0.31.0
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.30.0 to 0.31.0.
- [Commits](https://github.com/golang/mod/compare/v0.30.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-05 05:13:43 +00:00
dependabot[bot] abbb0da434 Bump github.com/ncruces/go-sqlite3 from 0.29.1 to 0.30.4
Bumps [github.com/ncruces/go-sqlite3](https://github.com/ncruces/go-sqlite3) from 0.29.1 to 0.30.4.
- [Release notes](https://github.com/ncruces/go-sqlite3/releases)
- [Commits](https://github.com/ncruces/go-sqlite3/compare/v0.29.1...v0.30.4)

---
updated-dependencies:
- dependency-name: github.com/ncruces/go-sqlite3
  dependency-version: 0.30.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-05 05:13:41 +00:00
dependabot[bot] 52f7484a71 Bump filippo.io/age from 1.2.1 to 1.3.1
Bumps [filippo.io/age](https://github.com/FiloSottile/age) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/FiloSottile/age/releases)
- [Commits](https://github.com/FiloSottile/age/compare/v1.2.1...v1.3.1)

---
updated-dependencies:
- dependency-name: filippo.io/age
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-01-05 05:13:38 +00:00
moloch-- a79b5fd7b9 go mod tidy 2026-01-04 21:11:55 -08:00
Joe 3e98d2d7a1 Merge pull request #2091 from BishopFox/feature/rc
Implement --rc for scripting initial inputs
2026-01-04 20:31:55 -07:00
moloch-- 13bf9d0a01 Implement --rc for scripting initial inputs 2026-01-04 17:51:59 -08:00
AnvithLobo 7aae725b88 Set form width dynamically based on terminal size
Introduced getTerminalWidth to determine the terminal width  and set the form width accordingly
2026-01-05 05:31:56 +05:30
AnvithLobo afd01073e4 Handle selection errors in builder and encoder prompts
Added error handling for empty selections in external builder and traffic encoder selection dialogs. Now returns a specific error or prints an error message if no selection is made
2026-01-05 04:58:53 +05:30
AnvithLobo ae0c33f41b Handle no selection in C2 profile selection
Updated the selectC2Profile function to return an error if no profile is selected, and modified callers to handle this error gracefully.
2026-01-05 04:47:40 +05:30
AnvithLobo 18da00b449 Fix: Allow user to cancel select prompts
Saves the original value before running the select prompt and restores it if the form is cancelled or an error occurs. This prevents unintended return value when the prompt was cancelled.
2026-01-05 04:39:11 +05:30
moloch-- dc736f6095 Update readme 2026-01-04 12:18:58 -08:00
Joe e02d53ce67 Merge pull request #2088 from BishopFox/fix/readline
Reduce flickering text in readline
2026-01-04 13:02:14 -07:00
Joe 064cd6a2cc Merge pull request #2087 from BishopFox/fix/mods3
Rename implant/sliver imports
2026-01-04 12:00:46 -07:00
moloch-- eab9d0b653 Reduce flickering text in readline 2026-01-04 10:59:20 -08:00
Joe 29c4ec0afa Merge pull request #2032 from carterburn/feature/aka-command
New command: add command aliases (aka)
2026-01-04 11:19:15 -07:00
moloch-- b7c336cb76 Rename implant/sliver imports 2026-01-04 10:17:56 -08:00
moloch-- d8ddd09c86 Update pretty table 2026-01-04 09:42:44 -08:00
Joe 04bb356207 Merge branch 'master' into feature/aka-command 2026-01-04 10:19:52 -07:00
Joe a003a5e3cf Merge pull request #2086 from AnvithLobo/master
Fix beacon panic race condition
2026-01-04 09:19:53 -07:00
Joe 75e3cad76a Merge branch 'master' into feature/aka-command 2026-01-04 09:06:36 -07:00
AnvithLobo 8aa0cbee7e Fix beacon panic race condition
Tasks are now dispatched to background goroutines, and their results are collected asynchronously via a channel. Check-ins include any pending results from previous tasks, improving responsiveness and reliability. Introduced beaconDispatchTasks to manage background execution and result collection.
2026-01-04 07:57:26 +05:30
Joe 30a50717e1 Merge pull request #2029 from BishopFox/7a58dependabot/go_modules/github.com/klauspost/compress-1.18.1
Bump github.com/klauspost/compress from 1.18.0 to 1.18.1
2026-01-02 21:34:03 -07:00
Joe f4c9e7109b Merge pull request #2084 from BishopFox/fix/mods2
Improvements to Implant Build Layout
2026-01-02 21:19:16 -07:00
moloch-- 08f3997756 Fix sliver.h -> main.h 2026-01-02 21:40:45 -06:00
moloch-- 3aebebecbb Implement RenameImport() 2026-01-02 21:22:19 -06:00
moloch-- 1d7eca8e39 Validate module path 2026-01-02 21:06:14 -06:00
moloch-- 773ee5f867 Rename sliver.go -> main.go 2026-01-02 20:55:26 -06:00
moloch-- 7608a4b0a0 Add GoPackage to ImplantConfig pb 2026-01-02 20:41:35 -06:00
moloch-- 9ecd36b7b6 Implement module rename 2026-01-02 20:33:27 -06:00
moloch-- d8fc5a6df4 Tweak project dir structure 2026-01-02 19:02:07 -06:00
moloch-- 649a6582e0 Add -mod=vendor to GoBuild 2026-01-02 18:53:31 -06:00
moloch-- 00e8a0cfca Move main implant code out of main pkg 2026-01-02 18:50:26 -06:00
moloch-- e9890bc8f9 Set limit to unauthed body req size 2026-01-02 17:32:43 -06:00
Joe 8b798f8307 Merge pull request #2083 from BishopFox/feature/mcp
Experiamental MCP Support
2026-01-02 16:25:24 -07:00
moloch-- d4728af1ea Add mcp logging 2026-01-02 16:26:01 -06:00
moloch-- d1b7d91b1a Add basic file operations to mcp 2026-01-02 12:48:35 -06:00
moloch-- cac6dced34 Tweak mcp 2026-01-02 11:14:32 -06:00
moloch-- a35bf0c1fc Implement mcp stdio option 2026-01-02 07:01:55 -06:00
moloch-- 67616e50c1 Wire rpc to mcp 2026-01-01 22:43:52 -06:00
moloch-- 54de8a2a97 Outline mcp 2026-01-01 22:32:25 -06:00
moloch-- 20916c655c Improve kick-operator command 2026-01-01 11:17:07 -06:00
Joe 6c18201ed6 Merge pull request #2081 from BishopFox/fix/rpc-errors
Improve gRPC Error Codes
2026-01-01 09:57:00 -07:00
moloch-- 29165aff0d Add /artifacts to gitignore 2026-01-01 10:04:53 -06:00
moloch-- 704285f28d Fix release workflow 2026-01-01 09:50:59 -06:00
moloch-- a007872fbc Include version history in release action 2026-01-01 09:48:56 -06:00
moloch-- c90e674f25 Merge branch 'master' into fix/rpc-errors 2026-01-01 09:40:57 -06:00
moloch-- 679b8d0c51 Add missing --permissions all flag 2026-01-01 09:40:43 -06:00
moloch-- 0a42a03052 Add missing --permissions all flag 2026-01-01 09:40:31 -06:00
moloch-- 73852e5bf2 Remove mingw, add operator --permissions flag 2026-01-01 09:37:16 -06:00
moloch-- 5bcf59e1b3 Fix installation script for amd64 minisig name 2026-01-01 09:32:02 -06:00
moloch-- 935c737c35 Implement better grpc error codes 2026-01-01 09:30:14 -06:00
moloch-- 5c04e61235 Simplify signing operation AGAIN 2025-12-31 16:25:10 -06:00
moloch-- cff7a8ce53 Simplify signing operation 2025-12-31 15:53:07 -06:00
moloch-- e6f452ea2e Simplify signing operation 2025-12-31 15:29:46 -06:00
moloch-- c5f533bc80 Simplify signing operation 2025-12-31 15:13:03 -06:00
moloch-- bc08760320 nop 2025-12-31 14:56:02 -06:00
moloch-- 9b37bee391 nop 2025-12-31 14:37:52 -06:00
moloch-- 709085238d Improve release minisign key handling 2025-12-31 14:20:12 -06:00
moloch-- 6e4240f0d9 Fix release workflow 2025-12-31 14:04:46 -06:00
moloch-- c95e5c6cd7 Implement armory forms 2025-12-31 13:48:00 -06:00
moloch-- ed3ab31741 Implement armory forms 2025-12-31 13:41:53 -06:00
moloch-- 9372b768be Update docs for minisign, remove gpg 2025-12-31 13:38:45 -06:00
moloch-- 2d7406f415 Update readme and npm 2025-12-31 13:13:44 -06:00
moloch-- 85ba0cb587 Update docs for v1.6 2025-12-31 11:33:21 -06:00
moloch-- 251871cca1 Switch to minisign for install script, add arm64 support 2025-12-31 11:15:33 -06:00
moloch-- 604318ea54 Update release workflow, add support for windows/arm64 2025-12-31 11:08:21 -06:00
moloch-- bc72b20f9b Update release workflow 2025-12-31 10:40:42 -06:00
Joe 61022e1eb1 Merge pull request #2078 from BishopFox/fix/forms
Refactor Survey to Huh
2025-12-31 09:17:11 -07:00
moloch-- a8d59daded tidy 2025-12-31 10:12:28 -06:00
moloch-- 9195376993 Refactor survey to huh 2025-12-31 10:04:27 -06:00
moloch-- a6de8e0304 Add profile forms 2025-12-31 09:13:21 -06:00
Joe d3ebd34b0d Merge pull request #2077 from BishopFox/fix/forms
Implement basic forms
2025-12-31 07:46:07 -07:00
moloch-- 310619526d Tweak settings 2025-12-31 08:40:02 -06:00
moloch-- 1990777c4b Tweak settings 2025-12-31 08:39:55 -06:00
moloch-- 9570556a87 Refactor to use debug.BuildInfo for version info 2025-12-31 08:27:09 -06:00
moloch-- b563919c19 Update x/crypto 2025-12-31 08:03:33 -06:00
moloch-- f1858ee8f3 Add 'generate beacon' form 2025-12-31 07:58:27 -06:00
moloch-- 46a8313a33 Tweak generate form 2025-12-31 07:51:57 -06:00
Joe 22454f3385 Merge pull request #2070 from xp3s/fix/dns-beacon-close-session
fix: DNS beacon Close() should call CloseSession() to prevent UDP port leak
2025-12-30 22:43:38 -07:00
moloch-- 868b2c294c Merge branch 'master' into fix/dnsc2 2025-12-30 23:04:32 -06:00
moloch-- 9827b4df4b Add forms pkg 2025-12-30 23:04:03 -06:00
moloch-- a48309a813 Add forms pkg 2025-12-30 23:03:55 -06:00
moloch-- 5df5dec5d4 Add forms pkg 2025-12-30 23:03:46 -06:00
Joe a0fb2a229f Merge branch 'master' into fix/dns-beacon-close-session 2025-12-30 21:59:35 -07:00
Joe 3ff3ef2ed4 Merge pull request #2076 from BishopFox/fix/dnsc2
Add cleanup to unit tests to avoid excessive disk usage
2025-12-30 21:59:21 -07:00
moloch-- 73cc8c38ff Add cleanup to unit tests to avoid excessive disk usage 2025-12-30 17:47:49 -06:00
Joe 896c763166 Merge branch 'master' into fix/dns-beacon-close-session 2025-12-30 16:42:38 -07:00
Joe ec7e2facbc Merge pull request #2075 from BishopFox/fix/dnsc2
Improvements to DNS C2, Port go-assest.sh to Golang
2025-12-30 15:37:37 -07:00
Joe 4969c2dc51 Merge branch 'master' into fix/dnsc2 2025-12-30 15:36:27 -07:00
moloch-- 94cb60b1f7 Updates to tests and implementation of dns c2 2025-12-30 16:35:37 -06:00
moloch-- 33fcfd54ef Update makefile to make goos/goarch override easier 2025-12-30 13:04:52 -06:00
moloch-- 10bb248e50 Code cleanup 2025-12-30 12:18:05 -06:00
moloch-- e023fd507e Port go-assets.sh to golang with go run 2025-12-30 11:55:56 -06:00
moloch-- 1a9f0f38dd Port go-assets.sh to golang with go run 2025-12-30 10:56:02 -06:00
moloch-- 02831a80bf Cleanup outputs of go-assets.sh 2025-12-30 10:26:24 -06:00
moloch-- 9ccff1798e Expand and update dns c2 unit tests 2025-12-30 09:46:49 -06:00
Joe cd5ef8b5c6 Merge pull request #2063 from BishopFox/3e11dependabot/npm_and_yarn/docs/sliver-docs/next-15.5.9
Bump next from 15.5.7 to 15.5.9 in /docs/sliver-docs
2025-12-30 08:12:38 -07:00
moloch-- dda1fdbbc2 Update dns unit tests 2025-12-30 08:52:26 -06:00
Joe 3b49df5662 Merge pull request #2067 from BishopFox/350cdependabot/github_actions/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-29 21:44:47 -07:00
Joe 32be71ff8e Merge pull request #2069 from BishopFox/0d1adependabot/github_actions/actions/download-artifact-7
Bump actions/download-artifact from 6 to 7
2025-12-29 21:44:33 -07:00
Joe 3f77da1524 Merge pull request #2068 from BishopFox/f7cedependabot/github_actions/actions/cache-5
Bump actions/cache from 4 to 5
2025-12-29 21:44:19 -07:00
Joe bae3e9e45d Merge pull request #2074 from BishopFox/polite-zig
Update zig download to be more polite and secure issue #2065
2025-12-29 14:52:04 -07:00
moloch-- 205f5b4196 Update zig download to be more polite and secure issue #2065 2025-12-29 12:04:27 -06:00
xp3s 12f12218e0 fix: DNS beacon Close() should call CloseSession() to prevent UDP port leak
The DNS beacon's Close() method was empty, causing DNS client worker
goroutines and UDP connections to accumulate over time. This fix ensures
that CloseSession() is called to properly release resources when the
beacon connection is closed.
2025-12-15 17:13:19 +08:00
dependabot[bot] fae7e211a7 Bump actions/download-artifact from 6 to 7
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-15 03:02:52 +00:00
dependabot[bot] 346da14431 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-15 03:02:47 +00:00
dependabot[bot] 15174f6c31 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-15 03:02:44 +00:00
dependabot[bot] f29df040bb Bump next from 15.5.7 to 15.5.9 in /docs/sliver-docs
Bumps [next](https://github.com/vercel/next.js) from 15.5.7 to 15.5.9.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.7...v15.5.9)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-12 01:53:24 +00:00
Joe 0bbecc16ec Merge pull request #2060 from BishopFox/go/v1.25.5
Go v1.25.5
2025-12-05 13:29:26 -07:00
moloch-- 0c216161c2 npm update 2025-12-05 12:28:39 -08:00
moloch-- 8ad9c62eb5 Go v1.25.5 2025-12-04 20:12:24 -08:00
Joe 4c99fbfd82 Merge pull request #2059 from BishopFox/e722dependabot/npm_and_yarn/docs/sliver-docs/next-15.5.7
Bump next from 15.5.4 to 15.5.7 in /docs/sliver-docs
2025-12-04 15:48:34 -07:00
dependabot[bot] ed713e199c Bump next from 15.5.4 to 15.5.7 in /docs/sliver-docs
Bumps [next](https://github.com/vercel/next.js) from 15.5.4 to 15.5.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.4...v15.5.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-04 20:55:48 +00:00
Joe aacdb71c4a Merge pull request #2058 from BishopFox/e72edependabot/npm_and_yarn/docs/sliver-docs/mdast-util-to-hast-13.2.1
Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs/sliver-docs
2025-12-04 13:54:17 -07:00
dependabot[bot] 6968c3aae5 Bump mdast-util-to-hast from 13.2.0 to 13.2.1 in /docs/sliver-docs
Bumps [mdast-util-to-hast](https://github.com/syntax-tree/mdast-util-to-hast) from 13.2.0 to 13.2.1.
- [Release notes](https://github.com/syntax-tree/mdast-util-to-hast/releases)
- [Commits](https://github.com/syntax-tree/mdast-util-to-hast/compare/13.2.0...13.2.1)

---
updated-dependencies:
- dependency-name: mdast-util-to-hast
  dependency-version: 13.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-02 04:02:29 +00:00
Joe 410fd93025 Merge pull request #2052 from BishopFox/d161dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-23 22:11:44 -07:00
dependabot[bot] f2a0435dd2 Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-24 03:04:10 +00:00
dependabot[bot] c09c57da3f Bump github.com/klauspost/compress from 1.18.0 to 1.18.1
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Changelog](https://github.com/klauspost/compress/blob/master/.goreleaser.yml)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-03 20:05:33 +00:00
Joe d66ec956a4 Merge pull request #2037 from BishopFox/2979dependabot/go_modules/tailscale.com-1.90.6
Bump tailscale.com from 1.90.2 to 1.90.6
2025-11-03 12:57:04 -07:00
dependabot[bot] 8229722735 Bump tailscale.com from 1.90.2 to 1.90.6
Bumps [tailscale.com](https://github.com/tailscale/tailscale) from 1.90.2 to 1.90.6.
- [Release notes](https://github.com/tailscale/tailscale/releases)
- [Commits](https://github.com/tailscale/tailscale/compare/v1.90.2...v1.90.6)

---
updated-dependencies:
- dependency-name: tailscale.com
  dependency-version: 1.90.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-03 03:04:28 +00:00
Joe dcd81a4db7 Merge pull request #2035 from BishopFox/util/rand
Secure Rand Utils
2025-11-02 10:01:16 -07:00
Joe 2617387417 Merge pull request #2028 from BishopFox/586cdependabot/go_modules/tailscale.com-1.90.2
Bump tailscale.com from 1.88.4 to 1.90.2
2025-11-02 09:39:38 -07:00
moloch-- 56aee7d6e4 Add missing pkg 2025-11-02 09:21:34 -07:00
moloch-- b4f07e8629 Fix various uses of util in implant/ 2025-11-02 08:39:31 -07:00
moloch-- 280cb34049 Replace most instances of insecureRand with util funcs wrapping secure rand 2025-11-02 08:25:14 -07:00
Carter Burn a21b0da928 Merge branch 'BishopFox:master' into feature/aka-command 2025-10-28 21:16:15 -04:00
carterburn 8a0ba038f6 Add readme to give aka command breakdown 2025-10-27 21:29:35 -04:00
Joe 8e5c5f1450 Merge commit from fork
Fix GHSA-q8j9-34qf-7vq7
2025-10-27 15:58:48 -07:00
Joe b8f4f0c1ea Merge pull request #2027 from BishopFox/dbf6dependabot/github_actions/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5
2025-10-26 22:16:59 -07:00
Joe 17cb7fbd50 Merge pull request #2030 from BishopFox/cd33dependabot/github_actions/actions/download-artifact-6
Bump actions/download-artifact from 5 to 6
2025-10-26 22:16:47 -07:00
Joe 9963bc6a3f Merge pull request #2031 from BishopFox/implant/dep-updates
Update implant's x/crypto version
2025-10-26 22:16:30 -07:00
moloch-- 274943f383 Update implant's x/crypto version 2025-10-26 20:13:26 -07:00
dependabot[bot] be81755056 Bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 03:05:46 +00:00
dependabot[bot] a5861c43f2 Bump tailscale.com from 1.88.4 to 1.90.2
Bumps [tailscale.com](https://github.com/tailscale/tailscale) from 1.88.4 to 1.90.2.
- [Release notes](https://github.com/tailscale/tailscale/releases)
- [Commits](https://github.com/tailscale/tailscale/compare/v1.88.4...v1.90.2)

---
updated-dependencies:
- dependency-name: tailscale.com
  dependency-version: 1.90.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 03:04:45 +00:00
dependabot[bot] e3025a2da2 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-27 03:03:53 +00:00
carterburn dd072bbdb6 Merge remote-tracking branch 'origin/master' into feature/aka-command 2025-10-26 21:48:59 -04:00
carterburn 1e3c736acf Update types to support storing aka aliases
Updated the way aka aliases are stored to support saving the aka aliases
to disk.

Aka aliases will automatically be saved to disk and loaded on client
startup.
2025-10-26 21:45:03 -04:00
Joe 3c46cf2210 Merge branch 'master' into socks5-stability 2025-10-24 11:41:27 -07:00
Joe 40cc1f4eeb Merge pull request #2024 from BishopFox/blincoln-2025-10-21-01
Fix Windows network adapter retrieval for DNS C2, add additional logging
2025-10-24 11:41:04 -07:00
carterburn 3d880914d9 Clean up settings and hook for no quotes
Previous commit forced the user to use quotes in the creation command
and in any arguments that include spaces when expanded a command alias.

This commit removes that requirement and allows the user to manipulate
the shell interface as if they were typing commands directly.
2025-10-23 20:39:19 -04:00
carterburn 1162de9ee3 Initial design of command aliases
Command aliases will allow a user to define "aliases" for shorthands and
to pass default arguments

Currently, aliases with more than one word commands (like a command
alias for "execute -o") require the use of single or double quotes to
pass the arguments correctly. Additionally, any use of flags in the
actual command will attempt to parse for the "aka" command and we want
the "aka create" command to be bare to just grab everything after the
command alias name.

Subsequent changes will include:
- better parsing of flags as described above
- saving command aliases to disk
- better help messages
2025-10-23 06:05:01 -04:00
Ben Lincoln b117a1325d Fix Windows network adapter retrieval for DNS C2, add additional logging 2025-10-21 12:08:44 -07:00
Joe a809989536 Merge pull request #2022 from BishopFox/go/v1.25.3
Update to Go v1.25.3
2025-10-19 22:11:07 -07:00
moloch-- a9ee3ca9ed Update some deps 2025-10-19 21:31:03 -07:00
moloch-- 56d972dd32 Update some deps 2025-10-19 21:17:40 -07:00
moloch-- b4091264d7 Update to Go v1.25.3 2025-10-19 21:09:39 -07:00
Joe 23bf325a66 Merge pull request #2015 from BishopFox/sgn-wasm
Sgn wasm
2025-10-19 20:42:54 -07:00
moloch-- d9344a4c79 Run native bin for unit tests 2025-10-19 19:41:04 -07:00
moloch-- 67d9a772e3 Add shellcode+sgn build to unit tests 2025-10-19 18:56:08 -07:00
moloch-- dae284e62e Add sgn unit tests to go-tests.sh 2025-10-19 18:35:11 -07:00
moloch-- 60f48bdc51 Add sgn unit tests 2025-10-19 18:26:15 -07:00
moloch-- 0d7885de29 Replace subprocess with library call 2025-10-19 17:18:34 -07:00
moloch-- d41d29fc1e Replace CGO build of sgn with wasm/pure-go 2025-10-19 17:09:36 -07:00
Joe 3f400bd3f0 Merge pull request #2004 from BishopFox/5069dependabot/github_actions/actions/upload-pages-artifact-4
Bump actions/upload-pages-artifact from 3 to 4
2025-10-12 15:39:17 -07:00
Joe d43f664821 Merge pull request #2003 from BishopFox/7682dependabot/github_actions/actions/setup-go-6
Bump actions/setup-go from 5 to 6
2025-10-12 15:39:06 -07:00
Joe 85c57c07da Merge pull request #2006 from BishopFox/3c4bdependabot/github_actions/actions/download-artifact-5
Bump actions/download-artifact from 4 to 5
2025-10-12 15:38:56 -07:00
Joe 60d15447e0 Merge pull request #2005 from BishopFox/dd46dependabot/github_actions/actions/setup-node-5
Bump actions/setup-node from 4 to 5
2025-10-12 15:38:44 -07:00
Joe 6351b2d671 Merge pull request #2007 from BishopFox/52f6dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-10-12 15:38:32 -07:00
dependabot[bot] c7535ec509 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 03:03:10 +00:00
dependabot[bot] 67e17d073b Bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 03:03:05 +00:00
dependabot[bot] c5cb4dcb6c Bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 03:03:02 +00:00
dependabot[bot] 0b9fe603a8 Bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 03:02:59 +00:00
dependabot[bot] c7141892d1 Bump actions/setup-go from 5 to 6
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-10-06 03:02:56 +00:00
Joe c5556f9e2b Merge pull request #2001 from BishopFox/codex/ioutil
Codex refactors ioutil usage
2025-10-05 18:06:31 -07:00
Joe 1bc45c2ab2 Merge pull request #1985 from hypnoticpattern/fix-daemon-multiplayer-conflict
Add info about potential causes for sliver daemon port binding error
2025-10-05 17:03:19 -07:00
moloch-- 739ad98b81 Codex refactors ioutil usage 2025-10-05 17:02:37 -07:00
Joe 95c1697d1b Merge pull request #1979 from z3xddd/add-ntcreatethreadex-syscall
feat: Add NtCreateThreadEx syscall for advanced evasion techniques
2025-10-05 12:09:34 -07:00
moloch-- f7b9571cfc Increase workflow time limits 2025-10-05 11:52:21 -07:00
Joe 0d08f8c62f Merge pull request #2000 from BishopFox/codex/deps
Dependency Updates
2025-10-05 11:51:52 -07:00
Joe aa9c8d89e9 Merge branch 'master' into add-ntcreatethreadex-syscall 2025-10-05 10:38:30 -07:00
moloch-- 152e60ba29 Fix fmt string syntax 2025-10-05 10:05:52 -07:00
moloch-- d13a71b808 Update most go deps, compiles okay 2025-10-05 10:04:24 -07:00
moloch-- b17e2f056f Update most go deps, compiles okay 2025-10-05 10:04:11 -07:00
moloch-- 42fb1e835e Snap to anchor on load 2025-10-04 21:09:02 -07:00
moloch-- 521356ff6e Make sidebar sticky 2025-10-04 21:03:39 -07:00
moloch-- 74b5d720b2 Fix docs build errors 2025-10-04 20:02:48 -07:00
moloch-- cd75545323 Add mobile version of docs site 2025-10-04 19:07:13 -07:00
Joe 396dc9724f Merge branch 'master' into add-ntcreatethreadex-syscall 2025-10-04 18:05:59 -07:00
moloch-- 33398146d3 Fix broken AV links 2025-10-04 18:02:52 -07:00
moloch-- 21039554e5 Add NoStarch 'Red Team Engineering' book to Community Guides 2025-10-04 18:02:07 -07:00
moloch-- cca73be1c8 Add missing types pkg 2025-10-04 17:57:09 -07:00
moloch-- 596bc455d8 Apply a pixel offset to anchor links 2025-10-04 17:55:28 -07:00
moloch-- 400c36c5df Small tweaks to how code blocks are rendered 2025-10-04 17:52:42 -07:00
moloch-- c94dde3efb Add docs watcher in dev mode, fix markdown syntax, update cross-compiler instructions 2025-10-04 17:32:26 -07:00
moloch-- 827a9ca156 Fix docs errors and warnings 2025-10-04 16:53:56 -07:00
Joe d066dbc49a Merge pull request #1999 from moloch--/codex/docs
Update Docs and Fix Various Quirks
2025-10-04 16:47:31 -07:00
Joe f1fbc345a2 Merge branch 'master' into codex/docs 2025-10-04 16:47:20 -07:00
moloch-- 2889c1ee29 Fix font for <code> 2025-10-04 16:46:31 -07:00
moloch-- 28ee216b36 Fix styling of <pre> blocks 2025-10-04 16:44:17 -07:00
moloch-- 2dd3e84f60 Re-implement markdown rendering 2025-10-04 15:39:20 -07:00
moloch-- 067cdf712d Sticky sidebar 2025-10-04 15:21:37 -07:00
moloch-- 22ae941e17 Tweak sidebar radius 2025-10-04 15:15:10 -07:00
moloch-- 7c0c2c2dda Tweak scroll styling 2025-10-04 15:12:55 -07:00
moloch-- 39de742223 Tweak styling 2025-10-04 15:10:03 -07:00
moloch-- b07e750cf5 Add missing tailwind.config.js 2025-10-04 15:06:19 -07:00
Joe 8786979001 Merge pull request #1998 from moloch--/codex/basics
Codex/basics
2025-10-04 14:53:40 -07:00
moloch-- aa223e4488 Update docs to latest tailwind, react, and heroui, some formatting broken 2025-10-04 14:26:03 -07:00
moloch-- 8785c55cf3 Update zig download to use mirrors 2025-10-04 13:27:52 -07:00
Joe 38fa64d266 Merge branch 'master' into codex/basics 2025-10-04 11:51:50 -07:00
moloch-- b04fff21fd Fix format str errors 2025-10-04 11:49:32 -07:00
moloch-- 3f656da786 Add readmes and AGENT.md 2025-10-04 10:25:58 -07:00
Joe 55311868a7 Merge pull request #1983 from hypnoticpattern/enrich-session-logs
Enrich client logging in implant sessions
2025-10-03 07:21:04 -07:00
Ronan Kervella 422a1d39c3 Merge pull request #1996 from moloch--/go/v1.25
Go v1.25.1
2025-09-18 07:57:18 -07:00
moloch-- 6938759e9d Update workflows to use go v1.25.x 2025-09-17 12:09:56 -07:00
moloch-- 5febe5e539 Go v1.25.1 2025-09-17 10:11:12 -07:00
hypnoticpattern 38479e34aa Add comment about the conflict 2025-08-10 15:02:59 -07:00
hypnoticpattern abe8f8e77b Enrich client logging in implant sessions 2025-08-10 10:39:33 -07:00
Israel Comazzetto dos Reis c0f3161525 feat: Add NtCreateThreadEx syscall for advanced evasion techniques 2025-07-29 01:57:48 -03:00
moloch-- c6bbba16e1 Go v1.24.5 2025-07-21 15:33:25 -07:00
Joe af4f99de51 Merge pull request #1970 from atomicchonk/master
Suggested adjective and noun changes
2025-07-18 07:59:28 -07:00
Max Andreacchi 8e6e8e18ad Update nouns.txt
Suggested updates to nouns list to remove some nouns that result in NSFW names upon beacon generation.

Signed-off-by: Max Andreacchi <[email protected]>
2025-07-17 20:05:13 -04:00
Max Andreacchi 544e577eb2 Update adjectives.txt
Updated list to remove adjectives that could result in NSFW names for beacons

Signed-off-by: Max Andreacchi <[email protected]>
2025-07-17 20:01:30 -04:00
Joe 9939d2c320 Merge pull request #1956 from tandem97/master
closes #1955
2025-06-30 13:28:41 -07:00
Joe f47af1e94e Merge pull request #1958 from Hunter-0x07/fix-c2generator-closure
Fix: correct closure capture in C2Generator to prevent loop variable bug
2025-06-16 17:52:10 -07:00
Hunter-0x07 d3302583f5 Fix: correct closure capture in C2Generator to prevent loop variable bug 2025-06-03 14:37:14 +08:00
tandem97 d9403c7f05 fixes #1955 2025-05-31 00:23:40 +03:00
Joe 65eb72506b Merge pull request #1947 from BishopFox/bugfix_shellcodeDefaultExport
add exports when creating profile for windows shellcode
2025-05-15 06:57:19 -07:00
rkervella 87dfe96ee9 Remove insertion to db, already performed elsewhere 2025-04-18 12:32:48 -07:00
rkervella 5bb4338d76 Add an IPTable to the tun device and set the default INPUT
chain to deny everything. Add an `AllowTCPPort` method to add
exceptions at runtime.
2025-04-18 12:31:45 -07:00
rkervella bb35fa56d6 Use AllowTCPPort to whitelist the required ports for the wg listner 2025-04-18 12:30:59 -07:00
rkervella fbab18b2fe Remove job insertion to db, already done elsewhere 2025-04-18 12:30:22 -07:00
Ronan Kervella edcdfb5937 Merge branch 'master' into socks5-stability 2024-12-04 09:36:59 -10:00
Ronan Kervella f81efdb38c Merge branch 'master' into socks5-stability 2024-12-02 15:40:40 -08:00
Ronan Kervella db34ded0a1 Merge branch 'master' into socks5-stability 2024-11-18 15:06:32 -08:00
navsec 8a7a92a9b9 Refine implant/server signaling for tunnel closures 2024-11-18 17:02:15 -06:00
navsec 3eda32f9d7 Performance Optimizations and Implant SOCKS Changes 2024-11-17 15:01:04 -06:00
navsec c18c193ea7 Improve SOCKS lifecycle mangement and resolve leak 2024-11-06 23:24:44 -06:00
navsec 79a8758448 SOCKS5 Stability Improvements for HTTP/mTLS 2024-10-31 12:46:40 -05:00
11573 changed files with 3751508 additions and 1026255 deletions

No files matched your search

+7
View File
@@ -1,4 +1,9 @@
# 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
@@ -19,3 +24,5 @@
/SECURITY.md
/README.md
/CONTRIBUTING.md
.downloaded_assets
+1 -1
View File
@@ -1 +1 @@
* @BishopFox/red-team
* @BishopFox/bf-red-team
+82 -109
View File
@@ -2,136 +2,109 @@ name: Release
on:
push:
branches: [master]
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
servers-build:
name: Release Build Server
if: startsWith( github.ref, 'refs/tags/v')
tag-check:
name: Check Release Tag
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 30
outputs:
tag: ${{ steps.tag.outputs.tag }}
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@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make
run: make servers
- name: Server Artifacts
uses: actions/upload-artifact@v4
uses: actions/checkout@v7
with:
name: servers
path: ./sliver-server_*
fetch-depth: 0
clients-build:
name: Release Build Client
if: startsWith( github.ref, 'refs/tags/v')
- 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.22
uses: actions/setup-go@v5
with:
go-version: "^1.22"
- name: Check Out Code
uses: actions/checkout@v4
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
- name: Make
run: make clients
- name: Client Artifacts
uses: actions/upload-artifact@v4
uses: actions/checkout@v7
with:
name: clients
path: ./sliver-client_*
fetch-depth: 0
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@v4
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
path: ./builds
go-version: "1.27.3"
- name: Extract Artifacts
run: |
mkdir -p ./artifacts
cp ./builds/clients/* ./artifacts
cp ./builds/servers/* ./artifacts
- name: Install Minisign
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
- name: GPG Key(s)
- name: Build Clients
run: |
cat <(echo -e "${{ secrets.SLIVER_GPG }}") | gpg --batch --import
gpg --list-secret-keys --keyid-format LONG
make clients
mkdir -p artifacts
mv sliver-client_* artifacts/
# Linux
- name: Linux 386 (Client Only)
- name: Build Servers
run: |
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-386.sig --detach-sig ./artifacts/sliver-client_linux-386
- name: Linux AMD64
run: |
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
make servers
mv sliver-server_* artifacts/
# Windows
- name: Windows 386 (Client Only)
- name: Sign Artifacts
env:
MINISIGN_KEY: ${{ secrets.MINISIGN_SLIVER_RELEASE_KEY }}
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
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
# MacOS
- name: MacOS AMD64
- name: Create Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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
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
# 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/*
- name: Upload Artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload "${{ needs.tag-check.outputs.tag }}" artifacts/* --clobber
+7 -7
View File
@@ -16,13 +16,13 @@ jobs:
steps:
# Setup go environment
- name: Go 1.22
uses: actions/setup-go@v5
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "^1.22"
go-version: "1.27.3"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
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@v4
uses: actions/checkout@v7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
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@v3
uses: github/codeql-action/analyze@v4
env:
VERSION: ${{github.sha}}
+551
View File
@@ -0,0 +1,551 @@
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@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v7
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@v7
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@v7
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@v7
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@v7
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@v7
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@v7
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@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v7
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@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v7
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@v4
uses: actions/checkout@v7
- name: Detect package manager
id: detect-package-manager
@@ -56,17 +56,17 @@ jobs:
fi
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
cache: "npm"
cache-dependency-path: "**/package-lock.json"
node-version: "lts/*"
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Restore cache
uses: actions/cache@v4
uses: actions/cache@v6
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@v3
uses: actions/upload-pages-artifact@v5
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@v5
+56
View File
@@ -0,0 +1,56 @@
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@v7
with:
fetch-depth: 0
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "1.27.3"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.14.0
# 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
@@ -0,0 +1,164 @@
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@v7
- name: Set up Go
uses: actions/setup-go@v7
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@v7
- name: Set up Go
uses: actions/setup-go@v7
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@v7
- name: Set up Go
uses: actions/setup-go@v7
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
@@ -0,0 +1,201 @@
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@v7
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@v7
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@v7
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v7
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
+99 -28
View File
@@ -6,11 +6,15 @@ 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
@@ -18,15 +22,15 @@ on:
- ready_for_review
jobs:
linux-windows-build-test:
name: Linux & Windows Test
linux-build-test:
name: Linux Build & Test
runs-on: ubuntu-latest
timeout-minutes: 90
timeout-minutes: 180
steps:
- name: Go 1.22
uses: actions/setup-go@v5
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "^1.22"
go-version: "1.27.3"
- name: OS Packages
run: |
@@ -34,38 +38,92 @@ jobs:
git build-essential zlib1g zlib1g-dev wget zip unzip
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v7
- 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.3
uses: actions/setup-go@v7
with:
go-version: "1.27.3"
- name: Install make
run: choco install make -y --no-progress
- name: Check Out Code
uses: actions/checkout@v7
- 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: 90
timeout-minutes: 180
steps:
- name: Go 1.22
uses: actions/setup-go@v5
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "^1.22"
- name: Mingw
run: brew install mingw-w64
go-version: "1.27.3"
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
@@ -73,24 +131,37 @@ jobs:
- name: Make MacOS (amd64)
run: make macos-amd64
- name: Test MacOS (amd64)
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.22
uses: actions/setup-go@v5
- name: Go 1.27.3
uses: actions/setup-go@v7
with:
go-version: "^1.22"
go-version: "1.27.3"
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Git Fetch Tags
run: git fetch --prune --unshallow --tags -f
+12
View File
@@ -1,7 +1,17 @@
/crush
/examples
/.vscode
/.vscode/*
/.devcontainer
/.idea
/artifacts
/artifacts/**
*.rc
/Dockerfile.*
/*.sdk.tar.xz
@@ -45,6 +55,8 @@ sliver-server-windows.zip
.DS_Store
dist/
.gocache/
.gomodcache/
# Flag to show that go-assets.sh has been run
.downloaded_assets
+27
View File
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,18 @@
# 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.
+8 -4
View File
@@ -5,11 +5,12 @@
#
# 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:1.23.5 AS base
FROM golang:latest AS base
### Base packages
RUN apt-get update --fix-missing && apt-get -y install \
@@ -20,10 +21,10 @@ 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 clean-all
RUN make
RUN make
RUN cp -vv sliver-server /opt/sliver-server
# STAGE: test
@@ -31,6 +32,9 @@ RUN cp -vv sliver-server /opt/sliver-server
## 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}"
RUN apt-get update --fix-missing \
&& apt-get -y upgrade \
&& apt-get -y install \
@@ -39,7 +43,7 @@ RUN apt-get update --fix-missing \
RUN /opt/sliver-server unpack --force
### Run unit tests
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh ${GO_TESTS_FLAGS}
# STAGE: production
## Final dockerized form of Sliver
+293 -165
View File
@@ -2,202 +2,330 @@
# Makefile for Sliver
#
.DEFAULT_GOAL := default
GO ?= go
ARTIFACT_SUFFIX ?=
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
GOLANGCI_LINT ?= golangci-lint
# The tree has legacy lint findings, so gate issues introduced by this change.
GOLANGCI_LINT_BASE ?= master
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
GOLANGCI_LINT_PACKAGES = \
./client/... \
./server/... \
./util/... \
./protobuf/... \
./docs \
./implant/...
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git zip date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Version 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
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)
VERSION ?= $(shell git describe --abbrev=0)
COMPILED_AT = $(shell date +%s)
RELEASES_URL ?= https://api.github.com/repos/BishopFox/sliver/releases
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
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
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
GIT_COMMIT = $(shell git rev-parse HEAD)
.PHONY: lint
lint:
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
ifneq ($(OS),Windows_NT)
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
LDFLAGS = -ldflags "-s -w \
-X $(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 $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(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
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
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
# *** 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
#
# Targets
# >>> WINDOWS <<<
#
.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
else
# 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
SHELL := cmd.exe
.SHELLFLAGS := /C
.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
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-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 = -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: 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
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
.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
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
+7 -28
View File
@@ -1,33 +1,16 @@
# Sliver
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 is an open-source, cross-platform adversary emulation and red team framework designed for organizations of all sizes to perform authorized security testing.
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 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.
[![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)
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.
# v1.6.0 / `master`
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.
**NOTE:** You are looking at 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.
[![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)
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!
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
### Getting Started
@@ -39,16 +22,12 @@ 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). We also tend to hang out in the #golang Slack channel on the [Bloodhound Gang](https://bloodhoundgang.herokuapp.com/) server.
Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions).
### 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.
-135
View File
@@ -1,135 +0,0 @@
#!/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")
+23 -15
View File
@@ -1,18 +1,26 @@
Client
=======
# client
This directory contains the client implementation, pretty much all of this code is also used by the server console.
## Overview
__Important:__ No code in this directory can import any code from `server/`.
Main command-line client for interacting with and administering Sliver servers. Provides the Cobra entrypoint and aggregates console, command, and transport subsystems.
* `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
## 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.
+14 -2
View File
@@ -1,3 +1,15 @@
Assets
======
# client/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.
+24 -6
View File
@@ -34,12 +34,19 @@ const (
SliverClientDirName = ".sliver-client"
versionFileName = "version"
envVarName = "SLIVER_CLIENT_ROOT_DIR"
)
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
func GetRootAppDir() string {
user, _ := user.Current()
dir := filepath.Join(user.HomeDir, SliverClientDirName)
value := os.Getenv(envVarName)
var dir string
if len(value) == 0 {
user, _ := user.Current()
dir = filepath.Join(user.HomeDir, SliverClientDirName)
} else {
dir = value
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0700)
if err != nil {
@@ -73,6 +80,18 @@ 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))
@@ -96,7 +115,7 @@ func Setup(force bool, echo bool) {
if force || localVer == "" || localVer != ver.GitCommit {
if echo {
fmt.Printf(`
Sliver Copyright (C) 2022 Bishop Fox
Sliver Copyright (C) 2026 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.`)
@@ -104,7 +123,6 @@ under certain conditions; type 'licenses' for details.`)
}
saveAssetVersion(appDir)
}
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
SaveSettings(nil)
}
_, _ = LoadSettings()
_, _ = LoadSpoofMetadataConfig()
}
+18 -7
View File
@@ -34,15 +34,26 @@ 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"`
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"`
}
// GetConfigDir - Returns the path to the config dir
+29
View File
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,27 @@
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)
}
+92 -16
View File
@@ -22,39 +22,111 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
const (
settingsFileName = "tui-settings.json"
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"
)
// ClientSettings - Client JSON config
type ClientSettings struct {
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"`
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"`
}
// LoadSettings - Load the client settings from disk
func LoadSettings() (*ClientSettings, error) {
rootDir, _ := filepath.Abs(GetRootAppDir())
data, err := os.ReadFile(filepath.Join(rootDir, settingsFileName))
if err != nil {
settingsPath := filepath.Join(rootDir, settingsFileName)
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
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) {
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
}
settings := defaultSettings()
err = json.Unmarshal(data, settings)
if err != nil {
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",
@@ -64,16 +136,20 @@ 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 := json.MarshalIndent(settings, "", " ")
data, err := yaml.Marshal(settings)
if err != nil {
return err
}
+137
View File
@@ -0,0 +1,137 @@
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
@@ -0,0 +1,216 @@
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: "",
},
},
}
}
+13 -3
View File
@@ -1,4 +1,14 @@
CLI
===
# client/cli
This package implements the Sliver client 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.
+37 -3
View File
@@ -19,6 +19,7 @@ package cli
*/
import (
"errors"
"fmt"
"log"
"os"
@@ -34,6 +35,8 @@ const (
logFileName = "sliver-client.log"
)
var clientLogFile *os.File
// Initialize logging.
func initLogging(appDir string) *os.File {
log.SetFlags(log.LstdFlags | log.Lshortfile)
@@ -47,10 +50,14 @@ func initLogging(appDir string) *os.File {
func init() {
appDir := assets.GetRootAppDir()
logFile := initLogging(appDir)
defer logFile.Close()
clientLogFile = initLogging(appDir)
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
@@ -69,6 +76,9 @@ 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
@@ -89,10 +99,34 @@ var rootCmd = &cobra.Command{
Long: ``,
}
type commandExitError struct {
code int
err error
}
func (e *commandExitError) Error() string {
return e.err.Error()
}
func (e *commandExitError) Unwrap() error {
return e.err
}
func commandExitCode(err error) int {
if err == nil {
return 0
}
if exitErr, ok := errors.AsType[*commandExitError](err); ok {
return exitErr.code
}
return 1
}
// Execute - Execute root command.
func Execute() {
defer func() { _ = clientLogFile.Close() }()
if err := rootCmd.Execute(); err != nil {
fmt.Printf("root command: %s\n", err)
os.Exit(1)
os.Exit(commandExitCode(err))
}
}
+23
View File
@@ -0,0 +1,23 @@
package cli
import (
"log"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/client/assets"
)
func TestPackageLoggerRemainsOpenAfterInit(t *testing.T) {
logFile, ok := log.Writer().(*os.File)
if !ok {
t.Fatalf("standard logger writer = %T, want *os.File", log.Writer())
}
if want := filepath.Join(assets.GetRootAppDir(), logFileName); filepath.Clean(logFile.Name()) != want {
t.Fatalf("standard logger file = %q, want %q", logFile.Name(), want)
}
if _, err := logFile.Stat(); err != nil {
t.Fatalf("standard logger file is closed after package initialization: %v", err)
}
}
+27 -29
View File
@@ -20,51 +20,49 @@ package cli
import (
"fmt"
"path/filepath"
"sort"
"github.com/bishopfox/sliver/client/assets"
"gopkg.in/AlecAivazis/survey.v1"
"github.com/bishopfox/sliver/client/forms"
)
func selectConfig() *assets.ClientConfig {
configs := assets.GetConfigs()
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 selectConfigFrom(configs map[string]*assets.ClientConfig, stdinTTY bool) (string, *assets.ClientConfig, error) {
if len(configs) == 0 {
return nil
return "", nil, fmt.Errorf("no config files found at %s", assets.GetConfigDir())
}
if len(configs) == 1 {
for _, config := range configs {
return config
for key, config := range configs {
return key, config, nil
}
}
answer := struct{ Config string }{}
qs := getPromptForConfigs(configs)
err := survey.Ask(qs, &answer)
if err != nil {
fmt.Println(err.Error())
return nil
if !stdinTTY {
return "", nil, fmt.Errorf("multiple configs found; use --config to select one")
}
return configs[answer.Config]
}
func getPromptForConfigs(configs map[string]*assets.ClientConfig) []*survey.Question {
keys := []string{}
for k := range configs {
keys = append(keys, k)
keys := make([]string, 0, len(configs))
for key := range configs {
keys = append(keys, key)
}
sort.Strings(keys)
return []*survey.Question{
{
Name: "config",
Prompt: &survey.Select{
Message: "Select a server:",
Options: keys,
Default: keys[0],
},
},
selection := keys[0]
err := forms.Select("Select a server:", keys, &selection)
if err != nil {
return "", nil, err
}
return selection, configs[selection], nil
}
+46
View File
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,177 @@
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
@@ -0,0 +1,145 @@
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
@@ -0,0 +1,26 @@
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)
}
}
}
+42 -46
View File
@@ -19,20 +19,21 @@ package cli
*/
import (
"context"
"fmt"
"os"
"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"
"google.golang.org/grpc/connectivity"
)
// 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 := &cobra.Command{
@@ -40,66 +41,61 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
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) {
var ln *grpc.ClientConn
pre = func(_ *cobra.Command, _ []string) error {
configs := assets.GetConfigs()
if len(configs) == 0 {
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
return nil
pre = func(cmd *cobra.Command, _ []string) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}
config := selectConfig()
if config == nil {
return nil
rcScript, err := ReadRCScript(cmd)
if err != nil {
return err
}
configPath := ""
if cmd.Flags().Lookup(ConsoleConfigFlagName) != nil {
configPath, err = cmd.Flags().GetString(ConsoleConfigFlagName)
if err != nil {
return err
}
}
stdinTTY := term.IsTerminal(int(os.Stdin.Fd()))
configKey, config, err := selectConfig(configPath, stdinTTY)
if err != nil {
return err
}
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 {
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
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)
}
var rpc rpcpb.SliverRPCClient
var err error
rpc, ln, err = transport.MTLSConnect(config)
if err != nil {
fmt.Printf("Connection to server failed %s", err)
return nil
return fmt.Errorf("connection to server failed: %w", err)
}
// Wait for any connection state changes and exit if the connection is lost.
go handleConnectionLost(ln)
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run)
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
}
// Close the RPC connection once exiting
post = func(_ *cobra.Command, _ []string) error {
if ln != nil {
return ln.Close()
}
return nil
return con.CloseConnection()
}
return pre, post
}
func handleConnectionLost(ln *grpc.ClientConn) {
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)
}
}
}
+3 -1
View File
@@ -57,7 +57,9 @@ func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, pos
// 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 {
startConsole(implantCmd, args)
if err := startConsole(implantCmd, args); err != nil {
return err
}
// Set the active target.
target, _ := implantCmd.Flags().GetString("use")
+21 -13
View File
@@ -20,7 +20,6 @@ package cli
import (
"fmt"
"os"
"github.com/bishopfox/sliver/client/assets"
"github.com/rsteube/carapace"
@@ -32,19 +31,12 @@ func importCmd() *cobra.Command {
Use: "import",
Short: "Import a client configuration file",
Long: `import [config files]`,
Run: func(cmd *cobra.Command, args []string) {
if 0 < len(args) {
for _, arg := range args {
conf, err := assets.ReadConfig(arg)
if err != nil {
fmt.Printf("[!] %s\n", err)
os.Exit(3)
}
assets.SaveConfig(conf)
}
} else {
fmt.Printf("Missing config file path, see --help")
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Missing config file path, see --help")
return nil
}
return importConfigs(args)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{}, cobra.ShellCompDirectiveDefault
@@ -55,3 +47,19 @@ func importCmd() *cobra.Command {
return cmdImport
}
func importConfigs(paths []string) error {
for _, path := range paths {
conf, err := assets.ReadConfig(path)
if err != nil {
return &commandExitError{
code: 3,
err: fmt.Errorf("read import %q: %w", path, err),
}
}
if err := assets.SaveConfig(conf); err != nil {
return fmt.Errorf("save import %q: %w", path, err)
}
}
return nil
}
+125
View File
@@ -0,0 +1,125 @@
package cli
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
)
func writeImportConfig(t *testing.T, name, contents string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name)
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
}
func TestImportCommandReportsInvalidConfig(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
path := writeImportConfig(t, "missing-operator.cfg", `{"lhost":"server"}`)
cmd := importCmd()
err := cmd.RunE(cmd, []string{path})
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
t.Fatalf("import error = %v, want error identifying %q", err, path)
}
if got := commandExitCode(err); got != 1 {
t.Fatalf("invalid config exit code = %d, want 1", got)
}
}
func TestImportCommandReportsConfigWriteFailure(t *testing.T) {
clientRoot := t.TempDir()
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
path := writeImportConfig(t, "alice.cfg", `{"operator":"alice","lhost":"server"}`)
configDir := filepath.Join(clientRoot, "configs")
if err := os.MkdirAll(filepath.Join(configDir, "alice_server.cfg"), 0o700); err != nil {
t.Fatal(err)
}
cmd := importCmd()
err := cmd.RunE(cmd, []string{path})
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
t.Fatalf("import error = %v, want write error identifying %q", err, path)
}
if got := commandExitCode(err); got != 1 {
t.Fatalf("config write failure exit code = %d, want 1", got)
}
if _, ok := errors.AsType[*os.PathError](err); !ok {
t.Fatalf("config write failure does not wrap the filesystem error: %v", err)
}
}
func TestImportCommandReadFailureExitCode(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
path := filepath.Join(t.TempDir(), "missing.cfg")
cmd := importCmd()
err := cmd.RunE(cmd, []string{path})
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
t.Fatalf("import error = %v, want read error identifying %q", err, path)
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("import error does not wrap the missing file error: %v", err)
}
if got := commandExitCode(fmt.Errorf("command failed: %w", err)); got != 3 {
t.Fatalf("wrapped read failure exit code = %d, want 3", got)
}
}
func TestImportCommandParseFailureExitCode(t *testing.T) {
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
path := writeImportConfig(t, "malformed.cfg", `{"operator":`)
cmd := importCmd()
err := cmd.RunE(cmd, []string{path})
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
t.Fatalf("import error = %v, want parse error identifying %q", err, path)
}
if _, ok := errors.AsType[*json.SyntaxError](err); !ok {
t.Fatalf("import error does not wrap the JSON syntax error: %v", err)
}
if got := commandExitCode(err); got != 3 {
t.Fatalf("parse failure exit code = %d, want 3", got)
}
}
func TestImportCommandStopsAfterFirstFailure(t *testing.T) {
clientRoot := t.TempDir()
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
invalidPath := writeImportConfig(t, "invalid.cfg", `{"lhost":"server"}`)
validPath := writeImportConfig(t, "valid.cfg", `{"operator":"alice","lhost":"server"}`)
cmd := importCmd()
err := cmd.RunE(cmd, []string{invalidPath, validPath})
if err == nil || !strings.Contains(err.Error(), strconv.Quote(invalidPath)) {
t.Fatalf("import error = %v, want first file %q", err, invalidPath)
}
if _, err := os.Stat(filepath.Join(clientRoot, "configs", "alice_server.cfg")); !os.IsNotExist(err) {
t.Fatalf("later config was imported after first failure: stat error = %v", err)
}
}
func TestImportCommandMissingPathPreservesSuccess(t *testing.T) {
cmd := importCmd()
var output bytes.Buffer
cmd.SetOut(&output)
err := cmd.RunE(cmd, nil)
if err != nil {
t.Fatalf("import without a path failed: %v", err)
}
if got := commandExitCode(err); got != 0 {
t.Fatalf("missing path exit code = %d, want 0", got)
}
if got, want := output.String(), "Missing config file path, see --help"; got != want {
t.Fatalf("missing path output = %q, want %q", got, want)
}
}
+110
View File
@@ -0,0 +1,110 @@
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"
"time"
"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"
"google.golang.org/grpc/keepalive"
)
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
}
// Keep the gRPC link alive while the MCP bridge sits idle between tool calls.
// The daemon permits pings >=30s without streams (server.yaml: grpc.keepalive).
transport.SetKeepaliveParams(keepalive.ClientParameters{
Time: 60 * time.Second,
Timeout: 20 * time.Second,
PermitWithoutStream: true,
})
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
// Do not install the connection-lost watchdog here: gRPC channels idle
// out after 30 minutes without RPCs (grpc-go default) and transparently
// reconnect on the next call. Exiting on the Idle state would kill an
// otherwise healthy MCP bridge between tool calls.
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,42 @@
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
@@ -0,0 +1,136 @@
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",
},
}
}
+65 -11
View File
@@ -1,14 +1,68 @@
Command
=======
# client/command
This package contains all of the client command implementations that are shared between the client and the server console.
## Overview
### Developers
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.
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.
## 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.
+5
View File
@@ -0,0 +1,5 @@
# 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
@@ -0,0 +1,242 @@
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
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,591 @@
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
@@ -0,0 +1,23 @@
# 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
@@ -0,0 +1,59 @@
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
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,68 @@
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)
}
+10 -7
View File
@@ -1,11 +1,14 @@
Alias
===========
# client/command/alias
Wraps external DLLs/tool in Sliver console commands.
## Overview
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
### Format
* `alias-dir/`
* * `manifest.json` -
- `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.
+251
View File
@@ -0,0 +1,251 @@
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
@@ -0,0 +1,259 @@
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")
}
}
+3
View File
@@ -1,6 +1,7 @@
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"
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
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]",
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
aliasCmd.AddCommand(aliasInstallCmd)
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
aliasRemove := &cobra.Command{
Use: consts.RmStr + " [ALIAS]",
+4 -6
View File
@@ -24,9 +24,9 @@ import (
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
"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"
)
@@ -62,8 +62,7 @@ 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
prompt := &survey.Confirm{Message: "Overwrite current install?"}
survey.AskOne(prompt, &confirm)
_ = forms.Confirm("Overwrite current install?", &confirm)
if !confirm {
return
}
@@ -114,7 +113,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
} else {
errorMsg = fmt.Sprintf("Failed to parse %s: %s\n", ManifestFileName, err)
}
con.PrintErrorf(errorMsg)
con.PrintErrorf("%s", errorMsg)
return nil
}
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.CommandName))
@@ -122,8 +121,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
if promptToOverwrite {
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
confirm := false
prompt := &survey.Confirm{Message: "Overwrite current install?"}
survey.AskOne(prompt, &confirm)
_ = forms.Confirm("Overwrite current install?", &confirm)
if !confirm {
return nil
}
+209 -17
View File
@@ -27,15 +27,16 @@ import (
"path/filepath"
"strings"
"github.com/AlecAivazis/survey/v2"
"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"
app "github.com/reeflective/console"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -76,6 +77,16 @@ type AliasFile struct {
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.
type AliasManifest struct {
Name string `json:"name"`
@@ -89,6 +100,7 @@ type AliasManifest struct {
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"`
@@ -158,21 +170,69 @@ 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: aliasManifest.CommandName,
Use: usage.String(),
Short: helpMsg,
Long: longHelpMsg,
Long: help.FormatHelpTmpl(longHelp.String()),
Run: func(cmd *cobra.Command, args []string) {
runAliasCommand(cmd, con, args)
},
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
GroupID: consts.AliasHelpGroup,
Annotations: makeAliasPlatformFilters(aliasManifest),
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),
}
if aliasManifest.IsAssembly {
@@ -182,9 +242,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", "", "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)")
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")
addAliasCmd.Flags().AddFlagSet(f)
}
@@ -193,9 +253,13 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
comps := carapace.Gen(addAliasCmd)
makeAliasArgCompleter(aliasManifest, comps)
cmd.AddCommand(addAliasCmd)
// Have to use a global map here, as passing the aliasCmd
@@ -272,6 +336,27 @@ 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)
@@ -311,10 +396,9 @@ 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(msgStr)
con.PrintWarnf("%s", msgStr)
confirm := false
prompt := &survey.Confirm{Message: "Do you want to continue?"}
survey.AskOne(prompt, &confirm, nil)
forms.Confirm("Do you want to continue?", &confirm)
if !confirm {
return
}
@@ -340,7 +424,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
var outFilePath *os.File
if save, _ := cmd.Flags().GetBool("save"); save {
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
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)))
outFilePath, err = os.CreateTemp("", outFile)
if err != nil {
con.PrintErrorf("%s\n", err)
@@ -590,3 +680,105 @@ 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")
}
+2 -3
View File
@@ -24,9 +24,9 @@ import (
"os"
"path/filepath"
"github.com/AlecAivazis/survey/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
"github.com/spf13/cobra"
)
@@ -39,8 +39,7 @@ func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
return
}
confirm := false
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' alias?", name)}
survey.AskOne(prompt, &confirm)
_ = forms.Confirm(fmt.Sprintf("Remove '%s' alias?", name), &confirm)
if !confirm {
return
}
+16
View File
@@ -0,0 +1,16 @@
# 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.
+16 -16
View File
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
// Something is wrong with this entry
pkgCache.Delete(value)
pkgCache.Delete(key)
return true
}
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
@@ -428,7 +428,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.SetTitle(console.Bold + "Packages" + console.Normal)
tw.SetTitle(console.StyleBold.Render("Packages"))
urlMargin := 150 // Extra margin needed to show URL column
@@ -493,26 +493,26 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
rows := []table.Row{}
for _, pkg := range entries {
color := console.Normal
style := console.StyleNormal
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
color = console.Green
style = console.StyleGreen
}
if con.Settings.SmallTermWidth+urlMargin < width {
rows = append(rows, table.Row{
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
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),
style.Render(pkg.ArmoryName),
style.Render(pkg.CommandName),
style.Render(pkg.Version),
style.Render(pkg.Type),
style.Render(pkg.Help),
style.Render(pkg.URL),
})
} else {
rows = append(rows, table.Row{
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
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),
style.Render(pkg.ArmoryName),
style.Render(pkg.CommandName),
style.Render(pkg.Version),
style.Render(pkg.Type),
style.Render(pkg.Help),
})
}
}
@@ -524,7 +524,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
tw.SetTitle(console.StyleBold.Render("Bundles"))
tw.AppendHeader(table.Row{
"Armory Name",
"Name",
+19 -1
View File
@@ -1,5 +1,23 @@
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"
@@ -37,7 +55,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Use: consts.InstallStr,
Short: "Install an alias or extension",
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.InstallStr}),
Args: cobra.ExactArgs(1),
Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) {
ArmoryInstallCmd(cmd, con, args)
},
+274 -95
View File
@@ -22,12 +22,14 @@ 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"
@@ -35,7 +37,7 @@ import (
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/util/minisign"
)
@@ -51,10 +53,9 @@ const (
// ArmoryInstallCmd - The armory install command
func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
var promptToOverwrite bool
name := args[0]
if name == "" {
con.PrintErrorf("A package or bundle name is required")
return
names := []string{}
if len(args) > 0 {
names = []string{args[0]}
}
forceInstallation, err := cmd.Flags().GetBool("force")
if err != nil {
@@ -83,51 +84,92 @@ func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
clientConfig := parseArmoryHTTPConfig(cmd)
refresh(clientConfig)
if name == "all" {
aliases, extensions := packageManifestsInCache()
aliasCount, extCount := countUniqueCommandsFromManifests(aliases, extensions)
confirm := false
pluralAliases := "es"
if aliasCount == 1 {
pluralAliases = ""
if len(names) == 0 {
armoryFilterName := ""
if armoryPK != "" {
armoryFilterName = armoryName
}
pluralExtensions := "s"
if extCount == 1 {
pluralExtensions = ""
}
prompt := &survey.Confirm{
Message: fmt.Sprintf("Install %d alias%s and %d extension%s?",
aliasCount, pluralAliases, extCount, pluralExtensions,
),
}
survey.AskOne(prompt, &confirm)
if !confirm {
options := armoryInstallOptions(armoryPK, armoryFilterName)
if len(options) == 0 {
con.PrintInfof("No packages or bundles found\n")
return
}
promptToOverwrite = false
}
err = installPackageByName(name, armoryPK, forceInstallation, promptToOverwrite, clientConfig, con)
if err == nil {
return
}
if errors.Is(err, ErrPackageNotFound) {
bundles := bundlesInCache()
for _, bundle := range bundles {
if bundle.Name == name {
installBundle(bundle, armoryPK, forceInstallation, clientConfig, con)
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 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)
if len(result.SelectedNames) == 0 {
return
}
names = result.SelectedNames
}
if len(names) == 0 {
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
}
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("No package or bundle named %q was found in armory %s\n", name, armoryName)
con.PrintErrorf("Could not install package: %s\n", err)
}
} 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)
}
}
@@ -232,19 +274,22 @@ func getInstalledPackageNames() []string {
}
// This is a convenience function to get the names of the commands in the cache
func getCommandsInCache() []string {
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
}
if cacheEntry.Pkg.IsAlias {
if !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
if cacheEntry.Alias.CommandName != "" && !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
commandNames = append(commandNames, cacheEntry.Alias.CommandName)
}
} else {
for _, command := range cacheEntry.Extension.ExtCommand {
if !slices.Contains(commandNames, command.CommandName) {
if command.CommandName != "" && !slices.Contains(commandNames, command.CommandName) {
commandNames = append(commandNames, command.CommandName)
}
}
@@ -256,32 +301,44 @@ func getCommandsInCache() []string {
return commandNames
}
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
if cacheEntry.Pkg.IsAlias {
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
}
if cacheEntry.Extension == nil {
return false
}
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
return true
}
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
return true
}
}
return false
}
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
packages := []*pkgCacheEntry{}
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry := value.(pkgCacheEntry)
if cacheEntry.LastErr == nil {
if cacheEntry.Pkg.IsAlias {
if cacheEntry.Alias.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
}
}
} else {
for _, command := range cacheEntry.Extension.ExtCommand {
if command.CommandName == name {
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
packages = append(packages, &cacheEntry)
}
break
}
}
}
}
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
})
@@ -291,16 +348,12 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
func getPackageIDFromUser(name string, options map[string]string) string {
selectedPackageKey := ""
optionKeys := util.Keys(options)
optionKeys := slices.Collect(maps.Keys(options))
slices.Sort(optionKeys)
// Add a cancel option
optionKeys = append(optionKeys, doNotInstallOption)
options[doNotInstallOption] = doNotInstallPackageName
prompt := &survey.Select{
Message: fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name),
Options: optionKeys,
}
survey.AskOne(prompt, &selectedPackageKey)
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
@@ -360,7 +413,7 @@ func buildInstallList(name, armoryPK string, forceInstallation bool, pendingPack
var requestedPackageList []string
if name == "all" {
requestedPackageList = []string{}
allCommands := getCommandsInCache()
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
@@ -420,6 +473,15 @@ func installPackageByName(name, armoryPK string, forceInstallation, promptToOver
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)
@@ -444,12 +506,114 @@ func installPackageByName(name, armoryPK string, forceInstallation, promptToOver
if name == "all" {
con.Printf("\n")
con.PrintInfof("Operation complete\n")
con.PrintSuccessf("Operation complete\n")
}
return nil
}
func armoryInstallOptions(armoryPK, armoryName string) []forms.ArmoryInstallOption {
options := []forms.ArmoryInstallOption{}
seen := make(map[string]bool)
packageCount := 0
pkgCache.Range(func(key, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok {
return true
}
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")
@@ -483,20 +647,15 @@ func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientCon
return errors.New("signature verification failed")
}
tmpFile, err := os.CreateTemp("", "sliver-armory-")
tmpFileName, err := writeArmoryTempFile(tarGz)
if err != nil {
return err
}
defer os.Remove(tmpFile.Name())
_, err = tmpFile.Write(tarGz)
if err != nil {
return err
}
tmpFile.Close()
defer os.Remove(tmpFileName)
con.Printf(console.Clearln + "\r") // Clear the line
installPath := alias.InstallFromFile(tmpFile.Name(), entry.Alias.CommandName, promptToOverwrite, con)
installPath := alias.InstallFromFile(tmpFileName, entry.Alias.CommandName, promptToOverwrite, con)
if installPath == nil {
return errors.New("failed to install alias")
}
@@ -577,23 +736,43 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
return errors.New("signature verification failed")
}
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()
tmpFileName, err := writeArmoryTempFile(tarGz)
if err != nil {
return err
}
defer os.Remove(tmpFileName)
con.Printf(console.Clearln + "\r") // Clear download message
extensions.InstallFromDir(tmpFile.Name(), promptToOverwrite, con, true)
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
}
return nil
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
}
+218
View File
@@ -0,0 +1,218 @@
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)
}
}
+20 -3
View File
@@ -1,5 +1,23 @@
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"
@@ -8,10 +26,10 @@ import (
"strings"
"sync"
"github.com/AlecAivazis/survey/v2"
"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"
)
@@ -602,8 +620,7 @@ func ModifyArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if err != nil {
con.PrintErrorf("could not modify armory: %s\n", err)
confirm := false
prompt := &survey.Confirm{Message: "Would you like to revert the properties of the armory back to their previous values?"}
survey.AskOne(prompt, &confirm, nil)
_ = forms.Confirm("Would you like to revert the properties of the armory back to their previous values?", &confirm)
if !confirm {
return
} else {
+73 -86
View File
@@ -19,13 +19,13 @@ package armory
*/
import (
"errors"
"fmt"
"maps"
"os"
"slices"
"sort"
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
@@ -34,13 +34,14 @@ import (
"github.com/bishopfox/sliver/client/command/extensions"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/client/forms"
)
type VersionInformation struct {
OldVersion string
NewVersion string
ArmoryName string
PackageID string
}
type PackageType uint
@@ -87,16 +88,19 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if len(aliasUpdates) > 0 || len(extUpdates) > 0 {
updateKeys := sortUpdateIdentifiers(aliasUpdates, extUpdates)
displayAvailableUpdates(con, updateKeys, aliasUpdates, extUpdates)
selectedUpdates, err = getUpdatesFromUser(con, updateKeys)
selectedUpdates, err = getUpdatesFromUser(updateKeys)
if err != nil {
con.PrintErrorf(err.Error() + "\n")
if errors.Is(err, forms.ErrUserAborted) {
return
}
con.PrintErrorf("%s\n", err.Error())
return
}
if len(selectedUpdates) == 0 {
return
}
} else {
con.PrintSuccessf("All packages are up to date")
con.PrintSuccessf("All packages are up to date\n")
return
}
@@ -122,9 +126,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
if !ok {
continue
}
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
if err != nil {
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
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)
@@ -171,8 +175,21 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
return results
}
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
if localManifest.PackageName != "" {
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
}
for _, localCommand := range localManifest.ExtCommand {
for _, latestCommand := range latestManifest.ExtCommand {
if localCommand.CommandName == latestCommand.CommandName {
return true
}
}
}
return false
}
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
_, cachedExtensions := packageManifestsInCache()
// Return a map of extension name to minimum version to install
results := make(map[string]VersionInformation)
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
@@ -184,7 +201,12 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
if err != nil {
continue
}
for _, latestExt := range cachedExtensions {
pkgCache.Range(func(_, value interface{}) bool {
cacheEntry, ok := value.(pkgCacheEntry)
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
return true
}
latestExt := cacheEntry.Extension
/*
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,
@@ -192,16 +214,22 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
so we will rely on the author of the package incrementing the version identifier somehow to determine
if a package is newer.
*/
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
results[localManifest.Name] = VersionInformation{
OldVersion: localManifest.Version,
NewVersion: latestExt.Version,
ArmoryName: latestExt.ArmoryName,
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
@@ -217,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
result := []UpdateIdentifier{}
aliasNames := util.Keys(aliasUpdates)
extensionNames := util.Keys(extensionUpdates)
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
for _, name := range aliasNames {
result = append(result, UpdateIdentifier{
Type: AliasPackage,
@@ -255,7 +283,7 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
if len(extensionUpdates) != 1 {
extensionSuffix = "s"
}
tw.SetTitle(console.Bold + fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix) + console.Normal)
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix)))
tw.AppendHeader(table.Row{
"Package Name",
"Package Type",
@@ -299,76 +327,35 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
con.Printf("%s\n\n", tw.Render())
}
func getUpdatesFromUser(con *console.SliverClient, updateKeys []UpdateIdentifier) (chosenUpdates []UpdateIdentifier, selectionError error) {
chosenUpdates = []UpdateIdentifier{}
func getUpdatesFromUser(updateKeys []UpdateIdentifier) ([]UpdateIdentifier, error) {
options := make([]forms.ArmoryUpdateOption, 0, len(updateKeys))
optionIndex := make(map[string]UpdateIdentifier, len(updateKeys))
var updateResponse string
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
}
con.Println("You can apply all, none, or some updates.\nTo apply some updates, specify the number of a single update, a range (1-3), or a combination of the two (1, 3-5, 7)\n")
err := survey.AskOne(&survey.Input{
Message: "Which updates would you like to apply? [A]ll, [N]one, or some:",
}, &updateResponse)
result, err := forms.ArmoryUpdateForm(options)
if err != nil {
// We do not need to set selectionError because an error here means the same thing as responding with "none"
return
return nil, err
}
updateResponse = strings.ToLower(updateResponse)
updateResponse = strings.Replace(updateResponse, " ", "", -1)
if updateResponse == "n" || updateResponse == "none" {
return
}
if updateResponse == "a" || updateResponse == "all" {
chosenUpdates = updateKeys
return
}
selections := strings.Split(updateResponse, ",")
for _, selection := range selections {
// Handle a range
if strings.Contains(selection, "-") {
rangeParts := strings.Split(selection, "-")
start, err := strconv.Atoi(rangeParts[0])
if err != nil {
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[0])
return
}
end, err := strconv.Atoi(rangeParts[1])
if err != nil {
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[1])
return
}
// Adjust for the 0 indexed slice we are working with
start -= 1
end -= 1
if start < 0 {
start = 0
}
if start > end {
selectionError = fmt.Errorf("%s is not a valid range", selection)
return
}
if end >= len(updateKeys) {
end = len(updateKeys) - 1
}
for i := start; i <= end; i++ {
chosenUpdates = append(chosenUpdates, updateKeys[i])
}
} else {
// Single entry
index, err := strconv.Atoi(selection)
if err != nil {
selectionError = fmt.Errorf("%s is not a valid range", selection)
return
}
index -= 1
if index >= 0 && index < len(updateKeys) {
chosenUpdates = append(chosenUpdates, updateKeys[index])
}
chosenUpdates := make([]UpdateIdentifier, 0, len(result.SelectedIDs))
for _, selectedID := range result.SelectedIDs {
if update, ok := optionIndex[selectedID]; ok {
chosenUpdates = append(chosenUpdates, update)
}
}
return
return chosenUpdates, nil
}
+9 -3
View File
@@ -1,4 +1,10 @@
Backdoor
========
# client/command/backdoor
Implements command related to `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.
+13 -3
View File
@@ -1,4 +1,14 @@
Beacons
========
# client/command/beacons
This package implements the beacons command, which can be used to list and interact with 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.
+163 -94
View File
@@ -30,6 +30,7 @@ import (
"github.com/bishopfox/sliver/client/command/kill"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
@@ -39,6 +40,7 @@ import (
func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
killFlag, _ := cmd.Flags().GetString("kill")
killAll, _ := cmd.Flags().GetBool("kill-all")
interact, _ := cmd.Flags().GetString("interact")
// Handle kill
if killFlag != "" {
@@ -72,6 +74,18 @@ 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,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
if err != nil {
width = 999
}
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
}
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
wideTermWidth := con.Settings.SmallTermWidth < width
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
// preventing a second boundary from changing the table width while it is fitted.
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
windowsBeaconInList := false
for _, beacon := range beacons {
if beacon.OS == "windows" {
windowsBeaconInList = true
break
}
}
if wideTermWidth {
if windowsBeaconInList {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Integrity",
"Operating System",
"Locale",
"Last Check-in",
"Next Check-in",
})
} else {
tw.AppendHeader(table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
"Operating System",
"Locale",
"Last Check-in",
"Next Check-in",
})
}
} else {
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
})
}
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
tw := table.NewWriter()
tw.SetStyle(settings.GetTableStyle(con))
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
if layout.Compact {
tw.AppendHeader(table.Row{
"ID",
"Name",
@@ -162,79 +156,154 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
"Last Check-in",
"Next Check-in",
})
} else {
header := table.Row{
"ID",
"Name",
"Tasks",
"Transport",
"Remote Address",
"Hostname",
"Username",
"Process (PID)",
}
if includeIntegrity {
header = append(header, "Integrity")
}
header = append(header, "Operating System")
if layout.IncludeLocale {
header = append(header, "Locale")
}
header = append(header, "Last Check-in", "Next Check-in")
tw.AppendHeader(header)
}
activeBeaconID := ""
if con.ActiveTarget != nil {
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
activeBeaconID = activeBeacon.ID
}
}
for _, beacon := range beacons {
color := console.Normal
activeBeacon := con.ActiveTarget.GetBeacon()
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
color = console.Green
integrity := beacon.Integrity
if integrity == "" {
integrity = "-"
}
if beacon.Integrity == "" {
beacon.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
}
// We need a slice of strings so we can apply filters
var rowEntries []string
if wideTermWidth {
if layout.Compact {
rowEntries = []string{
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 (%d)"+console.Normal, beacon.Filename, beacon.PID),
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),
}
if windowsBeaconInList {
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, beacon.Integrity))
}
rowEntries = append(rowEntries, []string{
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 {
rowEntries = []string{
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),
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)),
}
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)
}
// Apply filters if any
if filter == "" && filterRegex == nil {
tw.AppendRow(row)
} else {
for _, rowEntry := range rowEntries {
if filter != "" {
if strings.Contains(rowEntry, filter) {
tw.AppendRow(row)
break
}
}
if filterRegex != nil {
if filterRegex.MatchString(rowEntry) {
tw.AppendRow(row)
break
}
}
}
}
tw.AppendRow(row)
}
return tw
}
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
if filter == "" && filterRegex == nil {
return true
}
for _, entry := range entries {
if filter != "" && strings.Contains(entry, filter) {
return true
}
if filterRegex != nil && filterRegex.MatchString(entry) {
return true
}
}
return false
}
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
date := timestamp.Format(time.UnixDate)
interval := ""
if timestamp.Before(now) {
delta := now.Sub(timestamp).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (%s ago)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldRed.Render(interval)
}
} else {
delta := timestamp.Sub(now).Round(time.Second)
if includeDate {
interval = fmt.Sprintf("%s (in %s)", date, delta)
} else {
interval = delta.String()
}
if color {
interval = console.StyleBoldGreen.Render(interval)
}
}
return interval
}
+257
View File
@@ -0,0 +1,257 @@
package beacons
import (
"regexp"
"strings"
"testing"
"time"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/command/targettable"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
)
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
beacon := testWindowsBeacon()
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("full process path missing from table:\n%s", rendered)
}
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
tests := []struct {
name string
beacon *clientpb.Beacon
hasIntegrity bool
wantProcess string
}{
{
name: "windows path",
beacon: testWindowsBeacon(),
hasIntegrity: true,
wantProcess: "winterfell.exe (2020)",
},
{
name: "posix path",
beacon: testLinuxBeacon(),
hasIntegrity: false,
wantProcess: "winterfell-agent (2020)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
beacons := []*clientpb.Beacon{tt.beacon}
fullLayout := targettable.Layout{
IncludeIntegrity: tt.hasIntegrity,
IncludeLocale: true,
}
basenameLayout := fullLayout
basenameLayout.ProcessBasename = true
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
if basenameWidth >= fullWidth {
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
if !strings.Contains(rendered, tt.wantProcess) {
t.Fatalf("process basename missing from table:\n%s", rendered)
}
if strings.Contains(rendered, tt.beacon.Filename) {
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, basenameWidth)
})
}
}
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
withAll := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
withoutIntegrity := withAll
withoutIntegrity.IncludeIntegrity = false
withoutLocale := withoutIntegrity
withoutLocale.IncludeLocale = false
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
}
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
}
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
beacon := testLinuxBeacon()
beacons := []*clientpb.Beacon{beacon}
layout := targettable.Layout{IncludeLocale: true}
width := beaconCandidateWidth(t, beacons, layout, false)
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
assertHeaderPresence(t, rendered, "Integrity", false)
assertHeaderPresence(t, rendered, "Locale", true)
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, width)
}
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
basenameLayout := targettable.Layout{
ProcessBasename: true,
IncludeIntegrity: true,
IncludeLocale: true,
}
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
withoutLocale := basenameLayout
withoutLocale.IncludeIntegrity = false
withoutLocale.IncludeLocale = false
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
t.Run("full process path survives basename projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
}
if strings.Contains(rendered, `AppData\Local\Temp`) {
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
}
})
t.Run("hidden integrity survives column projection", func(t *testing.T) {
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
if !strings.Contains(rendered, beacon.Name) {
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
}
assertHeaderPresence(t, rendered, "Integrity", false)
})
}
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
beacon := testWindowsBeacon()
beacon.Integrity = ""
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
if beacon.Integrity != "" {
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
}
}
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
beacon := testWindowsBeacon()
beacons := []*clientpb.Beacon{beacon}
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
if compactWidth >= withoutOptionalWidth {
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
}
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
assertHeaderPresence(t, rendered, omitted, false)
}
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
}
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
}
assertBeaconTableFits(t, rendered, compactWidth)
}
func testWindowsBeacon() *clientpb.Beacon {
return &clientpb.Beacon{
ID: "0c9f6b27-1111-2222-3333-444444444444",
Name: "winterfell-session",
TasksCount: 3,
TasksCountCompleted: 2,
Transport: "mtls",
RemoteAddress: "10.13.37.11:49760",
Hostname: "winterfell",
Username: `winterfell\vagrant`,
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
PID: 2020,
Integrity: "High",
OS: "windows",
Arch: "amd64",
Locale: "en-US",
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
}
}
func testLinuxBeacon() *clientpb.Beacon {
beacon := testWindowsBeacon()
beacon.OS = "linux"
beacon.Hostname = "winterfell-linux"
beacon.Username = "vagrant"
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
beacon.Integrity = ""
return beacon
}
func testBeaconConsole() *console.SliverClient {
return &console.SliverClient{
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
}
}
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
t.Helper()
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
return text.LongestLineLen(tw.Render())
}
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
t.Helper()
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
return text.StripEscape(tw.Render())
}
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
t.Helper()
if got := text.LongestLineLen(rendered); got > width {
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
}
}
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
t.Helper()
headerLine := strings.SplitN(rendered, "\n", 2)[0]
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
}
}
+71 -16
View File
@@ -28,11 +28,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
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")
@@ -40,7 +41,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
})
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",
@@ -81,22 +84,74 @@ func Commands(con *console.SliverClient) []*cobra.Command {
// BeaconIDCompleter completes beacon IDs
func BeaconIDCompleter(con *console.SliverClient) carapace.Action {
callback := func(_ carapace.Context) carapace.Action {
results := make([]string, 0)
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
if err == nil {
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])
results = append(results, desc)
}
}
return carapace.ActionValuesDescribed(results...).Tag("beacons")
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
}
+2 -7
View File
@@ -26,9 +26,8 @@ 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"
)
@@ -83,12 +82,8 @@ 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 := ""
survey.AskOne(prompt, &selected)
_ = forms.Select("Select a beacon:", options, &selected)
if selected == "" {
return nil, ErrNoSelection
}
+2 -3
View File
@@ -21,10 +21,10 @@ 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"
)
@@ -71,8 +71,7 @@ func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
}
con.Println()
confirm := false
prompt := &survey.Confirm{Message: "Prune these beacons?"}
survey.AskOne(prompt, &confirm)
_ = forms.Confirm("Prune these beacons?", &confirm)
if !confirm {
return
}
+10
View File
@@ -0,0 +1,10 @@
# 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.
+10
View File
@@ -0,0 +1,10 @@
# 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.
+24 -11
View File
@@ -21,27 +21,32 @@ 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) {
profileName, _ := cmd.Flags().GetString("name")
@@ -53,7 +58,11 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
return
}
if len(httpC2Profiles.Configs) != 1 {
profileName = selectC2Profile(httpC2Profiles.Configs)
profileName, err = selectC2Profile(httpC2Profiles.Configs)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
}
}
@@ -124,7 +133,11 @@ func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
return
}
if len(httpC2Profiles.Configs) != 1 {
profileName = selectC2Profile(httpC2Profiles.Configs)
profileName, err = selectC2Profile(httpC2Profiles.Configs)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
}
}
@@ -540,20 +553,20 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
con.Println("\n")
}
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) (string, error) {
c2profile := ""
var choices []string
for _, c2profile := range c2profiles {
choices = append(choices, c2profile.Name)
}
prompt := &survey.Select{
Message: "Select a c2 profile",
Options: choices,
}
survey.AskOne(prompt, &c2profile, nil)
_ = forms.Select("Select a c2 profile", choices, &c2profile)
return c2profile
if c2profile == "" {
return "", ErrNoSelection
}
return c2profile, nil
}
func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
+4
View File
@@ -1,6 +1,7 @@
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"
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
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,
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
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)
})
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
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)
+12
View File
@@ -0,0 +1,12 @@
# 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.
@@ -0,0 +1,89 @@
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())
}
+17 -18
View File
@@ -2,7 +2,6 @@ package certificates
import (
"context"
"fmt"
"time"
"github.com/bishopfox/sliver/client/command/settings"
@@ -95,22 +94,22 @@ func CertificateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []st
printCertificateInfo(con, certificateInfo.Info)
}
func checkCertExpiry(expiryTime time.Time) string {
func checkCertExpiry(expiryTime time.Time) console.TextStyle {
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
return console.Bold + console.Red
return console.StyleBoldRed
}
// One week is 168 hours - this is bad
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
return console.Bold + console.Red
return console.StyleBoldRed
}
// One month is approximately 730 hours - this is a warning
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
return console.Bold + console.Orange
return console.StyleBoldOrange
}
return console.Normal
return console.StyleNormal
}
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
@@ -148,28 +147,28 @@ func printCertificateInfo(con *console.SliverClient, certData []*clientpb.Certif
}
for _, cert := range certData {
rowColor := console.Normal
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 {
rowColor = checkCertExpiry(expiry)
rowStyle = checkCertExpiry(expiry)
}
if wideTermWidth {
tw.AppendRow(table.Row{
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CreationTime),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.Type),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.KeyAlgorithm),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityStart),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
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{
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
rowStyle.Render(cert.ID),
rowStyle.Render(cert.CN),
rowStyle.Render(cert.ValidityExpiry),
})
}
+14
View File
@@ -23,6 +23,7 @@ import (
"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"
)
@@ -44,6 +45,19 @@ func Commands(con *console.SliverClient) []*cobra.Command {
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,
@@ -0,0 +1,96 @@
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
@@ -0,0 +1,9 @@
# 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.
+2 -3
View File
@@ -21,10 +21,10 @@ package clean
import (
"context"
"github.com/AlecAivazis/survey/v2"
"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"
@@ -35,8 +35,7 @@ import (
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
prompt := &survey.Confirm{Message: "Are you sure you want to destroy everything?"}
survey.AskOne(prompt, &confirm)
_ = forms.Confirm("Are you sure you want to destroy everything?", &confirm)
if !confirm {
return
}
+8 -3
View File
@@ -1,4 +1,9 @@
Completers
===========
# client/command/completers
This package contains general purpose completers for the console, note that some package also contain their own package-specific completers.
## Overview
Implements the 'completers' command group for the Sliver client console.
## Go Files
- `completers.go` – Provides reusable carapace completion helpers for selecting client interfaces and proxy endpoints.
+58
View File
@@ -22,6 +22,7 @@ import (
"net"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
)
// ClientInterfacesCompleter completes interface addresses on the client host.
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
}).Invoke(c).Prefix(prefix).ToA()
})
}
// LocalFilePathCompleter completes local file and directory paths.
func LocalFilePathCompleter() carapace.Action {
return carapace.ActionFiles().Tag("local file path")
}
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
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, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
})
}
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
for _, name := range names {
RegisterLocalFilePathFlagCompletion(cmd, name)
}
}
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
if cmd == nil || index < 0 {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == index {
return nil, cobra.ShellCompDirectiveDefault
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
if cmd == nil {
return
}
if cmd.ValidArgsFunction != nil {
return
}
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
}
+17
View File
@@ -0,0 +1,17 @@
# client/command/crack
## Overview
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as cached benchmark inspection, durable jobs, real-time monitoring, and crack files.
## Go Files
- `commands.go` – Defines the crack command hierarchy, attaching operations for stations and file management.
- `crack-benchmarks.go` – Displays bounded cached crackstation benchmark summaries, freshness metadata, and representative or complete hash-mode rates.
- `crack-files.go` – Manages cracking asset uploads, listings, and removals, handling streaming transfers and chunking.
- `crack-jobs.go` – Lists durable cracking jobs, renders detailed task progress, manages pause/resume/cancel lifecycle actions, and removes terminal jobs after confirmation.
- `top.go` – Preserves the crack command's compatibility entry point and delegates to the dedicated monitor package.
- `top/` – Owns the full-screen Bubble Tea monitor, its lean real-time snapshot loader, live telemetry, history, charts, actions, and tests.
- `internal/hashcatdisplay/` – Shares Hashcat mode names and rate formatting between the command handlers and monitor without coupling the packages.
- `crack.go` – Implements station status reporting, health checks, and list rendering for distributed cracking nodes.
- `helpers.go` – Supplies completion helpers and byte formatting utilities for crack subcommands.
+123
View File
@@ -1,6 +1,9 @@
package crack
import (
"time"
"github.com/bishopfox/sliver/client/command/completers"
"github.com/rsteube/carapace"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -25,6 +28,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", true, crackCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("", false, crackCmd, func(f *pflag.FlagSet) {
bindCrackFlags(f)
})
flags.BindFlagCompletions(crackCmd, func(comp *carapace.ActionMap) {
(*comp)["input"] = CrackWordlistCompleter(con)
(*comp)["rules-file"] = CrackRulesCompleter(con)
(*comp)["markov-hcstat2"] = CrackHcstat2Completer(con)
})
crackStationsCmd := &cobra.Command{
Use: consts.StationsStr,
@@ -34,8 +45,117 @@ func Commands(con *console.SliverClient) []*cobra.Command {
CrackStationsCmd(cmd, con, args)
},
}
flags.Bind("", false, crackStationsCmd, func(f *pflag.FlagSet) {
f.Bool("show-benchmarks", false, "show benchmark rates for crackstations")
})
crackCmd.AddCommand(crackStationsCmd)
crackBenchmarksCmd := &cobra.Command{
Use: consts.BenchmarksStr,
Short: "Show cached crackstation benchmark results",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
BenchmarksCmd(cmd, con, args)
},
}
flags.Bind("", false, crackBenchmarksCmd, func(f *pflag.FlagSet) {
f.BoolP("all", "a", false, "show every cached hash mode")
})
crackCmd.AddCommand(crackBenchmarksCmd)
crackJobsCmd := &cobra.Command{
Use: "jobs",
Short: "List hash cracking jobs",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
CrackJobsCmd(cmd, con, args)
},
}
flags.Bind("", true, crackJobsCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
crackCmd.AddCommand(crackJobsCmd)
crackTopCmd := &cobra.Command{
Use: "top",
Short: "Monitor crack jobs and crackstations in real time",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
CrackTopCmd(cmd, con, args)
},
}
flags.Bind("", false, crackTopCmd, func(f *pflag.FlagSet) {
f.Duration("poll-interval", time.Second, "crack job and crackstation refresh interval")
})
crackCmd.AddCommand(crackTopCmd)
crackJobCmd := &cobra.Command{
Use: "job [id]",
Short: "Show a hash cracking job",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CrackJobCmd(cmd, con, args)
},
}
flags.Bind("", true, crackJobCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("", false, crackJobCmd, func(f *pflag.FlagSet) {
f.BoolP("watch", "w", false, "watch until the job reaches a terminal state")
f.Duration("poll-interval", time.Second, "job status polling interval")
})
carapace.Gen(crackJobCmd).PositionalCompletion(CrackJobIDCompleter(con).Usage("crack job ID (leave empty to select)"))
registerCrackJobIDCompletion(crackJobCmd, con)
crackJobCancelCmd := &cobra.Command{
Use: consts.CancelStr + " [id]",
Short: "Cancel an active or paused hash cracking job",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CrackJobCancelCmd(cmd, con, args)
},
}
carapace.Gen(crackJobCancelCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobCancellable).Usage("active or paused crack job ID (leave empty to select)"))
registerCrackJobIDCompletionFiltered(crackJobCancelCmd, con, crackJobCancellable)
crackJobCmd.AddCommand(crackJobCancelCmd)
crackJobPauseCmd := &cobra.Command{
Use: "pause [id]",
Short: "Pause an active hash cracking job",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CrackJobPauseCmd(cmd, con, args)
},
}
carapace.Gen(crackJobPauseCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobPausable).Usage("active crack job ID (leave empty to select)"))
registerCrackJobIDCompletionFiltered(crackJobPauseCmd, con, crackJobPausable)
crackJobCmd.AddCommand(crackJobPauseCmd)
crackJobResumeCmd := &cobra.Command{
Use: "resume [id]",
Short: "Resume a paused hash cracking job",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CrackJobResumeCmd(cmd, con, args)
},
}
carapace.Gen(crackJobResumeCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobResumable).Usage("paused crack job ID (leave empty to select)"))
registerCrackJobIDCompletionFiltered(crackJobResumeCmd, con, crackJobResumable)
crackJobCmd.AddCommand(crackJobResumeCmd)
crackJobRmCmd := &cobra.Command{
Use: consts.RmStr + " [id]",
Short: "Remove a completed, failed, or cancelled hash cracking job",
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CrackJobRmCmd(cmd, con, args)
},
}
carapace.Gen(crackJobRmCmd).PositionalCompletion(crackTerminalJobIDCompleter(con).Usage("terminal crack job ID (leave empty to select)"))
registerCrackTerminalJobIDCompletion(crackJobRmCmd, con)
crackJobCmd.AddCommand(crackJobRmCmd)
crackCmd.AddCommand(crackJobCmd)
wordlistsCmd := &cobra.Command{
Use: consts.WordlistsStr,
Short: "Manage wordlists",
@@ -57,6 +177,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "wordlist name (blank = filename)")
})
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
wordlistsCmd.AddCommand(wordlistsAddCmd)
wordlistsRmCmd := &cobra.Command{
@@ -91,6 +212,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "rules name (blank = filename)")
})
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
rulesCmd.AddCommand(rulesAddCmd)
rulesRmCmd := &cobra.Command{
@@ -126,6 +248,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
})
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
hcstat2Cmd.AddCommand(hcstat2AddCmd)
hcstat2RmCmd := &cobra.Command{
+216
View File
@@ -0,0 +1,216 @@
package crack
import (
"fmt"
"strconv"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/jedib0t/go-pretty/v6/table"
)
type backendDeviceDetails struct {
deviceType string
vendor string
vendorID int32
version string
runtimeVersion string
deviceID uint32
deviceAlias *uint32
preferredThreadSize *uint32
memoryUnified *bool
localMemory string
cacheSize string
memoryAllocPerBlock string
pciAddress string
}
func hasReportedBackendDevice(cracker *clientpb.Crackstation) bool {
for _, device := range cracker.CUDA {
if device != nil {
return true
}
}
for _, device := range cracker.HIP {
if device != nil {
return true
}
}
for _, device := range cracker.Metal {
if device != nil {
return true
}
}
for _, device := range cracker.OpenCL {
if device != nil {
return true
}
}
return false
}
func appendCrackstationDetailRows(tw table.Writer, cracker *clientpb.Crackstation) {
if cracker.HostUUID != "" {
tw.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), safeCrackCell(cracker.HostUUID)})
}
if cracker.Version != "" {
tw.AppendRow(table.Row{console.StyleBold.Render("Crackstation Version"), safeCrackCell(cracker.Version)})
}
}
func appendCUDABackendRows(tw table.Writer, device *clientpb.CUDABackendInfo, level uint32) {
if device == nil {
return
}
appendBackendSummaryRows(tw, "CUDA Device", formatCUDADevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
if level < 2 {
return
}
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
deviceType: device.Type,
vendor: device.Vendor,
vendorID: device.VendorID,
version: device.Version,
runtimeVersion: device.CUDAVersion,
deviceID: device.BackendDeviceID,
deviceAlias: device.BackendDeviceAlias,
preferredThreadSize: device.PreferredThreadSize,
memoryUnified: device.MemoryUnified,
localMemory: device.LocalMemory,
cacheSize: device.CacheSize,
pciAddress: device.PCIAddress,
})
}
func appendHIPBackendRows(tw table.Writer, device *clientpb.HIPBackendInfo, level uint32) {
if device == nil {
return
}
appendBackendSummaryRows(tw, "HIP Device", formatHIPDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
if level < 2 {
return
}
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
deviceType: device.Type,
vendor: device.Vendor,
vendorID: device.VendorID,
version: device.Version,
runtimeVersion: device.HIPVersion,
deviceID: device.BackendDeviceID,
deviceAlias: device.BackendDeviceAlias,
preferredThreadSize: device.PreferredThreadSize,
memoryUnified: device.MemoryUnified,
localMemory: device.LocalMemory,
cacheSize: device.CacheSize,
pciAddress: device.PCIAddress,
})
}
func appendMetalBackendRows(tw table.Writer, device *clientpb.MetalBackendInfo, level uint32) {
if device == nil {
return
}
appendBackendSummaryRows(tw, "Metal Device", formatMetalDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
if level < 2 {
return
}
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
deviceType: device.Type,
vendor: device.Vendor,
vendorID: device.VendorID,
version: device.Version,
runtimeVersion: device.MetalVersion,
deviceID: device.BackendDeviceID,
deviceAlias: device.BackendDeviceAlias,
preferredThreadSize: device.PreferredThreadSize,
memoryUnified: device.MemoryUnified,
localMemory: device.LocalMemory,
cacheSize: device.CacheSize,
memoryAllocPerBlock: device.MemoryAllocPerBlock,
})
appendStringBackendDetailRow(tw, "Physical Location", device.PhysicalLocation)
appendUint32BackendDetailRow(tw, "Registry ID", device.RegistryID)
appendStringBackendDetailRow(tw, "Max TX Rate", device.MaxTXRate)
appendBoolBackendDetailRow(tw, "Headless", device.GPUHeadless)
appendBoolBackendDetailRow(tw, "Low Power", device.GPULowPower)
appendBoolBackendDetailRow(tw, "Removable", device.GPURemovable)
}
func appendOpenCLBackendRows(tw table.Writer, device *clientpb.OpenCLBackendInfo, level uint32) {
if device == nil {
return
}
appendBackendSummaryRows(tw, "OpenCL Device", formatOpenCLDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
if level < 2 {
return
}
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
deviceType: device.Type,
vendor: device.Vendor,
vendorID: device.VendorID,
version: device.Version,
runtimeVersion: device.OpenCLVersion,
deviceID: device.BackendDeviceID,
deviceAlias: device.BackendDeviceAlias,
preferredThreadSize: device.PreferredThreadSize,
memoryUnified: device.MemoryUnified,
localMemory: device.LocalMemory,
memoryAllocPerBlock: device.MemoryAllocPerBlock,
pciAddress: device.PCIAddress,
})
appendStringBackendDetailRow(tw, "OpenCL Driver", device.OpenCLDriverVersion)
if device.PlatformID != 0 || device.PlatformVendor != "" || device.PlatformName != "" || device.PlatformVersion != "" {
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Platform ID"), strconv.FormatUint(uint64(device.PlatformID), 10)})
}
appendStringBackendDetailRow(tw, "OpenCL Platform Vendor", device.PlatformVendor)
appendStringBackendDetailRow(tw, "OpenCL Platform", device.PlatformName)
appendStringBackendDetailRow(tw, "OpenCL Platform Version", device.PlatformVersion)
}
func appendBackendSummaryRows(tw table.Writer, label, name, memoryFree, memoryTotal string, clock, processors int32) {
tw.AppendSeparator()
tw.AppendRow(table.Row{console.StyleBold.Render(label), console.StyleBoldGreen.Render(safeCrackCell(name))})
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", valueOrDash(memoryFree), valueOrDash(memoryTotal))})
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), strconv.FormatInt(int64(clock), 10)})
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), strconv.FormatInt(int64(processors), 10)})
}
func appendBackendDeviceDetailRows(tw table.Writer, details backendDeviceDetails) {
appendStringBackendDetailRow(tw, "Device Type", details.deviceType)
appendStringBackendDetailRow(tw, "Vendor", details.vendor)
if details.vendorID != 0 {
tw.AppendRow(table.Row{console.StyleBold.Render("Vendor ID"), strconv.FormatInt(int64(details.vendorID), 10)})
}
if details.version != "" && details.version != details.runtimeVersion {
appendStringBackendDetailRow(tw, "Device Version", details.version)
}
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Device ID"), strconv.FormatUint(uint64(details.deviceID), 10)})
appendUint32BackendDetailRow(tw, "Backend Device Alias", details.deviceAlias)
appendUint32BackendDetailRow(tw, "Preferred Thread Size", details.preferredThreadSize)
appendBoolBackendDetailRow(tw, "Unified Memory", details.memoryUnified)
appendStringBackendDetailRow(tw, "Local Memory", details.localMemory)
appendStringBackendDetailRow(tw, "Cache Size", details.cacheSize)
appendStringBackendDetailRow(tw, "Memory Allocation per Block", details.memoryAllocPerBlock)
appendStringBackendDetailRow(tw, "PCI Address", details.pciAddress)
}
func appendStringBackendDetailRow(tw table.Writer, label, value string) {
if value == "" {
return
}
tw.AppendRow(table.Row{console.StyleBold.Render(label), safeCrackCell(value)})
}
func appendUint32BackendDetailRow(tw table.Writer, label string, value *uint32) {
if value == nil {
return
}
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatUint(uint64(*value), 10)})
}
func appendBoolBackendDetailRow(tw table.Writer, label string, value *bool) {
if value == nil {
return
}
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatBool(*value)})
}
+210
View File
@@ -0,0 +1,210 @@
package crack
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/charmbracelet/x/ansi"
"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"
)
var representativeCrackBenchmarkModes = map[int32]struct{}{
0: {}, // MD5
100: {}, // SHA1
1000: {}, // NTLM
1400: {}, // SHA2-256
3200: {}, // bcrypt
22000: {}, // WPA-PBKDF2
}
const (
crackBenchmarkOutputWidth = 80
crackBenchmarkHashTypeWidth = 48
crackBenchmarkMetadataValueWidth = 48
)
// BenchmarksCmd displays benchmark results persisted by the server.
func BenchmarksCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
defer cancel()
response, err := con.Rpc.CrackstationBenchmarks(ctx, &commonpb.Empty{})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
showAll, _ := cmd.Flags().GetBool("all")
benchmarks := response.GetSnapshots()
rendered := renderCrackBenchmarks(benchmarks, settings.GetTableStyle(con), showAll)
if rendered == "" {
con.PrintInfof("No cached crackstation benchmarks\n")
return
}
con.Printf("%s\n", rendered)
}
func renderCrackBenchmarks(benchmarks []*clientpb.CrackBenchmarkSnapshot, style table.Style, showAll bool) string {
ordered := make([]*clientpb.CrackBenchmarkSnapshot, 0, len(benchmarks))
for _, benchmark := range benchmarks {
if benchmark != nil {
ordered = append(ordered, benchmark)
}
}
if len(ordered) == 0 {
return ""
}
sort.SliceStable(ordered, func(i, j int) bool {
leftName := safeCrackCell(ordered[i].GetName())
rightName := safeCrackCell(ordered[j].GetName())
if leftName == rightName {
return safeCrackCell(ordered[i].GetHostUUID()) < safeCrackCell(ordered[j].GetHostUUID())
}
return leftName < rightName
})
sections := make([]string, 0, len(ordered)*2+1)
for index, benchmark := range ordered {
identity := cachedBenchmarkIdentity(benchmark)
title := renderCachedBenchmarkTitle(index, identity)
metadata := table.NewWriter()
metadata.SetStyle(boundedCrackBenchmarkStyle(style))
metadata.SetColumnConfigs([]table.ColumnConfig{
{Number: 2, WidthMax: crackBenchmarkMetadataValueWidth},
})
metadata.AppendRow(table.Row{console.StyleBold.Render("Name"), valueOrDash(benchmark.GetName())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), console.StyleBoldPrimary.Render(valueOrDash(benchmark.GetHostUUID()))})
metadata.AppendRow(table.Row{console.StyleBold.Render("Operator"), valueOrDash(benchmark.GetOperatorName())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Connection"), renderCachedBenchmarkConnection(benchmark.GetOnline())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Cache"), renderCachedBenchmarkFreshness(benchmark.GetFresh())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Current Hashcat Version"), valueOrDash(benchmark.GetCurrentHashcatVersion())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Hashcat Version"), valueOrDash(benchmark.GetBenchmarkHashcatVersion())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Schema"), strconv.FormatUint(uint64(benchmark.GetBenchmarkSchemaVersion()), 10)})
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmarked At"), formatCachedBenchmarkTime(benchmark.GetBenchmarkedAt())})
metadata.AppendRow(table.Row{console.StyleBold.Render("Cached Modes"), strconv.Itoa(len(benchmark.GetBenchmarks()))})
sections = append(sections, title+"\n"+metadata.Render())
rates := table.NewWriter()
rates.SetStyle(boundedCrackBenchmarkStyle(style))
rates.SetColumnConfigs([]table.ColumnConfig{
{Name: "Hash Type", WidthMax: crackBenchmarkHashTypeWidth},
})
if showAll {
rates.SetTitle(console.StyleBold.Render("All Cached Rates"))
} else {
rates.SetTitle(console.StyleBold.Render("Representative Rates"))
}
rates.AppendHeader(table.Row{"Mode", "Hash Type", "Rate"})
modes := cachedBenchmarkModes(benchmark.GetBenchmarks(), showAll)
if len(modes) == 0 {
rates.AppendRow(table.Row{"-", "No benchmark rates cached", "-"})
} else {
for _, mode := range modes {
rates.AppendRow(table.Row{
strconv.FormatInt(int64(mode), 10),
crackBenchmarkModeName(mode),
humanizeHashRate(benchmark.GetBenchmarks()[mode]),
})
}
}
sections = append(sections, rates.Render())
}
if !showAll {
sections = append(sections, console.StyleGray.Render("Showing representative rates; use --all for every cached mode."))
}
return strings.Join(sections, "\n\n")
}
func boundedCrackBenchmarkStyle(style table.Style) table.Style {
style.Size.WidthMax = crackBenchmarkOutputWidth
return style
}
func renderCachedBenchmarkTitle(index int, identity string) string {
prefix := fmt.Sprintf(">>> Cached Crackstation %02d - ", index+1)
maximumIdentityWidth := crackBenchmarkOutputWidth - ansi.StringWidth(prefix)
if maximumIdentityWidth < 1 {
return console.StyleBoldPrimary.Render(ansi.Truncate(prefix, crackBenchmarkOutputWidth, "…"))
}
identity = ansi.Truncate(identity, maximumIdentityWidth, "…")
return console.StyleBoldPrimary.Render(prefix + identity)
}
func cachedBenchmarkIdentity(benchmark *clientpb.CrackBenchmarkSnapshot) string {
if benchmark == nil {
return "Unknown crackstation"
}
if name := safeCrackCell(benchmark.GetName()); name != "" {
return name
}
if hostUUID := safeCrackCell(benchmark.GetHostUUID()); hostUUID != "" {
return hostUUID
}
return "Unknown crackstation"
}
func renderCachedBenchmarkConnection(online bool) string {
if online {
return console.StyleBoldSuccess.Render("Online")
}
return console.StyleBoldGray.Render("Offline")
}
func renderCachedBenchmarkFreshness(fresh bool) string {
if fresh {
return console.StyleBoldSuccess.Render("Fresh")
}
return console.StyleBoldWarning.Render("Stale")
}
func formatCachedBenchmarkTime(unix int64) string {
if unix == 0 {
return "-"
}
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
}
func cachedBenchmarkModes(benchmarks map[int32]uint64, showAll bool) []int32 {
modes := make([]int32, 0, len(benchmarks))
for mode := range benchmarks {
if showAll {
modes = append(modes, mode)
continue
}
if _, representative := representativeCrackBenchmarkModes[mode]; representative {
modes = append(modes, mode)
}
}
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
if showAll || len(modes) != 0 {
return modes
}
for mode := range benchmarks {
modes = append(modes, mode)
}
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
if len(modes) > 6 {
modes = modes[:6]
}
return modes
}
func crackBenchmarkModeName(mode int32) string {
if name, ok := hashcatHashTypeName(mode); ok {
return name
}
if enumName := clientpb.HashType(mode).String(); enumName != strconv.FormatInt(int64(mode), 10) {
return enumName
}
return "Unknown hash mode"
}
@@ -0,0 +1,376 @@
package crack
import (
"context"
"regexp"
"strconv"
"strings"
"testing"
"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/bishopfox/sliver/protobuf/rpcpb"
"github.com/charmbracelet/x/ansi"
"github.com/spf13/cobra"
"google.golang.org/grpc"
)
func TestCrackBenchmarksCommandIsRegistered(t *testing.T) {
root := Commands(nil)[0]
benchmarks, _, err := root.Find([]string{"benchmarks"})
if err != nil || benchmarks == root {
t.Fatalf("find crack benchmarks: command=%v error=%v", benchmarks, err)
}
if benchmarks.Use != "benchmarks" {
t.Fatalf("crack benchmarks use = %q, want benchmarks", benchmarks.Use)
}
if err := benchmarks.Args(benchmarks, nil); err != nil {
t.Fatalf("crack benchmarks rejected no arguments: %v", err)
}
if err := benchmarks.Args(benchmarks, []string{"unexpected"}); err == nil {
t.Fatal("crack benchmarks accepted a positional argument")
}
allFlag := benchmarks.Flags().Lookup("all")
if allFlag == nil {
t.Fatal("crack benchmarks is missing --all")
}
if allFlag.Shorthand != "a" {
t.Fatalf("--all shorthand = %q, want %q", allFlag.Shorthand, "a")
}
if benchmarks.PersistentFlags().Lookup("all") != nil || root.PersistentFlags().Lookup("all") != nil {
t.Fatal("--all must remain local to crack benchmarks")
}
if benchmarks.InheritedFlags().Lookup("timeout") == nil {
t.Fatal("crack benchmarks must inherit --timeout")
}
}
func TestRenderCrackBenchmarksSortsStationsAndRepresentativeModes(t *testing.T) {
benchmarks := []*clientpb.CrackBenchmarkSnapshot{
nil,
{
Name: "zeta",
HostUUID: "host-z",
OperatorName: "operator-z",
CurrentHashcatVersion: "v7.1.3",
BenchmarkHashcatVersion: "v7.1.2",
BenchmarkSchemaVersion: 1,
BenchmarkedAt: 1_700_000_000,
Online: true,
Fresh: true,
Benchmarks: map[int32]uint64{
22000: 6_000_000_000_000,
0: 999,
3200: 5_000_000_000,
100: 1_000,
1400: 3_000_000,
1000: 2_000_000,
99999: 7_000_000,
},
},
{Name: "alpha", HostUUID: "host-b", Benchmarks: map[int32]uint64{0: 1}},
{Name: "alpha", HostUUID: "host-a", Benchmarks: map[int32]uint64{0: 2}},
}
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
output := ansi.Strip(raw)
requireOrderedCrackBenchmarkText(t, output, "host-a", "host-b", "host-z")
zetaStart := strings.Index(output, ">>> Cached Crackstation 03 - zeta")
if zetaStart < 0 {
t.Fatalf("rendered benchmarks are missing sorted zeta section:\n%s", output)
}
requireOrderedCrackBenchmarkText(t, output[zetaStart:],
"MD5", "SHA1", "NTLM", "SHA2-256", "bcrypt $2\\*$, Blowfish (Unix)", "WPA-PBKDF2-PMKID+EAPOL",
)
for _, expected := range []string{
"Name", "Host UUID", "Operator", "Connection", "Online", "Cache", "Fresh",
"Current Hashcat Version", "v7.1.3", "Benchmark Hashcat Version", "v7.1.2",
"Benchmark Schema", "Benchmarked At", "2023-11-14T22:13:20Z", "Cached Modes",
"operator-z", "Representative Rates", "Showing representative rates; use --all for every cached mode.",
} {
if !strings.Contains(output, expected) {
t.Errorf("rendered benchmarks do not contain %q:\n%s", expected, output)
}
}
requireCrackBenchmarkRow(t, output, 0, "MD5", "999 H/s")
requireCrackBenchmarkRow(t, output, 100, "SHA1", "1.00 kH/s")
requireCrackBenchmarkRow(t, output, 1000, "NTLM", "2.00 MH/s")
requireCrackBenchmarkRow(t, output, 1400, "SHA2-256", "3.00 MH/s")
requireCrackBenchmarkRow(t, output, 3200, "bcrypt $2\\*$, Blowfish (Unix)", "5.00 GH/s")
requireCrackBenchmarkRow(t, output, 22000, "WPA-PBKDF2-PMKID+EAPOL", "6.00 TH/s")
if hasCrackBenchmarkRow(output, 99999, "Plaintext", "7.00 MH/s") {
t.Fatalf("default benchmark view unexpectedly contains non-representative mode 99999:\n%s", output)
}
for _, styled := range []string{
console.StyleBoldPrimary.Render(">>> Cached Crackstation 01 - alpha"),
console.StyleBoldPrimary.Render("host-a"),
console.StyleBoldSuccess.Render("Online"),
console.StyleBoldSuccess.Render("Fresh"),
console.StyleBold.Render("Representative Rates"),
console.StyleGray.Render("Showing representative rates; use --all for every cached mode."),
} {
if !strings.Contains(raw, styled) {
t.Errorf("rendered benchmarks are missing shared Lip Gloss styling %q:\n%s", styled, raw)
}
}
}
func TestRenderCrackBenchmarksFallsBackToFirstSixModes(t *testing.T) {
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
Name: "fallback",
Benchmarks: map[int32]uint64{
23: 23,
10: 10,
22: 22,
12: 12,
20: 20,
11: 11,
21: 21,
},
}}
output := ansi.Strip(renderCrackBenchmarks(benchmarks, settings.SliverDefault, false))
requireOrderedCrackBenchmarkText(t, output,
"md5($pass.$salt)",
"Joomla < 2.5.18",
"PostgreSQL",
"md5($salt.$pass)",
"osCommerce, xt:Commerce",
"Juniper NetScreen/SSG (ScreenOS)",
)
if hasCrackBenchmarkRow(output, 23, "Skype", "23 H/s") {
t.Fatalf("fallback benchmark view contains more than six modes:\n%s", output)
}
}
func TestRenderCrackBenchmarksAllShowsEveryModeAndUnknownFallback(t *testing.T) {
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
Name: "all-modes",
Benchmarks: map[int32]uint64{
424242: 1_234,
36100: 3_600,
99999: 7_000_000,
0: 500,
},
}}
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, true)
output := ansi.Strip(raw)
for _, row := range []struct {
mode int32
hashType string
rate string
}{
{mode: 0, hashType: "MD5", rate: "500 H/s"},
{mode: 36100, hashType: "YESCRYPT", rate: "3.60 kH/s"},
{mode: 99999, hashType: "Plaintext", rate: "7.00 MH/s"},
{mode: 424242, hashType: "Unknown hash mode", rate: "1.23 kH/s"},
} {
requireCrackBenchmarkRow(t, output, row.mode, row.hashType, row.rate)
}
if !strings.Contains(raw, console.StyleBold.Render("All Cached Rates")) {
t.Fatalf("all-modes view is missing styled title:\n%s", raw)
}
if strings.Contains(output, "Showing representative rates") {
t.Fatalf("all-modes view unexpectedly contains representative-rate hint:\n%s", output)
}
}
func TestRenderCrackBenchmarksSanitizesMetadata(t *testing.T) {
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
Name: "alpha\nbeta\x1b]52;c;payload\a",
HostUUID: "host\r\nuuid",
OperatorName: "op\tname",
CurrentHashcatVersion: "current-v7\x00.2",
BenchmarkHashcatVersion: "benchmark-v7\x00.1",
Benchmarks: map[int32]uint64{0: 1},
}}
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
output := ansi.Strip(raw)
for _, expected := range []string{"alphabeta]52;c;payload", "hostuuid", "opname", "current-v7.2", "benchmark-v7.1"} {
if !strings.Contains(output, expected) {
t.Errorf("sanitized benchmark metadata does not contain %q:\n%s", expected, output)
}
}
for _, control := range []string{"\x1b]52;", "\a", "\r", "\x00", "\t"} {
if strings.Contains(raw, control) {
t.Errorf("rendered benchmark metadata retained control text %q:\n%q", control, raw)
}
}
}
func TestRenderCrackBenchmarksStylesConnectionAndFreshness(t *testing.T) {
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{
{Name: "fresh", Online: true, Fresh: true, Benchmarks: map[int32]uint64{0: 1}},
{Name: "stale", Online: false, Fresh: false, Benchmarks: map[int32]uint64{0: 1}},
}, settings.SliverDefault, false)
for _, styled := range []string{
console.StyleBoldSuccess.Render("Online"),
console.StyleBoldSuccess.Render("Fresh"),
console.StyleBoldGray.Render("Offline"),
console.StyleBoldWarning.Render("Stale"),
} {
if !strings.Contains(raw, styled) {
t.Errorf("rendered benchmarks are missing status styling %q:\n%s", styled, raw)
}
}
}
func TestRenderCrackBenchmarksBoundsEveryLine(t *testing.T) {
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
Name: strings.Repeat("very-long-crackstation-name-", 8),
HostUUID: "12345678-1234-1234-1234-123456789abc",
OperatorName: strings.Repeat("operator-", 16),
CurrentHashcatVersion: strings.Repeat("current-version-", 10),
BenchmarkHashcatVersion: strings.Repeat("benchmark-version-", 10),
BenchmarkedAt: 1_700_000_000,
Benchmarks: map[int32]uint64{6211: 1_000_000},
}}, settings.SliverDefault, true)
for lineNumber, line := range strings.Split(raw, "\n") {
if width := ansi.StringWidth(line); width > crackBenchmarkOutputWidth {
t.Errorf("rendered line %d width = %d, want <= %d:\n%s", lineNumber+1, width, crackBenchmarkOutputWidth, line)
}
}
if !strings.Contains(ansi.Strip(raw), "…") {
t.Fatalf("long benchmark heading was not truncated:\n%s", raw)
}
}
func TestFormatCachedBenchmarkTime(t *testing.T) {
if got := formatCachedBenchmarkTime(0); got != "-" {
t.Fatalf("zero benchmark time = %q, want -", got)
}
if got := formatCachedBenchmarkTime(1_700_000_000); got != "2023-11-14T22:13:20Z" {
t.Fatalf("benchmark time = %q, want UTC RFC3339", got)
}
}
func TestRenderCrackBenchmarksHandlesNilAndEmptyData(t *testing.T) {
for _, benchmarks := range [][]*clientpb.CrackBenchmarkSnapshot{nil, {nil, nil}} {
if got := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false); got != "" {
t.Fatalf("renderCrackBenchmarks(%#v) = %q, want empty output", benchmarks, got)
}
}
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
HostUUID: "host-only",
}}, settings.SliverDefault, false)
output := ansi.Strip(raw)
for _, expected := range []string{
">>> Cached Crackstation 01 - host-only",
"No benchmark rates cached",
"Representative Rates",
} {
if !strings.Contains(output, expected) {
t.Errorf("empty benchmark snapshot does not contain %q:\n%s", expected, output)
}
}
unknown := ansi.Strip(renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{}}, settings.SliverDefault, false))
if !strings.Contains(unknown, ">>> Cached Crackstation 01 - Unknown crackstation") {
t.Fatalf("missing unknown-station identity fallback:\n%s", unknown)
}
}
type crackBenchmarksRPCCapture struct {
rpcpb.SliverRPCClient
response *clientpb.CrackBenchmarkSnapshots
err error
benchmarkCalls int
crackstationsCalls int
hadDeadline bool
}
func (capture *crackBenchmarksRPCCapture) CrackstationBenchmarks(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackBenchmarkSnapshots, error) {
capture.benchmarkCalls++
_, capture.hadDeadline = ctx.Deadline()
return capture.response, capture.err
}
func (capture *crackBenchmarksRPCCapture) Crackstations(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
capture.crackstationsCalls++
return &clientpb.Crackstations{}, nil
}
func TestCrackBenchmarksCommandUsesDedicatedRPCWithDeadline(t *testing.T) {
capture := &crackBenchmarksRPCCapture{response: &clientpb.CrackBenchmarkSnapshots{Snapshots: []*clientpb.CrackBenchmarkSnapshot{{
Name: "station",
HostUUID: "host-id",
Benchmarks: map[int32]uint64{0: 1},
}}}}
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
t.Fatal(err)
}
root := Commands(con)[0]
root.SetArgs([]string{"benchmarks", "--timeout=1", "-a"})
root.SilenceErrors = true
root.SilenceUsage = true
if err := root.Execute(); err != nil {
t.Fatalf("execute crack benchmarks: %v", err)
}
if capture.benchmarkCalls != 1 {
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
}
if capture.crackstationsCalls != 0 {
t.Fatalf("Crackstations calls = %d, want 0", capture.crackstationsCalls)
}
if !capture.hadDeadline {
t.Fatal("CrackstationBenchmarks context did not inherit --timeout")
}
}
func TestCrackBenchmarksCommandHandlesNilResponse(t *testing.T) {
capture := &crackBenchmarksRPCCapture{}
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
t.Fatal(err)
}
root := Commands(con)[0]
root.SetArgs([]string{"benchmarks", "--timeout=1"})
root.SilenceErrors = true
root.SilenceUsage = true
if err := root.Execute(); err != nil {
t.Fatalf("execute crack benchmarks with nil response: %v", err)
}
if capture.benchmarkCalls != 1 {
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
}
}
func requireOrderedCrackBenchmarkText(t *testing.T, output string, values ...string) {
t.Helper()
remaining := output
for _, value := range values {
index := strings.Index(remaining, value)
if index < 0 {
t.Fatalf("rendered benchmarks do not contain %q in order:\n%s", value, output)
}
remaining = remaining[index+len(value):]
}
}
func requireCrackBenchmarkRow(t *testing.T, output string, mode int32, hashType string, rate string) {
t.Helper()
if !hasCrackBenchmarkRow(output, mode, hashType, rate) {
t.Fatalf("rendered benchmarks are missing mode %d, hash type %q, rate %q:\n%s", mode, hashType, rate, output)
}
}
func hasCrackBenchmarkRow(output string, mode int32, hashType string, rate string) bool {
pattern := `(?m)^[ \t]*` + regexp.QuoteMeta(strconv.FormatInt(int64(mode), 10)) + `[ \t]+` +
regexp.QuoteMeta(hashType) + `[ \t]+` + regexp.QuoteMeta(rate) + `[ \t]*$`
return regexp.MustCompile(pattern).MatchString(output)
}
+593
View File
@@ -0,0 +1,593 @@
package crack
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const maxKeyboardLayoutMappingBytes int64 = 1 << 20
func readKeyboardLayoutMapping(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open keyboard layout mapping %q: %w", path, err)
}
defer func() { _ = file.Close() }()
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("stat keyboard layout mapping %q: %w", path, err)
}
if !info.Mode().IsRegular() {
return nil, fmt.Errorf("keyboard layout mapping %q is not a regular file", path)
}
if info.Size() > maxKeyboardLayoutMappingBytes {
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
}
data, err := io.ReadAll(io.LimitReader(file, maxKeyboardLayoutMappingBytes+1))
if err != nil {
return nil, fmt.Errorf("read keyboard layout mapping %q: %w", path, err)
}
if int64(len(data)) > maxKeyboardLayoutMappingBytes {
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
}
if len(data) == 0 {
return nil, fmt.Errorf("keyboard layout mapping %q is empty", path)
}
return data, nil
}
func shouldRunCrack(cmd *cobra.Command, args []string) bool {
if len(args) > 0 {
return true
}
hasFlag := false
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
if !flag.Changed || flag.Name == "timeout" {
return
}
// An explicitly disabled standalone mode is a no-op, not a crack job.
switch flag.Name {
case "backend-info", "keyspace", "total-candidates", "identify-mode", "hash-info":
enabled, _ := cmd.Flags().GetBool(flag.Name)
if !enabled {
return
}
}
hasFlag = true
})
return hasFlag
}
func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackCommand, error) {
flags := cmd.Flags()
req := &clientpb.CrackCommand{HashType: clientpb.HashType_INVALID}
if flags.Changed("attack-mode") {
raw, _ := flags.GetString("attack-mode")
attackMode, err := parseCrackAttackMode(raw)
if err != nil {
return nil, err
}
req.AttackMode = attackMode
}
if flags.Changed("hash-type") {
raw, _ := flags.GetString("hash-type")
hashType, err := parseHashType(raw)
if err != nil {
return nil, err
}
req.HashType = hashType
}
if flags.Changed("hash-mode") {
hashMode, _ := flags.GetUint32("hash-mode")
req.HashMode = &hashMode
}
hashes, _ := flags.GetStringArray("hash")
if len(args) > 0 {
hashes = append(hashes, args...)
}
req.Hashes = hashes
req.CredentialIDs, _ = flags.GetStringArray("credential")
for index, credentialID := range req.CredentialIDs {
req.CredentialIDs[index] = strings.TrimSpace(credentialID)
if req.CredentialIDs[index] == "" {
return nil, fmt.Errorf("--credential cannot be empty")
}
}
req.CredentialCollection, _ = flags.GetString("credential-collection")
req.CredentialCollection = strings.TrimSpace(req.CredentialCollection)
req.IncludeCrackedCredentials, _ = flags.GetBool("include-cracked-credentials")
if req.IncludeCrackedCredentials && len(req.CredentialIDs) == 0 && req.CredentialCollection == "" {
return nil, fmt.Errorf("--include-cracked-credentials requires --credential or --credential-collection")
}
req.PositionalArguments, _ = flags.GetStringArray("input")
req.Crackstation, _ = flags.GetString("crackstation")
req.Crackstation = strings.TrimSpace(req.Crackstation)
if stdin, _ := flags.GetString("stdin"); stdin != "" {
req.Stdin = []byte(stdin)
}
if flags.Changed("segment-size") {
return nil, fmt.Errorf("--segment-size was removed in Hashcat v7")
}
req.Quiet, _ = flags.GetBool("quiet")
req.HexCharset, _ = flags.GetBool("hex-charset")
req.HexSalt, _ = flags.GetBool("hex-salt")
req.HexWordlist, _ = flags.GetBool("hex-wordlist")
req.Force, _ = flags.GetBool("force")
req.DeprecatedCheckDisable, _ = flags.GetBool("deprecated-check-disable")
req.AdviceDisable, _ = flags.GetBool("advice-disable")
req.Status, _ = flags.GetBool("status")
req.StatusJSON, _ = flags.GetBool("status-json")
req.StatusTimer, _ = flags.GetUint32("status-timer")
if flags.Changed("status-timer") {
value := req.StatusTimer
req.StatusTimerV7 = &value
}
req.StdinTimeoutAbort, _ = flags.GetUint32("stdin-timeout-abort")
if flags.Changed("stdin-timeout-abort") {
value := req.StdinTimeoutAbort
req.StdinTimeoutAbortV7 = &value
}
req.MachineReadable, _ = flags.GetBool("machine-readable")
req.KeepGuessing, _ = flags.GetBool("keep-guessing")
req.SelfTestDisable, _ = flags.GetBool("self-test-disable")
req.Loopback, _ = flags.GetBool("loopback")
if raw, _ := flags.GetString("markov-hcstat2"); raw != "" {
req.MarkovHcstat2 = []byte(raw)
}
req.MarkovDisable, _ = flags.GetBool("markov-disable")
req.MarkovClassic, _ = flags.GetBool("markov-classic")
req.MarkovInverse, _ = flags.GetBool("markov-inverse")
req.MarkovThreshold, _ = flags.GetUint32("markov-threshold")
req.Runtime, _ = flags.GetUint32("runtime")
req.Session, _ = flags.GetString("session")
req.Restore, _ = flags.GetBool("restore")
req.RestoreDisable, _ = flags.GetBool("restore-disable")
if raw, _ := flags.GetString("restore-file"); raw != "" {
req.RestoreFile = []byte(raw)
}
req.RestorePosition, _ = flags.GetBool("restore-position")
req.RestoreShowCommand, _ = flags.GetBool("restore-show-command")
if req.RestoreShowCommand && (req.Restore || req.RestorePosition) { //nolint:staticcheck // Restore is required for legacy command compatibility.
return nil, fmt.Errorf("--restore-show-command and restore-position modes are mutually exclusive")
}
req.Outfile, _ = flags.GetString("outfile")
if formats, _ := flags.GetStringSlice("outfile-format"); len(formats) > 0 {
for _, raw := range formats {
format, err := parseCrackOutfileFormat(raw)
if err != nil {
return nil, err
}
req.OutfileFormat = append(req.OutfileFormat, format)
}
}
req.OutfileJSON, _ = flags.GetBool("outfile-json")
req.OutfileAutohexDisable, _ = flags.GetBool("outfile-autohex-disable")
req.OutfileCheckTimer, _ = flags.GetUint32("outfile-check-timer")
if flags.Changed("outfile-check-timer") {
value := req.OutfileCheckTimer
req.OutfileCheckTimerV7 = &value
}
req.WordlistAutohexDisable, _ = flags.GetBool("wordlist-autohex-disable")
req.Separator, _ = flags.GetString("separator")
req.Show, _ = flags.GetBool("show")
req.Left, _ = flags.GetBool("left")
req.Username, _ = flags.GetBool("username")
req.Remove, _ = flags.GetBool("remove")
req.RemoveTimer, _ = flags.GetUint32("remove-timer")
req.PotfileDisable, _ = flags.GetBool("potfile-disable")
if raw, _ := flags.GetString("potfile"); raw != "" {
req.Potfile = []byte(raw)
}
if flags.Changed("encoding-from") {
raw, _ := flags.GetString("encoding-from")
if enc, err := parseCrackEncoding(raw); err == nil {
req.EncodingFrom = enc
}
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
req.EncodingFromName = raw
}
}
if flags.Changed("encoding-to") {
raw, _ := flags.GetString("encoding-to")
if enc, err := parseCrackEncoding(raw); err == nil {
req.EncodingTo = enc
}
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
req.EncodingToName = raw
}
}
req.DebugMode, _ = flags.GetUint32("debug-mode")
req.DebugFile, _ = flags.GetString("debug-file")
req.InductionDir, _ = flags.GetString("induction-dir")
req.OutfileCheckDir, _ = flags.GetString("outfile-check-dir")
req.LogfileDisable, _ = flags.GetBool("logfile-disable")
req.HccapxMessagePair, _ = flags.GetUint32("hccapx-message-pair")
if flags.Changed("hccapx-message-pair") {
value := req.HccapxMessagePair
req.HccapxMessagePairV7 = &value
}
req.NonceErrorCorrections, _ = flags.GetUint32("nonce-error-corrections")
if flags.Changed("nonce-error-corrections") {
value := req.NonceErrorCorrections
req.NonceErrorCorrectionsV7 = &value
}
if raw, _ := flags.GetString("keyboard-layout-mapping"); raw != "" {
mapping, err := readKeyboardLayoutMapping(raw)
if err != nil {
return nil, err
}
req.KeyboardLayoutMapping = mapping
}
req.TruecryptKeyfiles, _ = flags.GetString("truecrypt-keyfiles")
req.VeracryptKeyfiles, _ = flags.GetString("veracrypt-keyfiles")
if flags.Changed("veracrypt-pim-start") {
value, _ := flags.GetUint32("veracrypt-pim-start")
req.VeracryptPimStart = &value
}
if flags.Changed("veracrypt-pim-stop") {
value, _ := flags.GetUint32("veracrypt-pim-stop")
req.VeracryptPimStop = &value
}
req.Benchmark, _ = flags.GetBool("benchmark")
req.BenchmarkAll, _ = flags.GetBool("benchmark-all")
req.BenchmarkMin, _ = flags.GetUint32("benchmark-min")
if flags.Changed("benchmark-max") {
value, _ := flags.GetUint32("benchmark-max")
req.BenchmarkMax = &value
}
req.SpeedOnly, _ = flags.GetBool("speed-only")
req.ProgressOnly, _ = flags.GetBool("progress-only")
req.PipelineStats, _ = flags.GetBool("pipeline-stats")
req.TaskTimeBreakdown, _ = flags.GetBool("task-time-breakdown")
req.BitmapMin, _ = flags.GetUint32("bitmap-min")
if flags.Changed("bitmap-min") {
value := req.BitmapMin
req.BitmapMinV7 = &value
}
req.BitmapMax, _ = flags.GetUint32("bitmap-max")
if flags.Changed("bitmap-max") {
value := req.BitmapMax
req.BitmapMaxV7 = &value
}
req.CPUAffinity = uintSliceToUint32(flags, "cpu-affinity")
req.HookThreads, _ = flags.GetUint32("hook-threads")
req.HashInfo, _ = flags.GetBool("hash-info")
if req.HashInfo {
req.HashInfoLevel = 1
}
if flags.Changed("hash-info-level") {
req.HashInfoLevel, _ = flags.GetUint32("hash-info-level")
}
req.BackendIgnoreCUDA, _ = flags.GetBool("backend-ignore-cuda")
req.BackendIgnoreHip, _ = flags.GetBool("backend-ignore-hip")
req.BackendIgnoreMetal, _ = flags.GetBool("backend-ignore-metal")
req.BackendIgnoreOpenCL, _ = flags.GetBool("backend-ignore-opencl")
req.BackendInfo, _ = flags.GetBool("backend-info")
if req.BackendInfo {
req.BackendInfoLevel = 1
}
if flags.Changed("backend-info-level") {
req.BackendInfoLevel, _ = flags.GetUint32("backend-info-level")
}
req.BackendDevices = uintSliceToUint32(flags, "backend-devices")
req.BackendDevicesVirtMulti, _ = flags.GetUint32("backend-devices-virtmulti")
req.BackendDevicesVirtHost, _ = flags.GetUint32("backend-devices-virthost")
req.OpenCLDeviceTypes = uintSliceToUint32(flags, "opencl-device-types")
req.MetalCompilerRuntime, _ = flags.GetUint32("metal-compiler-runtime")
req.OptimizedKernelEnable, _ = flags.GetBool("optimized-kernel-enable")
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disable")
if !req.MultiplyAccelDisabled {
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disabled")
}
if flags.Changed("workload-profile") {
raw, _ := flags.GetString("workload-profile")
profile, err := parseCrackWorkloadProfile(raw)
if err != nil {
return nil, err
}
req.WorkloadProfile = profile
}
req.KernelAccel, _ = flags.GetUint32("kernel-accel")
req.KernelLoops, _ = flags.GetUint32("kernel-loops")
req.KernelThreads, _ = flags.GetUint32("kernel-threads")
req.BackendVectorWidth, _ = flags.GetUint32("backend-vector-width")
req.SpinDamp, _ = flags.GetUint32("spin-damp")
req.HwmonDisable, _ = flags.GetBool("hwmon-disable")
req.HwmonTempAbort, _ = flags.GetUint32("hwmon-temp-abort")
if flags.Changed("hwmon-temp-abort") {
value := req.HwmonTempAbort
req.HwmonTempAbortV7 = &value
}
req.ScryptTMTO, _ = flags.GetUint32("scrypt-tmto")
if flags.Changed("scrypt-tmto") {
value := req.ScryptTMTO
req.ScryptTMTOV7 = &value
}
req.Skip, _ = flags.GetUint64("skip")
req.Limit, _ = flags.GetUint64("limit")
req.Keyspace, _ = flags.GetBool("keyspace")
req.TotalCandidates, _ = flags.GetBool("total-candidates")
req.Lookup, _ = flags.GetString("lookup")
req.DynamicX, _ = flags.GetBool("dynamic-x")
req.SeekDBPath, _ = flags.GetString("seekdb-path")
req.RuleLeft, _ = flags.GetString("rule-left")
req.RuleRight, _ = flags.GetString("rule-right")
if values, _ := flags.GetStringArray("rules-file"); len(values) > 0 {
req.RulesFile = []byte(values[0])
for _, value := range values {
req.RulesFilesV7 = append(req.RulesFilesV7, []byte(value))
}
}
req.GenerateRules, _ = flags.GetUint32("generate-rules")
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-func-min")
if !flags.Changed("generate-rules-func-min") {
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-fun-min")
}
if flags.Changed("generate-rules-func-min") || flags.Changed("generate-rules-fun-min") {
value := req.GenerateRulesFunMin
req.GenerateRulesFuncMinV7 = &value
}
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-func-max")
if !flags.Changed("generate-rules-func-max") {
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-fun-max")
}
if flags.Changed("generate-rules-func-max") || flags.Changed("generate-rules-fun-max") {
value := req.GenerateRulesFunMax
req.GenerateRulesFuncMaxV7 = &value
}
req.GenerateRulesFuncSel, _ = flags.GetString("generate-rules-func-sel")
req.GenerateRulesSeed, _ = flags.GetInt32("generate-rules-seed")
if flags.Changed("generate-rules-seed") && req.GenerateRulesSeed >= 0 {
value := uint32(req.GenerateRulesSeed)
req.GenerateRulesSeedV7 = &value
}
req.CustomCharset1, _ = flags.GetString("custom-charset1")
req.CustomCharset2, _ = flags.GetString("custom-charset2")
req.CustomCharset3, _ = flags.GetString("custom-charset3")
req.CustomCharset4, _ = flags.GetString("custom-charset4")
req.CustomCharset5, _ = flags.GetString("custom-charset5")
req.CustomCharset6, _ = flags.GetString("custom-charset6")
req.CustomCharset7, _ = flags.GetString("custom-charset7")
req.CustomCharset8, _ = flags.GetString("custom-charset8")
req.Identify, _ = flags.GetString("identify")
req.IdentifyMode, _ = flags.GetBool("identify-mode")
if req.IdentifyMode && (flags.Changed("hash-mode") || flags.Changed("hash-type")) {
return nil, fmt.Errorf("--identify-mode and hash mode/type are mutually exclusive")
}
req.Increment, _ = flags.GetBool("increment")
req.IncrementInverse, _ = flags.GetBool("increment-inverse")
req.IncrementMin, _ = flags.GetUint32("increment-min")
req.IncrementMax, _ = flags.GetUint32("increment-max")
if flags.Changed("bypass-delay") {
value, _ := flags.GetUint32("bypass-delay")
req.BypassDelay = &value
}
if flags.Changed("bypass-threshold") {
value, _ := flags.GetUint32("bypass-threshold")
req.BypassThreshold = &value
}
req.SlowCandidates, _ = flags.GetBool("slow-candidates")
req.BridgeParameter1, _ = flags.GetString("bridge-parameter1")
req.BridgeParameter2, _ = flags.GetString("bridge-parameter2")
req.BridgeParameter3, _ = flags.GetString("bridge-parameter3")
req.BridgeParameter4, _ = flags.GetString("bridge-parameter4")
req.BrainServer, _ = flags.GetBool("brain-server")
req.BrainServerTimer, _ = flags.GetUint32("brain-server-timer")
if flags.Changed("brain-server-timer") {
value := req.BrainServerTimer
req.BrainServerTimerV7 = &value
}
req.BrainClient, _ = flags.GetBool("brain-client")
req.BrainClientFeatures, _ = flags.GetString("brain-client-features")
if value, ok := parseUint32(req.BrainClientFeatures); ok {
req.BrainClientFeaturesV7 = value
}
req.BrainHost, _ = flags.GetString("brain-host")
req.BrainPort, _ = flags.GetUint32("brain-port")
req.BrainPassword, _ = flags.GetString("brain-password")
if flags.Changed("brain-password") {
value := req.BrainPassword
req.BrainPasswordV7 = &value
}
req.BrainSession, _ = flags.GetString("brain-session")
if value, ok := parseHexUint32(req.BrainSession); ok {
req.BrainSessionV7 = &value
}
req.BrainSessionWhitelist, _ = flags.GetString("brain-session-whitelist")
if values, ok := parseHexUint32List(req.BrainSessionWhitelist); ok {
req.BrainSessionWhitelistV7 = values
}
req.BrainFeed, _ = flags.GetBool("brain-feed")
req.ColorCracked, _ = flags.GetBool("color-cracked")
req.HashCopy, _ = flags.GetBool("hash-copy")
req.EncryptWithPubkey, _ = flags.GetString("encrypt-with-pubkey")
return req, nil
}
func uintSliceToUint32(flags *pflag.FlagSet, name string) []uint32 {
values, _ := flags.GetUintSlice(name)
if len(values) == 0 {
return nil
}
out := make([]uint32, len(values))
for i, value := range values {
out[i] = uint32(value)
}
return out
}
func parseUint32(raw string) (uint32, bool) {
if raw == "" {
return 0, false
}
value, err := strconv.ParseUint(raw, 10, 32)
return uint32(value), err == nil
}
func parseHexUint32(raw string) (uint32, bool) {
raw = strings.TrimSpace(raw)
if strings.HasPrefix(raw, "0x") || strings.HasPrefix(raw, "0X") {
raw = raw[2:]
}
if raw == "" {
return 0, false
}
value, err := strconv.ParseUint(raw, 16, 32)
return uint32(value), err == nil
}
func parseHexUint32List(raw string) ([]uint32, bool) {
if raw == "" {
return nil, false
}
parts := strings.Split(raw, ",")
values := make([]uint32, 0, len(parts))
for _, part := range parts {
value, ok := parseHexUint32(part)
if !ok {
return nil, false
}
values = append(values, value)
}
return values, true
}
func parseCrackAttackMode(raw string) (clientpb.CrackAttackMode, error) {
if raw == "" {
return clientpb.CrackAttackMode_STRAIGHT, nil
}
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
return clientpb.CrackAttackMode(numeric), nil
}
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.NewReplacer("-", "_", " ", "_").Replace(normalized)
switch normalized {
case "straight":
return clientpb.CrackAttackMode_STRAIGHT, nil
case "combination":
return clientpb.CrackAttackMode_COMBINATION, nil
case "bruteforce", "brute_force", "brute-force":
return clientpb.CrackAttackMode_BRUTEFORCE, nil
case "pcfg":
return clientpb.CrackAttackMode_PCFG, nil
case "hybrid_wordlist_mask":
return clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK, nil
case "hybrid_mask_wordlist":
return clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST, nil
case "generic":
return clientpb.CrackAttackMode_GENERIC, nil
case "association":
return clientpb.CrackAttackMode_ASSOCIATION, nil
case "no_attack", "none":
return clientpb.CrackAttackMode_NO_ATTACK, nil
case "hybrid":
return clientpb.CrackAttackMode_HYBRID, nil
default:
return clientpb.CrackAttackMode_STRAIGHT, fmt.Errorf("unknown attack mode: %s", raw)
}
}
func parseHashType(raw string) (clientpb.HashType, error) {
if raw == "" {
return clientpb.HashType_MD5, nil
}
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
return clientpb.HashType(numeric), nil
}
key := normalizeEnumKey(raw)
if value, ok := clientpb.HashType_value[key]; ok {
return clientpb.HashType(value), nil
}
return clientpb.HashType_MD5, fmt.Errorf("unknown hash type: %s", raw)
}
func parseCrackEncoding(raw string) (clientpb.CrackEncoding, error) {
if raw == "" {
return clientpb.CrackEncoding_INVALID_ENCODING, nil
}
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
return clientpb.CrackEncoding(numeric), nil
}
key := normalizeEnumKey(raw)
if value, ok := clientpb.CrackEncoding_value[key]; ok {
return clientpb.CrackEncoding(value), nil
}
return clientpb.CrackEncoding_INVALID_ENCODING, fmt.Errorf("unknown encoding: %s", raw)
}
func parseCrackOutfileFormat(raw string) (clientpb.CrackOutfileFormat, error) {
if raw == "" {
return clientpb.CrackOutfileFormat_INVALID_FORMAT, nil
}
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
return clientpb.CrackOutfileFormat(numeric), nil
}
key := normalizeEnumKey(raw)
if value, ok := clientpb.CrackOutfileFormat_value[key]; ok {
return clientpb.CrackOutfileFormat(value), nil
}
return clientpb.CrackOutfileFormat_INVALID_FORMAT, fmt.Errorf("unknown outfile format: %s", raw)
}
func parseCrackWorkloadProfile(raw string) (clientpb.CrackWorkloadProfile, error) {
if raw == "" {
return clientpb.CrackWorkloadProfile_INVALID_WORKLOAD_PROFILE, nil
}
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
return clientpb.CrackWorkloadProfile(numeric), nil
}
key := normalizeEnumKey(raw)
if value, ok := clientpb.CrackWorkloadProfile_value[key]; ok {
return clientpb.CrackWorkloadProfile(value), nil
}
return clientpb.CrackWorkloadProfile_INVALID_WORKLOAD_PROFILE, fmt.Errorf("unknown workload profile: %s", raw)
}
func normalizeEnumKey(value string) string {
if value == "" {
return ""
}
var builder strings.Builder
builder.Grow(len(value))
underscore := false
for i := 0; i < len(value); i++ {
ch := value[i]
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
if ch >= 'a' && ch <= 'z' {
ch -= 'a' - 'A'
}
builder.WriteByte(ch)
underscore = false
continue
}
if !underscore {
builder.WriteByte('_')
underscore = true
}
}
return strings.Trim(builder.String(), "_")
}
+487
View File
@@ -0,0 +1,487 @@
package crack
import (
"bytes"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/spf13/cobra"
"google.golang.org/protobuf/proto"
)
func TestBuildCrackCommandReadsKeyboardLayoutMapping(t *testing.T) {
mapping := []byte("a A\nb B\n")
path := filepath.Join(t.TempDir(), "keyboard.hckmap")
if err := os.WriteFile(path, mapping, 0600); err != nil {
t.Fatal(err)
}
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + path}); err != nil {
t.Fatal(err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand() error = %v", err)
}
if !bytes.Equal(got.KeyboardLayoutMapping, mapping) {
t.Fatalf("KeyboardLayoutMapping = %q, want file contents %q", got.KeyboardLayoutMapping, mapping)
}
}
func TestBuildCrackCommandRejectsInvalidKeyboardLayoutMapping(t *testing.T) {
tests := []struct {
name string
path func(*testing.T) string
want string
}{
{
name: "missing",
path: func(t *testing.T) string { return filepath.Join(t.TempDir(), "missing.hckmap") },
want: "open keyboard layout mapping",
},
{
name: "directory",
path: func(t *testing.T) string { return t.TempDir() },
want: "is not a regular file",
},
{
name: "empty",
path: func(t *testing.T) string {
path := filepath.Join(t.TempDir(), "empty.hckmap")
if err := os.WriteFile(path, nil, 0600); err != nil {
t.Fatal(err)
}
return path
},
want: "is empty",
},
{
name: "too large",
path: func(t *testing.T) string {
path := filepath.Join(t.TempDir(), "large.hckmap")
if err := os.WriteFile(path, make([]byte, maxKeyboardLayoutMappingBytes+1), 0600); err != nil {
t.Fatal(err)
}
return path
},
want: "exceeds",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + test.path(t)}); err != nil {
t.Fatal(err)
}
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("buildCrackCommand() error = %v, want substring %q", err, test.want)
}
})
}
}
func TestParseCrackAttackMode(t *testing.T) {
tests := []struct {
name string
want clientpb.CrackAttackMode
}{
{name: "straight", want: clientpb.CrackAttackMode_STRAIGHT},
{name: "combination", want: clientpb.CrackAttackMode_COMBINATION},
{name: "bruteforce", want: clientpb.CrackAttackMode_BRUTEFORCE},
{name: "pcfg", want: clientpb.CrackAttackMode_PCFG},
{name: "hybrid-wordlist-mask", want: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK},
{name: "hybrid-mask-wordlist", want: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST},
{name: "generic", want: clientpb.CrackAttackMode_GENERIC},
{name: "association", want: clientpb.CrackAttackMode_ASSOCIATION},
{name: "no-attack", want: clientpb.CrackAttackMode_NO_ATTACK},
{name: "hybrid", want: clientpb.CrackAttackMode_HYBRID},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseCrackAttackMode(test.name)
if err != nil {
t.Fatalf("parseCrackAttackMode(%q) returned error: %v", test.name, err)
}
if got != test.want {
t.Fatalf("parseCrackAttackMode(%q) = %v, want %v", test.name, got, test.want)
}
})
}
}
func TestBuildCrackCommandHashcatShortFlags(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"-a", "3", "-m", "1000"}); err != nil {
t.Fatal(err)
}
got, err := buildCrackCommand(cmd, []string{"hash"})
if err != nil {
t.Fatalf("buildCrackCommand() error = %v", err)
}
if got.AttackMode != clientpb.CrackAttackMode_BRUTEFORCE {
t.Fatalf("AttackMode = %s, want BRUTEFORCE", got.AttackMode)
}
if got.HashType != clientpb.HashType(1000) {
t.Fatalf("HashType = %d, want 1000", got.HashType)
}
}
func TestBuildCrackCommandHashcatV7Options(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
args := []string{
"--attack-mode=hybrid",
"--hash-mode=0",
"--input=first-input",
"--input=second-input",
"--stdin=stdin-data",
"--advice-disable",
"--status-timer=0",
"--stdin-timeout-abort=0",
"--restore-position",
"--outfile=outfile.txt",
"--outfile-json",
"--debug-file=debug.txt",
"--induction-dir=induction",
"--outfile-check-dir=outfile-check",
"--outfile-check-timer=0",
"--truecrypt-keyfiles=truecrypt.keys",
"--veracrypt-keyfiles=veracrypt.keys",
"--veracrypt-pim-start=11",
"--veracrypt-pim-stop=12",
"--encoding-from=windows-1252",
"--encoding-to=utf-8",
"--hccapx-message-pair=0",
"--nonce-error-corrections=0",
"--benchmark-min=13",
"--benchmark-max=14",
"--pipeline-stats",
"--task-time-breakdown",
"--bitmap-min=0",
"--bitmap-max=0",
"--hash-info",
"--hash-info-level=2",
"--backend-info",
"--backend-info-level=2",
"--backend-devices-virtmulti=15",
"--backend-devices-virthost=16",
"--metal-compiler-runtime=17",
"--multiply-accel-disable",
"--hwmon-temp-abort=0",
"--total-candidates",
"--lookup=lookup-value",
"--crackstation= station-one ",
"--dynamic-x",
"--seekdb-path=seekdb",
"--rule-left=l",
"--rule-right=r",
"--rules-file=:",
"--rules-file=$1",
"--custom-charset5=cs5",
"--custom-charset6=cs6",
"--custom-charset7=cs7",
"--custom-charset8=cs8",
"--identify=legacy-input",
"--increment-inverse",
"--bypass-delay=18",
"--bypass-threshold=19",
"--bridge-parameter1=bridge1",
"--bridge-parameter2=bridge2",
"--bridge-parameter3=bridge3",
"--bridge-parameter4=bridge4",
"--scrypt-tmto=0",
"--generate-rules-func-min=4",
"--generate-rules-func-max=8",
"--generate-rules-seed=0",
"--brain-client-features=3",
"--brain-password=",
"--brain-server-timer=0",
"--brain-session=deadbeef",
"--brain-session-whitelist=1,2,0x3",
"--brain-feed",
"--color-cracked",
"--hash-copy",
"--encrypt-with-pubkey=key.pub",
}
if err := cmd.Flags().Parse(args); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
zero := uint32(0)
want := &clientpb.CrackCommand{
AttackMode: clientpb.CrackAttackMode_HYBRID,
HashType: clientpb.HashType_INVALID,
HashMode: &zero,
Stdin: []byte("stdin-data"),
AdviceDisable: true,
StatusTimerV7: &zero,
StdinTimeoutAbortV7: &zero,
Outfile: "outfile.txt",
DebugFile: "debug.txt",
InductionDir: "induction",
OutfileCheckDir: "outfile-check",
OutfileCheckTimerV7: &zero,
TruecryptKeyfiles: "truecrypt.keys",
VeracryptKeyfiles: "veracrypt.keys",
VeracryptPimStart: uint32Pointer(11),
VeracryptPimStop: uint32Pointer(12),
EncodingFromName: "windows-1252",
EncodingToName: "utf-8",
HccapxMessagePairV7: &zero,
NonceErrorCorrectionsV7: &zero,
RuleLeft: "l",
RuleRight: "r",
RulesFile: []byte(":"),
RulesFilesV7: [][]byte{[]byte(":"), []byte("$1")},
PipelineStats: true,
TaskTimeBreakdown: true,
BitmapMinV7: &zero,
BitmapMaxV7: &zero,
HashInfo: true,
HashInfoLevel: 2,
BackendInfo: true,
BackendInfoLevel: 2,
MetalCompilerRuntime: 17,
RestorePosition: true,
OutfileJSON: true,
DynamicX: true,
SeekDBPath: "seekdb",
BenchmarkMin: 13,
BenchmarkMax: uint32Pointer(14),
BridgeParameter1: "bridge1",
BridgeParameter2: "bridge2",
BridgeParameter3: "bridge3",
BridgeParameter4: "bridge4",
BackendDevicesVirtMulti: 15,
BackendDevicesVirtHost: 16,
MultiplyAccelDisabled: true,
HwmonTempAbortV7: &zero,
TotalCandidates: true,
Lookup: "lookup-value",
Crackstation: "station-one",
CustomCharset5: "cs5",
CustomCharset6: "cs6",
CustomCharset7: "cs7",
CustomCharset8: "cs8",
IncrementInverse: true,
BypassDelay: uint32Pointer(18),
BypassThreshold: uint32Pointer(19),
ScryptTMTOV7: &zero,
GenerateRulesFunMin: 4,
GenerateRulesFunMax: 8,
GenerateRulesFuncMinV7: uint32Pointer(4),
GenerateRulesFuncMaxV7: uint32Pointer(8),
GenerateRulesSeedV7: &zero,
BrainClientFeatures: "3",
BrainClientFeaturesV7: 3,
BrainPasswordV7: stringPointer(""),
BrainSession: "deadbeef",
BrainSessionV7: uint32Pointer(0xdeadbeef),
BrainServerTimerV7: &zero,
BrainSessionWhitelist: "1,2,0x3",
BrainSessionWhitelistV7: []uint32{1, 2, 3},
BrainFeed: true,
ColorCracked: true,
HashCopy: true,
EncryptWithPubkey: "key.pub",
Identify: "legacy-input", //nolint:staticcheck // Verify the legacy positional operand remains populated for wire compatibility.
PositionalArguments: []string{"first-input", "second-input"},
}
if !proto.Equal(got, want) {
t.Fatal("buildCrackCommand() did not map the Hashcat v7 options as expected")
}
}
func uint32Pointer(value uint32) *uint32 {
return &value
}
func stringPointer(value string) *string {
return &value
}
func TestBuildCrackCommandBrainSessionsAreHexadecimal(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{
"--brain-session=10",
"--brain-session-whitelist=10,20",
}); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
if got.BrainSessionV7 == nil || *got.BrainSessionV7 != 0x10 {
t.Fatalf("BrainSessionV7 = %v, want 0x10", got.BrainSessionV7)
}
wantWhitelist := []uint32{0x10, 0x20}
if !slices.Equal(got.BrainSessionWhitelistV7, wantWhitelist) {
t.Fatalf("BrainSessionWhitelistV7 = %#v, want %#v", got.BrainSessionWhitelistV7, wantWhitelist)
}
}
func TestBuildCrackCommandPreservesCommasInPositionalInputs(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{
"--input=hashes,2026.txt",
"--input=?d?d,?l?l",
}); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
want := []string{"hashes,2026.txt", "?d?d,?l?l"}
if !slices.Equal(got.PositionalArguments, want) {
t.Fatalf("PositionalArguments = %#v, want %#v", got.PositionalArguments, want)
}
}
func TestBuildCrackCommandPreservesCommasInHashes(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{
"--hash=digest:salt,with,commas",
"--hash=second:digest,salt",
}); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
want := []string{"digest:salt,with,commas", "second:digest,salt"}
if !slices.Equal(got.Hashes, want) {
t.Fatalf("Hashes = %#v, want %#v", got.Hashes, want)
}
}
func TestBuildCrackCommandRejectsConflictingRestoreModes(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--restore-position", "--restore-show-command"}); err != nil {
t.Fatalf("parse flags: %v", err)
}
_, err := buildCrackCommand(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
t.Fatalf("buildCrackCommand() error = %v, want restore conflict error", err)
}
}
func TestBuildCrackCommandIdentifyHasNoImplicitHashType(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--identify-mode", "--input=hashes.txt"}); err != nil {
t.Fatal(err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatal(err)
}
if got.HashType != clientpb.HashType_INVALID || got.HashMode != nil {
t.Fatalf("identify command inherited hash mode/type: %#v", got)
}
}
func TestBuildCrackCommandLegacyFlagAliases(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{
"--multiply-accel-disabled",
"--generate-rules-fun-min=2",
"--generate-rules-fun-max=6",
}); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
if !got.MultiplyAccelDisabled || got.GenerateRulesFunMin != 2 || got.GenerateRulesFunMax != 6 {
t.Fatalf("legacy aliases were not preserved: %#v", got)
}
}
func TestBuildCrackCommandRejectsSegmentSize(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Set("segment-size", "32"); err != nil {
t.Fatalf("set segment-size: %v", err)
}
_, err := buildCrackCommand(cmd, nil)
if err == nil || !strings.Contains(err.Error(), "removed in Hashcat v7") {
t.Fatalf("buildCrackCommand() error = %v, want Hashcat v7 removal error", err)
}
}
func TestBuildCrackCommandCredentialSelectors(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{
"--credential= first-id ",
"--credential=second-id",
"--credential-collection= domain-admins ",
"--include-cracked-credentials",
}); err != nil {
t.Fatalf("parse flags: %v", err)
}
got, err := buildCrackCommand(cmd, nil)
if err != nil {
t.Fatalf("buildCrackCommand returned error: %v", err)
}
if !slices.Equal(got.CredentialIDs, []string{"first-id", "second-id"}) {
t.Fatalf("CredentialIDs = %#v", got.CredentialIDs)
}
if got.CredentialCollection != "domain-admins" {
t.Fatalf("CredentialCollection = %q", got.CredentialCollection)
}
if !got.IncludeCrackedCredentials {
t.Fatal("IncludeCrackedCredentials is false")
}
}
func TestBuildCrackCommandRejectsEmptyCredential(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--credential= "}); err != nil {
t.Fatalf("parse flags: %v", err)
}
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "cannot be empty") {
t.Fatalf("buildCrackCommand error = %v", err)
}
}
func TestBuildCrackCommandIncludeCrackedRequiresSelector(t *testing.T) {
cmd := &cobra.Command{Use: "crack-test"}
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse([]string{"--include-cracked-credentials"}); err != nil {
t.Fatalf("parse flags: %v", err)
}
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "requires --credential") {
t.Fatalf("buildCrackCommand error = %v", err)
}
}
+270
View File
@@ -0,0 +1,270 @@
package crack
import (
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const (
maxCrackBackendInfoLevel uint32 = 2
maxCrackHashInfoLevel uint32 = 2
)
type crackInvocationMode uint8
const (
crackInvocationDashboard crackInvocationMode = iota
crackInvocationJob
crackInvocationBackendInfo
crackInvocationKeyspace
crackInvocationTotalCandidates
crackInvocationLookup
crackInvocationIdentify
crackInvocationHashInfo
)
type crackInvocation struct {
mode crackInvocationMode
backendInfoLevel uint32
hashInfoLevel uint32
crackstation string
}
type crackQueryModeSelection struct {
mode crackInvocationMode
flagName string
}
var incompatibleCandidateQueryFlags = map[string]struct{}{
"benchmark": {},
"benchmark-all": {},
"benchmark-min": {},
"benchmark-max": {},
"brain-client": {},
"brain-client-features": {},
"brain-feed": {},
"brain-host": {},
"brain-password": {},
"brain-port": {},
"brain-server": {},
"brain-server-timer": {},
"brain-session": {},
"brain-session-whitelist": {},
"left": {},
"progress-only": {},
"restore": {},
"restore-file": {},
"restore-position": {},
"restore-show-command": {},
"show": {},
"speed-only": {},
}
// classifyCrackInvocation separates terminal command modes from work that
// belongs in the distributed crack queue. Backend information comes from the
// registration snapshots; the remaining query modes execute synchronously on
// one idle crackstation and never create a CrackJob.
//
//nolint:gocyclo // The ordered flag matrix keeps mutually exclusive query modes and their validation in one decision point.
func classifyCrackInvocation(cmd *cobra.Command, args []string) (crackInvocation, error) {
flags := cmd.Flags()
backendInfo, _ := flags.GetBool("backend-info")
backendInfoLevel, _ := flags.GetUint32("backend-info-level")
backendInfoLevelChanged := flags.Changed("backend-info-level")
hashInfo, _ := flags.GetBool("hash-info")
hashInfoLevel, _ := flags.GetUint32("hash-info-level")
hashInfoLevelChanged := flags.Changed("hash-info-level")
keyspace, _ := flags.GetBool("keyspace")
totalCandidates, _ := flags.GetBool("total-candidates")
lookup, _ := flags.GetString("lookup")
lookupChanged := flags.Changed("lookup")
identify, _ := flags.GetBool("identify-mode")
crackstation, _ := flags.GetString("crackstation")
crackstation = strings.TrimSpace(crackstation)
if flags.Changed("crackstation") && crackstation == "" {
return crackInvocation{}, fmt.Errorf("--crackstation cannot be empty")
}
if lookupChanged && lookup == "" {
return crackInvocation{}, fmt.Errorf("--lookup cannot be empty")
}
selections := make([]crackQueryModeSelection, 0, 6)
if backendInfo || backendInfoLevelChanged {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationBackendInfo, flagName: "--backend-info"})
}
if keyspace {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationKeyspace, flagName: "--keyspace"})
}
if totalCandidates {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationTotalCandidates, flagName: "--total-candidates"})
}
if lookupChanged {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationLookup, flagName: "--lookup"})
}
if identify {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationIdentify, flagName: "--identify-mode"})
}
if hashInfo || hashInfoLevelChanged {
selections = append(selections, crackQueryModeSelection{mode: crackInvocationHashInfo, flagName: "--hash-info"})
}
if len(selections) > 1 {
names := make([]string, 0, len(selections))
for _, selection := range selections {
names = append(names, selection.flagName)
}
sort.Strings(names)
return crackInvocation{}, fmt.Errorf("crack query modes cannot be combined: %s", strings.Join(names, ", "))
}
if len(selections) == 0 {
if flags.Changed("crackstation") {
return crackInvocation{}, fmt.Errorf("--crackstation requires --keyspace, --total-candidates, --lookup, --identify-mode, or --hash-info")
}
if shouldRunCrack(cmd, args) {
return crackInvocation{mode: crackInvocationJob}, nil
}
return crackInvocation{mode: crackInvocationDashboard}, nil
}
invocation := crackInvocation{mode: selections[0].mode, crackstation: crackstation}
switch invocation.mode {
case crackInvocationBackendInfo:
if !backendInfoLevelChanged {
backendInfoLevel = 1
}
if backendInfoLevel == 0 || backendInfoLevel > maxCrackBackendInfoLevel {
return crackInvocation{}, fmt.Errorf("--backend-info-level must be 1 or %d", maxCrackBackendInfoLevel)
}
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
"backend-info": {}, "backend-info-level": {}, "timeout": {},
})
if len(args) != 0 {
conflicts = append(conflicts, "positional hash arguments")
}
if len(conflicts) != 0 {
sort.Strings(conflicts)
return crackInvocation{}, fmt.Errorf("backend-info query cannot be combined with %s", strings.Join(conflicts, ", "))
}
invocation.backendInfoLevel = backendInfoLevel
case crackInvocationHashInfo:
if !hashInfoLevelChanged {
hashInfoLevel = 1
}
if hashInfoLevel == 0 || hashInfoLevel > maxCrackHashInfoLevel {
return crackInvocation{}, fmt.Errorf("--hash-info-level must be 1 or %d", maxCrackHashInfoLevel)
}
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
"crackstation": {}, "hash-info": {}, "hash-info-level": {}, "hash-mode": {}, "hash-type": {}, "timeout": {},
})
if len(args) != 0 {
conflicts = append(conflicts, "positional hash arguments")
}
if len(conflicts) != 0 {
sort.Strings(conflicts)
return crackInvocation{}, fmt.Errorf("hash-info query cannot be combined with %s", strings.Join(conflicts, ", "))
}
invocation.hashInfoLevel = hashInfoLevel
case crackInvocationIdentify:
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
"crackstation": {}, "hash": {}, "identify-mode": {}, "timeout": {},
})
if len(conflicts) != 0 {
return crackInvocation{}, fmt.Errorf("identify query cannot be combined with %s", strings.Join(conflicts, ", "))
}
if !hasCrackIdentifyInput(flags, args) {
return crackInvocation{}, fmt.Errorf("--identify-mode requires a positional hash or --hash")
}
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup:
if conflicts := candidateQueryConflicts(flags, args, invocation.mode); len(conflicts) != 0 {
return crackInvocation{}, fmt.Errorf("%s query cannot be combined with %s", crackInvocationName(invocation.mode), strings.Join(conflicts, ", "))
}
}
return invocation, nil
}
func crackInvocationName(mode crackInvocationMode) string {
switch mode {
case crackInvocationBackendInfo:
return "backend-info"
case crackInvocationKeyspace:
return "keyspace"
case crackInvocationTotalCandidates:
return "total-candidates"
case crackInvocationLookup:
return "lookup"
case crackInvocationIdentify:
return "identify"
case crackInvocationHashInfo:
return "hash-info"
default:
return "crack"
}
}
func changedCrackFlagConflicts(flags *pflag.FlagSet, allowed map[string]struct{}) []string {
conflicts := make([]string, 0)
flags.Visit(func(flag *pflag.Flag) {
if _, ok := allowed[flag.Name]; ok || changedBoolFlagIsFalse(flags, flag) {
return
}
conflicts = append(conflicts, "--"+flag.Name)
})
sort.Strings(conflicts)
return conflicts
}
func changedBoolFlagIsFalse(flags *pflag.FlagSet, flag *pflag.Flag) bool {
if flag.Value.Type() != "bool" {
return false
}
value, err := flags.GetBool(flag.Name)
return err == nil && !value
}
func hasCrackIdentifyInput(flags *pflag.FlagSet, args []string) bool {
if len(args) != 0 {
return true
}
hashes, _ := flags.GetStringArray("hash")
return len(hashes) != 0
}
func candidateQueryConflicts(flags *pflag.FlagSet, args []string, mode crackInvocationMode) []string {
conflicts := make([]string, 0)
if len(args) != 0 {
conflicts = append(conflicts, "positional hash arguments")
}
flags.Visit(func(flag *pflag.Flag) {
if changedBoolFlagIsFalse(flags, flag) {
return
}
switch flag.Name {
case "hash", "credential", "credential-collection":
conflicts = append(conflicts, "--"+flag.Name)
return
case "include-cracked-credentials":
includeCracked, _ := flags.GetBool(flag.Name)
if includeCracked {
conflicts = append(conflicts, "--"+flag.Name)
}
return
case "skip", "limit":
if mode != crackInvocationLookup {
value, _ := flags.GetUint64(flag.Name)
if value != 0 {
conflicts = append(conflicts, "--"+flag.Name)
}
}
return
}
if _, incompatible := incompatibleCandidateQueryFlags[flag.Name]; incompatible {
conflicts = append(conflicts, "--"+flag.Name)
}
})
sort.Strings(conflicts)
return conflicts
}
+527
View File
@@ -0,0 +1,527 @@
package crack
import (
"context"
"errors"
"strings"
"testing"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
func newCrackDispatchCommand(t *testing.T, flags ...string) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "crack-test"}
cmd.SetContext(context.Background())
cmd.Flags().Int64("timeout", 0, "test timeout")
bindCrackFlags(cmd.Flags())
if err := cmd.Flags().Parse(flags); err != nil {
t.Fatalf("parse flags: %v", err)
}
return cmd
}
func TestClassifyCrackInvocation(t *testing.T) {
tests := []struct {
name string
flags []string
args []string
wantMode crackInvocationMode
wantBackendLevel uint32
wantHashLevel uint32
wantCrackstation string
wantError string
}{
{name: "dashboard", wantMode: crackInvocationDashboard},
{name: "timeout only", flags: []string{"--timeout=5"}, wantMode: crackInvocationDashboard},
{name: "positional hash job", args: []string{"hash"}, wantMode: crackInvocationJob},
{name: "job flag", flags: []string{"--attack-mode=3"}, wantMode: crackInvocationJob},
{name: "explicit false is a no-op", flags: []string{"--backend-info=false"}, wantMode: crackInvocationDashboard},
{name: "explicit false with job flag", flags: []string{"--backend-info=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
{name: "keyspace false is a no-op", flags: []string{"--keyspace=false"}, wantMode: crackInvocationDashboard},
{name: "keyspace false with job flag", flags: []string{"--keyspace=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
{name: "keyspace", flags: []string{"--keyspace"}, wantMode: crackInvocationKeyspace},
{name: "keyspace candidate flags", flags: []string{"--keyspace", "--attack-mode=3", "--input=?d?d", "--increment"}, wantMode: crackInvocationKeyspace},
{name: "keyspace selected station", flags: []string{"--keyspace", "--crackstation= station-one "}, wantMode: crackInvocationKeyspace, wantCrackstation: "station-one"},
{name: "keyspace timeout", flags: []string{"--keyspace", "--timeout=5"}, wantMode: crackInvocationKeyspace},
{name: "keyspace skip conflict", flags: []string{"--keyspace", "--skip=1"}, wantError: "--skip"},
{name: "keyspace zero skip no-op", flags: []string{"--keyspace", "--skip=0"}, wantMode: crackInvocationKeyspace},
{name: "keyspace positional hash conflict", flags: []string{"--keyspace"}, args: []string{"hash"}, wantError: "positional hash arguments"},
{name: "keyspace hash flag conflict", flags: []string{"--keyspace", "--hash=digest"}, wantError: "--hash"},
{name: "keyspace credential conflict", flags: []string{"--keyspace", "--credential=deadbeef"}, wantError: "--credential"},
{name: "keyspace collection conflict", flags: []string{"--keyspace", "--credential-collection=secrets"}, wantError: "--credential-collection"},
{name: "keyspace include cracked conflict", flags: []string{"--keyspace", "--include-cracked-credentials"}, wantError: "--include-cracked-credentials"},
{name: "keyspace and backend info conflict", flags: []string{"--keyspace", "--backend-info"}, wantError: "--keyspace"},
{name: "backend info ignores disabled keyspace", flags: []string{"--backend-info", "--keyspace=false"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
{name: "backend info", flags: []string{"--backend-info"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
{name: "backend info and timeout", flags: []string{"--backend-info", "--timeout=5"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
{name: "level one", flags: []string{"--backend-info-level=1"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
{name: "level two", flags: []string{"--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
{name: "explicit level wins", flags: []string{"--backend-info", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
{name: "level independently enables mode", flags: []string{"--backend-info=false", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
{name: "zero level", flags: []string{"--backend-info-level=0"}, wantError: "must be 1 or 2"},
{name: "unsupported level", flags: []string{"--backend-info-level=3"}, wantError: "must be 1 or 2"},
{name: "positional conflict", flags: []string{"--backend-info"}, args: []string{"hash"}, wantError: "positional hash arguments"},
{name: "job flag conflict", flags: []string{"--backend-info", "--benchmark"}, wantError: "--benchmark"},
{name: "input conflict", flags: []string{"--backend-info-level=2", "--input=wordlist"}, wantError: "--input"},
{name: "backend info station conflict", flags: []string{"--backend-info", "--crackstation=station-one"}, wantError: "--crackstation"},
{name: "total candidates", flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationTotalCandidates},
{name: "total candidates false", flags: []string{"--total-candidates=false"}, wantMode: crackInvocationDashboard},
{name: "total candidates hash conflict", flags: []string{"--total-candidates", "--hash=digest"}, wantError: "--hash"},
{name: "total candidates limit conflict", flags: []string{"--total-candidates", "--limit=1"}, wantError: "--limit"},
{name: "lookup", flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationLookup},
{name: "lookup allows window", flags: []string{"--lookup=candidate", "--skip=1", "--limit=2"}, wantMode: crackInvocationLookup},
{name: "lookup empty", flags: []string{"--lookup="}, wantError: "cannot be empty"},
{name: "query modes conflict", flags: []string{"--keyspace", "--total-candidates"}, wantError: "cannot be combined"},
{name: "candidate query terminal conflict", flags: []string{"--keyspace", "--show"}, wantError: "--show"},
{name: "identify positional", flags: []string{"--identify-mode"}, args: []string{"digest"}, wantMode: crackInvocationIdentify},
{name: "identify hash flag", flags: []string{"--identify-mode", "--hash=digest"}, wantMode: crackInvocationIdentify},
{name: "identify false", flags: []string{"--identify-mode=false"}, wantMode: crackInvocationDashboard},
{name: "identify missing hash", flags: []string{"--identify-mode"}, wantError: "requires a positional hash or --hash"},
{name: "identify credential conflict", flags: []string{"--identify-mode", "--credential=deadbeef"}, wantError: "--credential"},
{name: "identify hash mode conflict", flags: []string{"--identify-mode", "--hash=digest", "--hash-mode=0"}, wantError: "--hash-mode"},
{name: "hash info", flags: []string{"--hash-info"}, wantMode: crackInvocationHashInfo, wantHashLevel: 1},
{name: "hash info false", flags: []string{"--hash-info=false"}, wantMode: crackInvocationDashboard},
{name: "hash info level", flags: []string{"--hash-info=false", "--hash-info-level=2", "--hash-mode=0", "--crackstation=station-one"}, wantMode: crackInvocationHashInfo, wantHashLevel: 2, wantCrackstation: "station-one"},
{name: "hash info zero level", flags: []string{"--hash-info-level=0"}, wantError: "must be 1 or 2"},
{name: "hash info unsupported level", flags: []string{"--hash-info-level=3"}, wantError: "must be 1 or 2"},
{name: "hash info hash conflict", flags: []string{"--hash-info", "--hash=digest"}, wantError: "--hash"},
{name: "crackstation requires query", flags: []string{"--crackstation=station-one"}, wantError: "requires --keyspace"},
{name: "crackstation cannot be empty", flags: []string{"--keyspace", "--crackstation= "}, wantError: "cannot be empty"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
invocation, err := classifyCrackInvocation(newCrackDispatchCommand(t, test.flags...), test.args)
if test.wantError != "" {
if err == nil || !strings.Contains(err.Error(), test.wantError) {
t.Fatalf("classifyCrackInvocation() error = %v, want substring %q", err, test.wantError)
}
return
}
if err != nil {
t.Fatalf("classifyCrackInvocation() error = %v", err)
}
if invocation.mode != test.wantMode || invocation.backendInfoLevel != test.wantBackendLevel ||
invocation.hashInfoLevel != test.wantHashLevel || invocation.crackstation != test.wantCrackstation {
t.Fatalf("classifyCrackInvocation() = %#v, want mode %d backend level %d hash level %d station %q",
invocation, test.wantMode, test.wantBackendLevel, test.wantHashLevel, test.wantCrackstation)
}
})
}
}
type crackDispatchRPCCapture struct {
rpcpb.SliverRPCClient
crackstationsCalls int
crackCalls int
crackFilesCalls int
hadDeadline bool
crackHadDeadline bool
err error
response *clientpb.Crackstations
crackResponse *clientpb.CrackResponse
crackFilesResponse *clientpb.CrackFiles
crackRequest *clientpb.CrackCommand
}
func (capture *crackDispatchRPCCapture) Crackstations(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
capture.crackstationsCalls++
_, capture.hadDeadline = ctx.Deadline()
if capture.err != nil {
return nil, capture.err
}
if capture.response != nil {
return capture.response, nil
}
return &clientpb.Crackstations{}, nil
}
func (capture *crackDispatchRPCCapture) Crack(ctx context.Context, request *clientpb.CrackCommand, _ ...grpc.CallOption) (*clientpb.CrackResponse, error) {
capture.crackCalls++
_, capture.crackHadDeadline = ctx.Deadline()
capture.crackRequest = request
if capture.crackResponse != nil {
return capture.crackResponse, nil
}
return &clientpb.CrackResponse{}, nil
}
func (capture *crackDispatchRPCCapture) CrackFilesList(_ context.Context, _ *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
capture.crackFilesCalls++
if capture.crackFilesResponse != nil {
return capture.crackFilesResponse, nil
}
return &clientpb.CrackFiles{}, nil
}
func TestCrackBackendInfoQueriesStationsWithoutCreatingJob(t *testing.T) {
capture := &crackDispatchRPCCapture{}
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
t.Fatal(err)
}
CrackCmd(newCrackDispatchCommand(t, "--backend-info", "--timeout=1"), con, nil)
if capture.crackstationsCalls != 1 {
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
}
if capture.crackCalls != 0 || capture.crackFilesCalls != 0 {
t.Fatalf("backend-info used job path: Crack=%d CrackFilesList=%d", capture.crackCalls, capture.crackFilesCalls)
}
if !capture.hadDeadline {
t.Fatal("Crackstations context did not inherit --timeout")
}
}
func TestCrackKeyspaceUsesSynchronousCrackResponse(t *testing.T) {
digest := strings.Repeat("a", 64)
capture := &crackDispatchRPCCapture{
crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: "100",
}},
crackFilesResponse: &clientpb.CrackFiles{
Files: []*clientpb.CrackFile{{
ID: "wordlist-id", Name: "words.txt", Type: clientpb.CrackFileType_WORDLIST, Sha2_256: digest,
}},
},
}
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
t.Fatal(err)
}
CrackCmd(newCrackDispatchCommand(t, "--keyspace", "--input=words.txt", "--crackstation=station-one", "--timeout=1"), con, nil)
if capture.crackCalls != 1 {
t.Fatalf("Crack calls = %d, want 1", capture.crackCalls)
}
if capture.crackstationsCalls != 0 {
t.Fatalf("keyspace queried crackstations directly %d times", capture.crackstationsCalls)
}
if capture.crackFilesCalls != 1 {
t.Fatalf("CrackFilesList calls = %d, want 1", capture.crackFilesCalls)
}
if !capture.crackHadDeadline {
t.Fatal("Crack context did not inherit --timeout")
}
if capture.crackRequest == nil || !capture.crackRequest.Keyspace {
t.Fatalf("Crack request = %#v, want keyspace mode", capture.crackRequest)
}
if capture.crackRequest.Crackstation != "station-one" {
t.Fatalf("Crack request station = %q, want station-one", capture.crackRequest.Crackstation)
}
wantInput := "crackfile://wordlist/" + digest
if len(capture.crackRequest.PositionalArguments) != 1 || capture.crackRequest.PositionalArguments[0] != wantInput {
t.Fatalf("resolved keyspace inputs = %#v, want %q", capture.crackRequest.PositionalArguments, wantInput)
}
if len(capture.crackRequest.Hashes) != 0 || len(capture.crackRequest.CredentialIDs) != 0 {
t.Fatalf("keyspace request contains hash selectors: %#v", capture.crackRequest)
}
}
func TestCrackSynchronousQueriesUseCrackRPCWithoutCreatingJob(t *testing.T) {
tests := []struct {
name string
flags []string
args []string
resultMode clientpb.CrackQueryMode
result string
check func(*testing.T, *clientpb.CrackCommand)
}{
{
name: "total candidates",
flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d", "--crackstation=station-one"},
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES,
result: "100",
check: func(t *testing.T, command *clientpb.CrackCommand) {
if !command.TotalCandidates || command.Crackstation != "station-one" {
t.Fatalf("total-candidates request = %#v", command)
}
},
},
{
name: "lookup",
flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"},
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
result: "lookup output",
check: func(t *testing.T, command *clientpb.CrackCommand) {
if command.Lookup != "candidate" {
t.Fatalf("lookup request = %#v", command)
}
},
},
{
name: "identify",
flags: []string{"--identify-mode"},
args: []string{"digest"},
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY,
result: "identify output",
check: func(t *testing.T, command *clientpb.CrackCommand) {
if !command.IdentifyMode || len(command.Hashes) != 1 || command.Hashes[0] != "digest" {
t.Fatalf("identify request = %#v", command)
}
},
},
{
name: "hash info",
flags: []string{"--hash-info-level=2", "--hash-mode=1000"},
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO,
result: "hash-info output",
check: func(t *testing.T, command *clientpb.CrackCommand) {
if command.HashInfoLevel != 2 || command.HashMode == nil || *command.HashMode != 1000 {
t.Fatalf("hash-info request = %#v", command)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
capture := &crackDispatchRPCCapture{crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: test.resultMode, Value: test.result,
}}}
con := console.NewConsole(false)
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
t.Fatal(err)
}
CrackCmd(newCrackDispatchCommand(t, test.flags...), con, test.args)
if capture.crackCalls != 1 || capture.crackstationsCalls != 0 {
t.Fatalf("RPC calls: Crack=%d Crackstations=%d", capture.crackCalls, capture.crackstationsCalls)
}
if capture.crackRequest == nil {
t.Fatal("Crack request was not captured")
}
test.check(t, capture.crackRequest)
})
}
}
func TestCanonicalCrackKeyspace(t *testing.T) {
tests := []struct {
name string
response *clientpb.CrackResponse
want string
wantErr bool
}{
{name: "canonical", response: &clientpb.CrackResponse{Keyspace: "100"}, want: "100"},
{name: "normalizes", response: &clientpb.CrackResponse{Keyspace: " 00100 "}, want: "100"},
{name: "zero", response: &clientpb.CrackResponse{Keyspace: "0"}, want: "0"},
{name: "missing response", wantErr: true},
{name: "missing value", response: &clientpb.CrackResponse{}, wantErr: true},
{name: "negative", response: &clientpb.CrackResponse{Keyspace: "-1"}, wantErr: true},
{name: "overflow", response: &clientpb.CrackResponse{Keyspace: "18446744073709551616"}, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := canonicalCrackKeyspace(test.response)
if (err != nil) != test.wantErr {
t.Fatalf("canonicalCrackKeyspace() error = %v, wantErr %v", err, test.wantErr)
}
if got != test.want {
t.Fatalf("canonicalCrackKeyspace() = %q, want %q", got, test.want)
}
})
}
}
func TestCrackQueryResponseValue(t *testing.T) {
tests := []struct {
name string
invocation crackInvocation
response *clientpb.CrackResponse
want string
wantError string
}{
{
name: "typed keyspace",
invocation: crackInvocation{mode: crackInvocationKeyspace},
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: " 00100 ",
}},
want: "100",
},
{
name: "typed total candidates",
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES, Value: "42\n",
}},
want: "42",
},
{
name: "text remains verbatim",
invocation: crackInvocation{mode: crackInvocationLookup},
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP, Value: "line one\nline two\n",
}},
want: "line one\nline two\n",
},
{
name: "wrong typed mode",
invocation: crackInvocation{mode: crackInvocationLookup},
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY, Value: "output",
}},
wantError: "for lookup query",
},
{
name: "new mode cannot use legacy keyspace",
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
response: &clientpb.CrackResponse{Keyspace: "42"},
wantError: "empty total-candidates query result",
},
{
name: "selected keyspace cannot use legacy response",
invocation: crackInvocation{mode: crackInvocationKeyspace, crackstation: "station-one"},
response: &clientpb.CrackResponse{Keyspace: "42"},
wantError: "empty keyspace query result",
},
{
name: "empty text",
invocation: crackInvocation{mode: crackInvocationHashInfo},
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO, Value: " \n",
}},
wantError: "empty hash-info query result",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := crackQueryResponseValue(test.response, test.invocation)
if test.wantError != "" {
if err == nil || !strings.Contains(err.Error(), test.wantError) {
t.Fatalf("crackQueryResponseValue() error = %v, want substring %q", err, test.wantError)
}
return
}
if err != nil {
t.Fatal(err)
}
if got != test.want {
t.Fatalf("crackQueryResponseValue() = %q, want %q", got, test.want)
}
})
}
}
func TestCrackQueryResponsePreservesStderr(t *testing.T) {
response := &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
Value: "lookup output",
Stderr: "hashcat warning\n",
}}
value, stderr, err := crackQueryResponse(response, crackInvocation{mode: crackInvocationLookup})
if err != nil {
t.Fatal(err)
}
if value != "lookup output" || stderr != "hashcat warning\n" {
t.Fatalf("crackQueryResponse() = (%q, %q), want preserved value and stderr", value, stderr)
}
}
func TestCrackResponseKeyspaceFieldNumberAndRoundTrip(t *testing.T) {
field := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Keyspace")
if field == nil {
t.Fatal("CrackResponse is missing field Keyspace")
}
if got, want := field.Number(), protoreflect.FieldNumber(2); got != want {
t.Fatalf("CrackResponse.Keyspace tag = %d, want %d", got, want)
}
queryField := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Query")
if queryField == nil {
t.Fatal("CrackResponse is missing field Query")
}
if got, want := queryField.Number(), protoreflect.FieldNumber(3); got != want {
t.Fatalf("CrackResponse.Query tag = %d, want %d", got, want)
}
stderrField := (&clientpb.CrackQueryResult{}).ProtoReflect().Descriptor().Fields().ByName("Stderr")
if stderrField == nil {
t.Fatal("CrackQueryResult is missing field Stderr")
}
if got, want := stderrField.Number(), protoreflect.FieldNumber(6); got != want {
t.Fatalf("CrackQueryResult.Stderr tag = %d, want %d", got, want)
}
want := &clientpb.CrackResponse{
Job: &clientpb.CrackJob{ID: "job-id"},
Keyspace: "18446744073709551615",
Query: &clientpb.CrackQueryResult{
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, CrackstationHostUUID: "host-id", Value: "42", Stderr: "warning\n",
},
}
raw, err := proto.Marshal(want)
if err != nil {
t.Fatalf("marshal crack response: %v", err)
}
got := &clientpb.CrackResponse{}
if err := proto.Unmarshal(raw, got); err != nil {
t.Fatalf("unmarshal crack response: %v", err)
}
if !proto.Equal(got, want) {
t.Fatalf("crack response round trip = %v, want %v", got, want)
}
}
func TestConnectedCrackstationsReturnsRPCError(t *testing.T) {
want := errors.New("query failed")
capture := &crackDispatchRPCCapture{err: want}
if _, err := connectedCrackstations(context.Background(), capture); !errors.Is(err, want) {
t.Fatalf("connectedCrackstations() error = %v, want %v", err, want)
}
if capture.crackstationsCalls != 1 {
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
}
}
func TestConnectedCrackstationsReturnsEveryNonNilStation(t *testing.T) {
capture := &crackDispatchRPCCapture{response: &clientpb.Crackstations{Crackstations: []*clientpb.Crackstation{
nil,
{Name: "one"},
{Name: "two"},
}}}
stations, err := connectedCrackstations(context.Background(), capture)
if err != nil {
t.Fatal(err)
}
if len(stations) != 2 || stations[0].Name != "one" || stations[1].Name != "two" {
t.Fatalf("connectedCrackstations() = %#v, want one and two", stations)
}
}
func TestCrackHashcatFlagsAreRootLocal(t *testing.T) {
root := Commands(nil)[0]
if root.Flags().Lookup("backend-info") == nil {
t.Fatal("root crack command is missing --backend-info")
}
if root.Flags().Lookup("crackstation") == nil {
t.Fatal("root crack command is missing --crackstation")
}
if root.PersistentFlags().Lookup("backend-info") != nil {
t.Fatal("--backend-info must not be inherited by crack subcommands")
}
if root.Flags().Lookup("stdout") != nil {
t.Fatal("root crack command unexpectedly exposes removed --stdout")
}
stations, _, err := root.Find([]string{"stations"})
if err != nil {
t.Fatalf("find crack stations: %v", err)
}
if stations.InheritedFlags().Lookup("backend-info") != nil {
t.Fatal("crack stations unexpectedly inherits --backend-info")
}
if stations.InheritedFlags().Lookup("timeout") == nil {
t.Fatal("crack stations must continue to inherit --timeout")
}
}
Loaded 100 of 11573 files, more files were not shown because too many files have changed in this diff. Show more