mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
375
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4afcd5bc9d | ||
|
|
c746a5262c | ||
|
|
e91cdf117f | ||
|
|
1fb671f71b | ||
|
|
c43347886b | ||
|
|
4ef33f389c | ||
|
|
acd52520e4 | ||
|
|
9a373e20f0 | ||
|
|
aad186a632 | ||
|
|
ec9d9b2cc1 | ||
|
|
05d3b2604b | ||
|
|
c3e8a97f66 | ||
|
|
547a56696d | ||
|
|
84bb2397c7 | ||
|
|
503d040885 | ||
|
|
801452eeff | ||
|
|
f6c7d73014 | ||
|
|
228757f656 | ||
|
|
738010e990 | ||
|
|
9dcf9b2d25 | ||
|
|
32ab020dbd | ||
|
|
6fcaf970b1 | ||
|
|
47c83bbaa0 | ||
|
|
7d76febbc9 | ||
|
|
f4c182427f | ||
|
|
836e33ade7 | ||
|
|
0aa7e5bf96 | ||
|
|
b46df4b05e | ||
|
|
cbc9200649 | ||
|
|
a0bf009c06 | ||
|
|
46f516fa3a | ||
|
|
1f82c38e11 | ||
|
|
6ac84ffc11 | ||
|
|
2638b5f384 | ||
|
|
cf19741b9d | ||
|
|
b2c0979b63 | ||
|
|
ea5e1412b2 | ||
|
|
bd948b8ab1 | ||
|
|
937a74d597 | ||
|
|
e747187dda | ||
|
|
ec992dea76 | ||
|
|
544c872881 | ||
|
|
4701b895b7 | ||
|
|
18c56ec3c5 | ||
|
|
4b12c80534 | ||
|
|
a8c6d5e0a3 | ||
|
|
67c6d0d356 | ||
|
|
5e3b432b9b | ||
|
|
fc4d9a1c36 | ||
|
|
55b97df9f7 | ||
|
|
227328fa87 | ||
|
|
071106d0bf | ||
|
|
8c326e6af9 | ||
|
|
5e9ba47bf4 | ||
|
|
0537e285fc | ||
|
|
ca685f5eed | ||
|
|
902999493b | ||
|
|
14363c5d2a | ||
|
|
926b7ae1a7 | ||
|
|
bf815b36ed | ||
|
|
637853e8c2 | ||
|
|
0c022ec722 | ||
|
|
7c83b458fa | ||
|
|
f447a170c6 | ||
|
|
d565592773 | ||
|
|
c21da4dceb | ||
|
|
b08e2ff5c0 | ||
|
|
09b0540e75 | ||
|
|
b4005efda8 | ||
|
|
6d086fd7a7 | ||
|
|
ccebd505b0 | ||
|
|
94c5022dd0 | ||
|
|
b19e383111 | ||
|
|
02ebd99c68 | ||
|
|
83d82e7161 | ||
|
|
c481a4f5db | ||
|
|
eb5ac1b309 | ||
|
|
e73618e0a5 | ||
|
|
0017fd2fe8 | ||
|
|
e44111b6b4 | ||
|
|
aa2dce510a | ||
|
|
5e6acde3c7 | ||
|
|
99a8d3f859 | ||
|
|
fc15a47b1e | ||
|
|
8b32b6525f | ||
|
|
9e0fafb785 | ||
|
|
12ebffb16c | ||
|
|
b539762de7 | ||
|
|
e71af809d4 | ||
|
|
f3ffa3323b | ||
|
|
778331d474 | ||
|
|
8d259577af | ||
|
|
aa108a66a8 | ||
|
|
44e5464950 | ||
|
|
1381200b33 | ||
|
|
f81b250a04 | ||
|
|
8b4ee5f5c5 | ||
|
|
68cb1363e8 | ||
|
|
2344a3ae50 | ||
|
|
8cec5164b7 | ||
|
|
be50c25cb2 | ||
|
|
ee2cc83aa1 | ||
|
|
8e62e2dcd9 | ||
|
|
1347059639 | ||
|
|
29efea561c | ||
|
|
82deca2b0f | ||
|
|
808b3b6408 | ||
|
|
eba10ebbfc | ||
|
|
13903e46a8 | ||
|
|
8aaaa583bd | ||
|
|
4b428065e2 | ||
|
|
578f9a28d0 | ||
|
|
ce2f494f96 | ||
|
|
c1c36491a6 | ||
|
|
a5c3482c6d | ||
|
|
b428274515 | ||
|
|
5dc6adae47 | ||
|
|
264b00f3c7 | ||
|
|
ada79c8ecd | ||
|
|
edd4c3d097 | ||
|
|
38f84a4f6f | ||
|
|
aee0958e99 | ||
|
|
23c067982a | ||
|
|
7e917da3ac | ||
|
|
674b49f41f | ||
|
|
4ba04df621 | ||
|
|
3274382bc7 | ||
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
db45c4aaf9 | ||
|
|
198913d6f8 | ||
|
|
cd85824185 | ||
|
|
63797ac79c | ||
|
|
4633409623 | ||
|
|
13c2d33449 | ||
|
|
8cf478d7b0 | ||
|
|
f55215ff93 | ||
|
|
b5d3d0bad6 | ||
|
|
a32d81252c | ||
|
|
a1537535c4 | ||
|
|
61dd34e989 | ||
|
|
2b67cb917b | ||
|
|
ceead92ec5 | ||
|
|
df9ed94635 | ||
|
|
e891613f6f | ||
|
|
528b04427a | ||
|
|
f60a0e91d3 | ||
|
|
2308314529 | ||
|
|
04bc82bca3 | ||
|
|
acdfa33231 | ||
|
|
a03592de81 | ||
|
|
5cf9d088e8 | ||
|
|
db3294b926 | ||
|
|
1847b8c24b | ||
|
|
79e4259081 | ||
|
|
9ff9b55352 | ||
|
|
9767e24edd | ||
|
|
bb572f3846 | ||
|
|
01c5b5cee4 | ||
|
|
1049dc4a98 | ||
|
|
d1b0ba13c4 | ||
|
|
2d98f14149 | ||
|
|
19e82064b1 | ||
|
|
3b72374685 | ||
|
|
20fb8c4975 | ||
|
|
68829c0a40 | ||
|
|
2c18062f4d | ||
|
|
f17ce0afbe | ||
|
|
116b12885c | ||
|
|
3d9860db6c | ||
|
|
1a6579e954 | ||
|
|
2659d848d7 | ||
|
|
df6571f63f | ||
|
|
aab1470e08 | ||
|
|
19d43fdc37 | ||
|
|
3b09e5ea80 | ||
|
|
b904d29b25 | ||
|
|
8e2b069a7d | ||
|
|
096c418d27 | ||
|
|
d9e241ccb5 | ||
|
|
086da95525 | ||
|
|
27ed557e55 | ||
|
|
af7c823508 | ||
|
|
f2a3d8ee6d | ||
|
|
de7215d2c0 | ||
|
|
2d555b16d9 | ||
|
|
597a2571fc | ||
|
|
409b4c8fdb | ||
|
|
2e72bc96b9 | ||
|
|
fa9557348e | ||
|
|
4768e36074 | ||
|
|
e53f66de72 | ||
|
|
1d33b89ec5 | ||
|
|
7b0f420290 | ||
|
|
00824d1b83 | ||
|
|
99ad05b7b7 | ||
|
|
80273462bd | ||
|
|
2cdefc7369 | ||
|
|
80c752a422 | ||
|
|
8a8a9dc498 | ||
|
|
5f8540b3c4 | ||
|
|
4eef868047 | ||
|
|
5ebfecd822 | ||
|
|
9762560228 | ||
|
|
6310c7d6a0 | ||
|
|
fafe360672 | ||
|
|
d9b6f41940 | ||
|
|
f1ccf0eac3 | ||
|
|
d2aa924bc4 | ||
|
|
46861cd428 | ||
|
|
1c1825f8f0 | ||
|
|
a943995040 | ||
|
|
f0d2a2cf24 | ||
|
|
27ac612c82 | ||
|
|
baedb563f3 | ||
|
|
45c7914602 | ||
|
|
92d85cb8ff | ||
|
|
f5e831844a | ||
|
|
ab2fc7dc4a | ||
|
|
5e377019e4 | ||
|
|
88a4bebca0 | ||
|
|
2daccb5bd3 | ||
|
|
e2a009d4ac | ||
|
|
eda51f826a | ||
|
|
f83dc42a64 | ||
|
|
17e8c996a6 | ||
|
|
2b667f0e7e | ||
|
|
73bcf40bfd | ||
|
|
b635adcb3c | ||
|
|
2b92397e07 | ||
|
|
c23067a0e0 | ||
|
|
3aefc39c08 | ||
|
|
abd8c83df8 | ||
|
|
6cce9426f2 | ||
|
|
2e742863a1 | ||
|
|
dc13a5bb93 | ||
|
|
1f2946eb87 | ||
|
|
5eeda4c02b | ||
|
|
55a3386313 | ||
|
|
b34589a002 | ||
|
|
4486b17b49 | ||
|
|
377ee5ca7c | ||
|
|
4765f40854 | ||
|
|
e239d85821 | ||
|
|
dec419b284 | ||
|
|
6d51a23cc9 | ||
|
|
3d6eec47be | ||
|
|
464fb7008c | ||
|
|
ffa5f89f5f | ||
|
|
f19cbdc957 | ||
|
|
83bfc8d8e6 | ||
|
|
365bb3596e | ||
|
|
9ee96c2ce6 | ||
|
|
e776e266d0 | ||
|
|
d080c8a646 | ||
|
|
cb6402245e | ||
|
|
e4357277ec | ||
|
|
5dc2ab1490 | ||
|
|
9f034a9956 | ||
|
|
f700535997 | ||
|
|
31bca034d8 | ||
|
|
fac4ad5fb7 | ||
|
|
c128d33514 | ||
|
|
4a37262bcf | ||
|
|
a2295112bd | ||
|
|
834dff0f6e | ||
|
|
f4a0431fdd | ||
|
|
d072b328c0 | ||
|
|
435bbcbd7e | ||
|
|
84dc28e584 | ||
|
|
639592cb02 | ||
|
|
8c7a1a6b40 | ||
|
|
06274c4061 | ||
|
|
593925b4a5 | ||
|
|
255477e572 | ||
|
|
0acc6911d8 | ||
|
|
4ca026906b | ||
|
|
97a249e28e | ||
|
|
9f331687d3 | ||
|
|
14d68d6fcd | ||
|
|
05c5334fea | ||
|
|
d277a14c57 | ||
|
|
a7b5dbe6ee | ||
|
|
c188dc1e69 | ||
|
|
9fc9d1762e | ||
|
|
4ef86448d0 | ||
|
|
02117a6072 | ||
|
|
c272c8abac | ||
|
|
cdf2412b39 | ||
|
|
40f52791d3 | ||
|
|
a5d8bd16b9 | ||
|
|
bf113f2151 | ||
|
|
4cd010afb9 | ||
|
|
04b0bdd199 | ||
|
|
9682db93b3 | ||
|
|
1d7ffb4ac2 | ||
|
|
c5e7cd6354 | ||
|
|
b20ba7284b | ||
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Preserve pinned upstream source and attribution files byte-for-byte.
|
||||
client/command/opfor/testdata/corpus/** -text
|
||||
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
|
||||
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @BishopFox/red-team
|
||||
* @BishopFox/bf-red-team
|
||||
@@ -2,8 +2,8 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -21,17 +21,36 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Find Semver Tag
|
||||
- name: Validate Release Tag
|
||||
id: tag
|
||||
run: |
|
||||
git fetch --prune --tags -f
|
||||
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
||||
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
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
needs:
|
||||
- tag-check
|
||||
- e2e
|
||||
if: needs.tag-check.outputs.tag != '' && needs.e2e.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 45
|
||||
@@ -41,10 +60,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Install Minisign
|
||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -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@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Test malicious destination poisoning and cross-session isolation
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: >-
|
||||
go test -buildvcs=false -mod=vendor
|
||||
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
|
||||
./server/c2
|
||||
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
|
||||
-count=1 -v
|
||||
|
||||
darwin:
|
||||
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os darwin \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
linux-native:
|
||||
name: comprehensive-e2e-linux-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os linux \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
windows:
|
||||
name: comprehensive-e2e-windows-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server.exe ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e.exe ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e.exe \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server.exe \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os windows \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate-rportfwd:
|
||||
name: Aggregate reverse-port-forward E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: rportfwd-coverage-input
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
|
||||
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce all 162 required cells
|
||||
id: aggregate
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input rportfwd-coverage-input
|
||||
-output rportfwd-coverage-summary
|
||||
|
||||
- name: Publish reverse-port-forward coverage table
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="rportfwd-coverage-summary/rportfwd-coverage.md"
|
||||
if [[ -s "${report}" ]]; then
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
- name: Upload aggregate reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-rportfwd-coverage-summary
|
||||
path: rportfwd-coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
aggregate-coverage:
|
||||
name: Aggregate comprehensive E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
|
||||
outputs:
|
||||
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: coverage-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p coverage-input coverage-summary
|
||||
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce required cells
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/report
|
||||
-input coverage-input
|
||||
-output coverage-summary
|
||||
|
||||
- name: Publish command coverage table
|
||||
id: publish_command_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
detailed_report="coverage-summary/coverage-summary.md"
|
||||
report="coverage-summary/command-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [[ -f "${detailed_report}" ]]; then
|
||||
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-coverage-summary
|
||||
path: coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites or failed or not-run coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
reflektor:
|
||||
name: Existing Reflektor test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/reflektor-tests.yml
|
||||
|
||||
shellcode:
|
||||
name: Shellcode E2E test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/shellcode-tests.yml
|
||||
@@ -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@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.13.1
|
||||
# Gate only findings introduced since the event base revision.
|
||||
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
|
||||
@@ -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@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-native:
|
||||
name: reflektor-linux-${{ matrix.goarch }}-native
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
windows:
|
||||
name: reflektor-windows-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: amd64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e.exe
|
||||
-repo .
|
||||
-server ./sliver-server.exe
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
@@ -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@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare target artifact
|
||||
run: |
|
||||
mkdir -p shellcode-results
|
||||
printf '%s/%s\n' \
|
||||
'${{ matrix.target_os }}' \
|
||||
'${{ matrix.target_arch }}' \
|
||||
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: ${{ matrix.server_os }}
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o ${{ matrix.server_executable }} ./server
|
||||
|
||||
- name: Compile shellcode E2E client
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.target_arch }}
|
||||
GOOS: ${{ matrix.target_os }}
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o ${{ matrix.test_executable }} ./test/e2e
|
||||
|
||||
- name: Run shellcode generation and execution tests
|
||||
run: >-
|
||||
${{ matrix.test_executable }}
|
||||
-test.v
|
||||
-test.run=^TestShellcodeE2E$
|
||||
-test.timeout=0
|
||||
-repo .
|
||||
-server ${{ matrix.server_executable }}
|
||||
-server-arch ${{ matrix.server_arch }}
|
||||
-target-os ${{ matrix.target_os }}
|
||||
-target-arch ${{ matrix.target_arch }}
|
||||
-results ./shellcode-results
|
||||
-transports mtls,wg,http
|
||||
-implant-modes session,beacon
|
||||
-beacon-interval 10s
|
||||
-shellcode-sgn-samples 4
|
||||
|
||||
- name: Upload target shellcode results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: shellcode-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate:
|
||||
name: Aggregate shellcode E2E coverage
|
||||
needs: target
|
||||
if: ${{ always() }}
|
||||
outputs:
|
||||
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p shellcode-input shellcode-summary
|
||||
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
|
||||
|
||||
- name: Download target shellcode results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: shellcode-e2e-target-*
|
||||
path: shellcode-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Aggregate shellcode coverage
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
|
||||
-input ./shellcode-input
|
||||
-output ./shellcode-summary
|
||||
|
||||
- name: Publish shellcode coverage table
|
||||
id: publish_shellcode_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="shellcode-summary/shellcode-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate shellcode coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-coverage-summary
|
||||
path: shellcode-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed or not-run shellcode coverage
|
||||
if: ${{ always() && steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
@@ -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: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -64,16 +68,59 @@ jobs:
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
windows-build-test:
|
||||
name: Windows Build & Test
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Install make
|
||||
run: choco install make -y --no-progress
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make Windows (amd64)
|
||||
run: make windows-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: pwsh
|
||||
run: |
|
||||
$baseSha = "${{ github.event.pull_request.base.sha }}"
|
||||
$headSha = "${{ github.sha }}"
|
||||
$changedFiles = git diff --name-only $baseSha $headSha
|
||||
|
||||
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
|
||||
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
} else {
|
||||
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Test Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
make clean
|
||||
make
|
||||
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
name: MacOS Test
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -108,10 +155,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.6"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/crush
|
||||
|
||||
/examples
|
||||
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
@@ -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
|
||||
@@ -4,7 +4,7 @@
|
||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
||||
- `./go-tests.sh` runs the curated Go test suite with build tags.
|
||||
- `./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
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
# Makefile for Sliver
|
||||
#
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
GOLANGCI_LINT ?= golangci-lint
|
||||
# The tree has legacy lint findings, so gate issues introduced by this change.
|
||||
GOLANGCI_LINT_BASE ?= master
|
||||
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
|
||||
GOLANGCI_LINT_PACKAGES = \
|
||||
./client/... \
|
||||
./server/... \
|
||||
./util/... \
|
||||
./protobuf/... \
|
||||
./docs \
|
||||
./implant/...
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 25
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 27
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
@@ -34,151 +34,298 @@ ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
|
||||
# *** 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 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
|
||||
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: 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
|
||||
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
|
||||
@@ -4,7 +4,7 @@ Sliver is an open source cross-platform adversary emulation/red team framework,
|
||||
|
||||
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://goreportcard.com/report/github.com/BishopFox/sliver) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
|
||||
+18
-7
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,9 @@ func init() {
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
|
||||
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
|
||||
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -20,6 +20,7 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command"
|
||||
@@ -44,6 +45,9 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
@@ -61,17 +65,20 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run {
|
||||
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
return transport.MTLSConnectWithStatus(config, statusFn)
|
||||
})
|
||||
} else {
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
|
||||
+5
-1
@@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
@@ -55,7 +59,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
defer transport.CloseGRPCConnection(ln)
|
||||
|
||||
con.Rpc = rpc
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
|
||||
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||
- `armory/` – Implements the 'armory' command group for the Sliver client console.
|
||||
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
|
||||
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -215,8 +215,6 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
|
||||
}
|
||||
}
|
||||
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
|
||||
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
|
||||
|
||||
// for each alias command, add a new app command
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
@@ -227,9 +225,14 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runAliasCommand(cmd, con, args)
|
||||
},
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
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 {
|
||||
@@ -250,7 +253,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
|
||||
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.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
|
||||
@@ -333,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)
|
||||
@@ -400,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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
|
||||
@@ -301,32 +301,44 @@ func getCommandsInCache(armoryPK string) []string {
|
||||
return commandNames
|
||||
}
|
||||
|
||||
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
|
||||
}
|
||||
if cacheEntry.Extension == nil {
|
||||
return false
|
||||
}
|
||||
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
|
||||
return true
|
||||
}
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
|
||||
packages := []*pkgCacheEntry{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
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
|
||||
})
|
||||
@@ -336,7 +348,7 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
|
||||
|
||||
func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
selectedPackageKey := ""
|
||||
optionKeys := util.Keys(options)
|
||||
optionKeys := slices.Collect(maps.Keys(options))
|
||||
slices.Sort(optionKeys)
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
@@ -732,9 +744,7 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear download message
|
||||
|
||||
extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
|
||||
|
||||
return nil
|
||||
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
|
||||
}
|
||||
|
||||
func writeArmoryTempFile(data []byte) (string, error) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -21,7 +21,9 @@ package armory
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -33,13 +35,13 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
type VersionInformation struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
ArmoryName string
|
||||
PackageID string
|
||||
}
|
||||
|
||||
type PackageType uint
|
||||
@@ -124,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)
|
||||
@@ -173,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() {
|
||||
@@ -186,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,
|
||||
@@ -194,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
|
||||
@@ -219,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
|
||||
|
||||
result := []UpdateIdentifier{}
|
||||
|
||||
aliasNames := util.Keys(aliasUpdates)
|
||||
extensionNames := util.Keys(extensionUpdates)
|
||||
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
|
||||
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
|
||||
for _, name := range aliasNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: AliasPackage,
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -121,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
wideTermWidth := con.Settings.SmallTermWidth < width
|
||||
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
|
||||
// preventing a second boundary from changing the table width while it is fitted.
|
||||
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
|
||||
windowsBeaconInList := false
|
||||
for _, beacon := range beacons {
|
||||
if beacon.OS == "windows" {
|
||||
windowsBeaconInList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if wideTermWidth {
|
||||
if windowsBeaconInList {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Integrity",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
|
||||
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
|
||||
})
|
||||
}
|
||||
|
||||
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
|
||||
if layout.Compact {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
@@ -175,22 +156,84 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
header := table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
}
|
||||
if includeIntegrity {
|
||||
header = append(header, "Integrity")
|
||||
}
|
||||
header = append(header, "Operating System")
|
||||
if layout.IncludeLocale {
|
||||
header = append(header, "Locale")
|
||||
}
|
||||
header = append(header, "Last Check-in", "Next Check-in")
|
||||
tw.AppendHeader(header)
|
||||
}
|
||||
|
||||
activeBeaconID := ""
|
||||
if con.ActiveTarget != nil {
|
||||
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
|
||||
activeBeaconID = activeBeacon.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, beacon := range beacons {
|
||||
integrity := beacon.Integrity
|
||||
if integrity == "" {
|
||||
integrity = "-"
|
||||
}
|
||||
username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")
|
||||
lastCheckin := time.Unix(beacon.LastCheckin, 0)
|
||||
nextCheckin := time.Unix(beacon.NextCheckin, 0)
|
||||
canonicalEntries := []string{
|
||||
strings.Split(beacon.ID, "-")[0],
|
||||
beacon.Name,
|
||||
fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount),
|
||||
beacon.Transport,
|
||||
beacon.RemoteAddress,
|
||||
beacon.Hostname,
|
||||
username,
|
||||
fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID),
|
||||
integrity,
|
||||
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
|
||||
beacon.Locale,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, false),
|
||||
}
|
||||
if !beaconMatchesFilter(canonicalEntries, filter, filterRegex) {
|
||||
continue
|
||||
}
|
||||
|
||||
style := console.StyleNormal
|
||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||
if activeBeaconID == beacon.ID {
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if beacon.Integrity == "" {
|
||||
beacon.Integrity = "-"
|
||||
}
|
||||
|
||||
// We need a slice of strings so we can apply filters
|
||||
var rowEntries []string
|
||||
|
||||
if wideTermWidth {
|
||||
if layout.Compact {
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
formatBeaconDateDelta(lastCheckin, now, false, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, false, true),
|
||||
}
|
||||
} else {
|
||||
processName := beacon.Filename
|
||||
if layout.ProcessBasename {
|
||||
processName = targettable.ProcessName(processName)
|
||||
}
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
@@ -198,56 +241,69 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.RemoteAddress),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s (%d)", processName, beacon.PID)),
|
||||
}
|
||||
|
||||
if windowsBeaconInList {
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Integrity))
|
||||
if includeIntegrity {
|
||||
rowEntries = append(rowEntries, style.Render(integrity))
|
||||
}
|
||||
|
||||
rowEntries = append(rowEntries, []string{
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
style.Render(beacon.Locale),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
}...)
|
||||
} else {
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)))
|
||||
if layout.IncludeLocale {
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Locale))
|
||||
}
|
||||
rowEntries = append(rowEntries,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, true),
|
||||
)
|
||||
}
|
||||
// Build the row struct
|
||||
|
||||
row := table.Row{}
|
||||
for _, entry := range rowEntries {
|
||||
row = append(row, entry)
|
||||
}
|
||||
// Apply filters if any
|
||||
if filter == "" && filterRegex == nil {
|
||||
tw.AppendRow(row)
|
||||
} else {
|
||||
for _, rowEntry := range rowEntries {
|
||||
if filter != "" {
|
||||
if strings.Contains(rowEntry, filter) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
if filterRegex != nil {
|
||||
if filterRegex.MatchString(rowEntry) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
return tw
|
||||
}
|
||||
|
||||
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
|
||||
if filter == "" && filterRegex == nil {
|
||||
return true
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if filter != "" && strings.Contains(entry, filter) {
|
||||
return true
|
||||
}
|
||||
if filterRegex != nil && filterRegex.MatchString(entry) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
|
||||
date := timestamp.Format(time.UnixDate)
|
||||
interval := ""
|
||||
if timestamp.Before(now) {
|
||||
delta := now.Sub(timestamp).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (%s ago)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldRed.Render(interval)
|
||||
}
|
||||
} else {
|
||||
delta := timestamp.Sub(now).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (in %s)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldGreen.Render(interval)
|
||||
}
|
||||
}
|
||||
return interval
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package beacons
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
|
||||
|
||||
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
|
||||
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
beacon *clientpb.Beacon
|
||||
hasIntegrity bool
|
||||
wantProcess string
|
||||
}{
|
||||
{
|
||||
name: "windows path",
|
||||
beacon: testWindowsBeacon(),
|
||||
hasIntegrity: true,
|
||||
wantProcess: "winterfell.exe (2020)",
|
||||
},
|
||||
{
|
||||
name: "posix path",
|
||||
beacon: testLinuxBeacon(),
|
||||
hasIntegrity: false,
|
||||
wantProcess: "winterfell-agent (2020)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
beacons := []*clientpb.Beacon{tt.beacon}
|
||||
fullLayout := targettable.Layout{
|
||||
IncludeIntegrity: tt.hasIntegrity,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameLayout := fullLayout
|
||||
basenameLayout.ProcessBasename = true
|
||||
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
|
||||
if basenameWidth >= fullWidth {
|
||||
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
|
||||
}
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
|
||||
if !strings.Contains(rendered, tt.wantProcess) {
|
||||
t.Fatalf("process basename missing from table:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, tt.beacon.Filename) {
|
||||
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, basenameWidth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
withAll := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
withoutIntegrity := withAll
|
||||
withoutIntegrity.IncludeIntegrity = false
|
||||
withoutLocale := withoutIntegrity
|
||||
withoutLocale.IncludeLocale = false
|
||||
|
||||
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
|
||||
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
|
||||
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
|
||||
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
|
||||
|
||||
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
|
||||
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
|
||||
beacon := testLinuxBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
layout := targettable.Layout{IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, beacons, layout, false)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
assertHeaderPresence(t, rendered, "Locale", true)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
basenameLayout := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
|
||||
withoutLocale := basenameLayout
|
||||
withoutLocale.IncludeIntegrity = false
|
||||
withoutLocale.IncludeLocale = false
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
|
||||
t.Run("full process path survives basename projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, `AppData\Local\Temp`) {
|
||||
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hidden integrity survives column projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
|
||||
}
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.Integrity = ""
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if beacon.Integrity != "" {
|
||||
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
|
||||
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
|
||||
if compactWidth >= withoutOptionalWidth {
|
||||
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
|
||||
}
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
|
||||
|
||||
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
|
||||
assertHeaderPresence(t, rendered, omitted, false)
|
||||
}
|
||||
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
|
||||
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
|
||||
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, compactWidth)
|
||||
}
|
||||
|
||||
func testWindowsBeacon() *clientpb.Beacon {
|
||||
return &clientpb.Beacon{
|
||||
ID: "0c9f6b27-1111-2222-3333-444444444444",
|
||||
Name: "winterfell-session",
|
||||
TasksCount: 3,
|
||||
TasksCountCompleted: 2,
|
||||
Transport: "mtls",
|
||||
RemoteAddress: "10.13.37.11:49760",
|
||||
Hostname: "winterfell",
|
||||
Username: `winterfell\vagrant`,
|
||||
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
|
||||
PID: 2020,
|
||||
Integrity: "High",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Locale: "en-US",
|
||||
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
|
||||
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func testLinuxBeacon() *clientpb.Beacon {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.OS = "linux"
|
||||
beacon.Hostname = "winterfell-linux"
|
||||
beacon.Username = "vagrant"
|
||||
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
|
||||
beacon.Integrity = ""
|
||||
return beacon
|
||||
}
|
||||
|
||||
func testBeaconConsole() *console.SliverClient {
|
||||
return &console.SliverClient{
|
||||
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
|
||||
}
|
||||
}
|
||||
|
||||
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
|
||||
t.Helper()
|
||||
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
|
||||
return text.LongestLineLen(tw.Render())
|
||||
}
|
||||
|
||||
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
|
||||
t.Helper()
|
||||
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
|
||||
return text.StripEscape(tw.Render())
|
||||
}
|
||||
|
||||
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
|
||||
t.Helper()
|
||||
if got := text.LongestLineLen(rendered); got > width {
|
||||
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
|
||||
t.Helper()
|
||||
headerLine := strings.SplitN(rendered, "\n", 2)[0]
|
||||
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
|
||||
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ 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")
|
||||
@@ -145,8 +145,8 @@ func filterCompletionValues(values []string, prefix string) []string {
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
|
||||
@@ -28,11 +28,11 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
|
||||
@@ -84,8 +84,8 @@ func filterCompletionValues(values []string, prefix string) []string {
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as crack files.
|
||||
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 and renders detailed task progress, device telemetry, and recovered credentials.
|
||||
- `crack-top.go` – Runs the full-screen Bubble Tea monitor, including responsive job/worker panes and a Huh-backed status filter.
|
||||
- `crack-top-loader.go` – Loads one lean, target-free snapshot containing every active job, bounded recent history, and connected crackstation telemetry.
|
||||
- `crack-top-telemetry.go` – Calculates weighted shard progress plus per-worker and global live hash rates without treating cached benchmarks as current throughput.
|
||||
- `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.
|
||||
@@ -1,6 +1,8 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -25,8 +27,15 @@ 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,
|
||||
@@ -41,6 +50,62 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
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")
|
||||
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)
|
||||
crackCmd.AddCommand(crackJobCmd)
|
||||
|
||||
wordlistsCmd := &cobra.Command{
|
||||
Use: consts.WordlistsStr,
|
||||
Short: "Manage wordlists",
|
||||
|
||||
@@ -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)})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -10,6 +12,38 @@ import (
|
||||
"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
|
||||
@@ -17,16 +51,25 @@ func shouldRunCrack(cmd *cobra.Command, args []string) bool {
|
||||
|
||||
hasFlag := false
|
||||
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
if flag.Changed && flag.Name != "timeout" {
|
||||
hasFlag = true
|
||||
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{}
|
||||
req := &clientpb.CrackCommand{HashType: clientpb.HashType_INVALID}
|
||||
|
||||
if flags.Changed("attack-mode") {
|
||||
raw, _ := flags.GetString("attack-mode")
|
||||
@@ -45,12 +88,39 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
}
|
||||
req.HashType = hashType
|
||||
}
|
||||
if flags.Changed("hash-mode") {
|
||||
hashMode, _ := flags.GetUint32("hash-mode")
|
||||
req.HashMode = &hashMode
|
||||
}
|
||||
|
||||
hashes, _ := flags.GetStringSlice("hash")
|
||||
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")
|
||||
@@ -58,10 +128,19 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
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")
|
||||
@@ -81,7 +160,13 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
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)
|
||||
@@ -91,11 +176,15 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
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.Stdout, _ = flags.GetBool("stdout")
|
||||
req.Show, _ = flags.GetBool("show")
|
||||
req.Left, _ = flags.GetBool("left")
|
||||
req.Username, _ = flags.GetBool("username")
|
||||
@@ -108,48 +197,107 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
|
||||
if flags.Changed("encoding-from") {
|
||||
raw, _ := flags.GetString("encoding-from")
|
||||
enc, err := parseCrackEncoding(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingFrom = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingFromName = raw
|
||||
}
|
||||
req.EncodingFrom = enc
|
||||
}
|
||||
if flags.Changed("encoding-to") {
|
||||
raw, _ := flags.GetString("encoding-to")
|
||||
enc, err := parseCrackEncoding(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingTo = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingToName = raw
|
||||
}
|
||||
req.EncodingTo = enc
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
req.KeyboardLayoutMapping = []byte(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.SegmentSize, _ = flags.GetUint32("segment-size")
|
||||
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-disabled")
|
||||
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")
|
||||
@@ -167,37 +315,114 @@ func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackComman
|
||||
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")
|
||||
|
||||
if raw, _ := flags.GetString("rules-file"); raw != "" {
|
||||
req.RulesFile = []byte(raw)
|
||||
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-fun-min")
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-fun-max")
|
||||
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
|
||||
}
|
||||
@@ -214,6 +439,42 @@ func uintSliceToUint32(flags *pflag.FlagSet, name string) []uint32 {
|
||||
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
|
||||
@@ -231,14 +492,20 @@ func parseCrackAttackMode(raw string) (clientpb.CrackAttackMode, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const crackFileScheme = "crackfile://"
|
||||
|
||||
type crackFilesListFunc func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error)
|
||||
|
||||
// resolveManagedCrackFileReferences replaces exact server-side crack file
|
||||
// names, IDs, and digests with the content-addressed URI understood by a
|
||||
// crackstation. Inputs that do not match a managed file remain ordinary
|
||||
// Hashcat operands, masks, paths, or inline rule content.
|
||||
//
|
||||
//nolint:gocyclo // Resolve every managed-file role together in one deterministic pass.
|
||||
func resolveManagedCrackFileReferences(ctx context.Context, command *clientpb.CrackCommand, list crackFilesListFunc) error {
|
||||
if command == nil {
|
||||
return fmt.Errorf("missing crack command")
|
||||
}
|
||||
if list == nil {
|
||||
return fmt.Errorf("missing crack file lister")
|
||||
}
|
||||
|
||||
wordlistIndexes := make([]int, 0, len(command.PositionalArguments))
|
||||
wordlistInputs := make([]string, 0, len(command.PositionalArguments))
|
||||
for index, argument := range command.PositionalArguments {
|
||||
// Only wordlist-role operands may be resolved by alias. An inline mask can
|
||||
// legitimately have the same text as a managed file name and must remain
|
||||
// an inline mask. Explicit crackfile URIs are still validated here so a
|
||||
// mistyped managed reference fails at the client boundary.
|
||||
if !crackOperandIsWordlist(command.AttackMode, index) && !strings.HasPrefix(argument, crackFileScheme) {
|
||||
continue
|
||||
}
|
||||
wordlistIndexes = append(wordlistIndexes, index)
|
||||
wordlistInputs = append(wordlistInputs, argument)
|
||||
}
|
||||
if len(wordlistInputs) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, wordlistInputs, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, argument := range resolved {
|
||||
command.PositionalArguments[wordlistIndexes[index]] = argument
|
||||
}
|
||||
}
|
||||
legacyIdentify := command.Identify //nolint:staticcheck // Identify remains required for legacy wire compatibility.
|
||||
if len(command.PositionalArguments) == 0 && legacyIdentify != "" &&
|
||||
(crackOperandIsWordlist(command.AttackMode, 0) || strings.HasPrefix(legacyIdentify, crackFileScheme)) {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{legacyIdentify}, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.Identify = resolved[0] //nolint:staticcheck // Keep the resolved legacy operand synchronized for older servers.
|
||||
}
|
||||
|
||||
rules := command.RulesFilesV7
|
||||
if len(rules) == 0 && len(command.RulesFile) > 0 {
|
||||
rules = [][]byte{command.RulesFile}
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
values := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
values = append(values, string(rule))
|
||||
}
|
||||
resolved, err := resolveCrackFileValues(ctx, values, clientpb.CrackFileType_RULES, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.RulesFilesV7 = make([][]byte, 0, len(resolved))
|
||||
for _, rule := range resolved {
|
||||
command.RulesFilesV7 = append(command.RulesFilesV7, []byte(rule))
|
||||
}
|
||||
command.RulesFile = append([]byte(nil), command.RulesFilesV7[0]...)
|
||||
}
|
||||
|
||||
if len(command.MarkovHcstat2) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{string(command.MarkovHcstat2)}, clientpb.CrackFileType_MARKOV_HCSTAT2, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.MarkovHcstat2 = []byte(resolved[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackOperandIsWordlist(attackMode clientpb.CrackAttackMode, index int) bool {
|
||||
switch attackMode {
|
||||
case clientpb.CrackAttackMode_STRAIGHT, clientpb.CrackAttackMode_COMBINATION:
|
||||
return true
|
||||
case clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK:
|
||||
return index == 0
|
||||
case clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST:
|
||||
return index == 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Validation and alias disambiguation intentionally share the manifest lookup and preserve input ordering.
|
||||
func resolveCrackFileValues(ctx context.Context, values []string, fileType clientpb.CrackFileType, list crackFilesListFunc) ([]string, error) {
|
||||
needsManifest := false
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
if err := validateCrackFileURI(value, fileType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
needsManifest = true
|
||||
}
|
||||
if !needsManifest {
|
||||
return append([]string(nil), values...), nil
|
||||
}
|
||||
|
||||
manifest, err := list(ctx, &clientpb.CrackFile{Type: fileType})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list managed %s files: %w", crackFileTypeToken(fileType), err)
|
||||
}
|
||||
files := []*clientpb.CrackFile(nil)
|
||||
if manifest != nil {
|
||||
files = manifest.Files
|
||||
}
|
||||
resolved := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
matches := make([]*clientpb.CrackFile, 0, 1)
|
||||
for _, file := range files {
|
||||
if file == nil || file.Type != fileType {
|
||||
continue
|
||||
}
|
||||
if value == file.Name || value == file.ID || strings.EqualFold(value, file.Sha2_256) {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("managed %s file reference %q is ambiguous", crackFileTypeToken(fileType), value)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
uri, err := crackFileURI(matches[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, uri)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func crackFileURI(file *clientpb.CrackFile) (string, error) {
|
||||
if file == nil {
|
||||
return "", fmt.Errorf("missing managed crack file")
|
||||
}
|
||||
digest := strings.ToLower(strings.TrimSpace(file.Sha2_256))
|
||||
if err := validateCrackFileDigest(digest); err != nil {
|
||||
return "", fmt.Errorf("managed crack file %q has invalid SHA-256: %w", file.Name, err)
|
||||
}
|
||||
fileType := crackFileTypeToken(file.Type)
|
||||
if fileType == "" {
|
||||
return "", fmt.Errorf("managed crack file %q has unsupported type %s", file.Name, file.Type.String())
|
||||
}
|
||||
return crackFileScheme + fileType + "/" + digest, nil
|
||||
}
|
||||
|
||||
func validateCrackFileURI(value string, expectedType clientpb.CrackFileType) error {
|
||||
remainder := strings.TrimPrefix(value, crackFileScheme)
|
||||
parts := strings.Split(remainder, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return fmt.Errorf("invalid managed crack file URI %q", value)
|
||||
}
|
||||
if parts[0] != crackFileTypeToken(expectedType) {
|
||||
return fmt.Errorf("managed crack file URI %q has type %q, expected %q", value, parts[0], crackFileTypeToken(expectedType))
|
||||
}
|
||||
if parts[1] != strings.ToLower(parts[1]) {
|
||||
return fmt.Errorf("managed crack file URI %q must use a lowercase SHA-256", value)
|
||||
}
|
||||
if err := validateCrackFileDigest(parts[1]); err != nil {
|
||||
return fmt.Errorf("invalid managed crack file URI %q: %w", value, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrackFileDigest(value string) error {
|
||||
if len(value) != 64 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
decoded, err := hex.DecodeString(value)
|
||||
if err != nil || len(decoded) != 32 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackFileTypeToken(fileType clientpb.CrackFileType) string {
|
||||
switch fileType {
|
||||
case clientpb.CrackFileType_WORDLIST:
|
||||
return "wordlist"
|
||||
case clientpb.CrackFileType_RULES:
|
||||
return "rules"
|
||||
case clientpb.CrackFileType_MARKOV_HCSTAT2:
|
||||
return "hcstat2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestResolveManagedCrackFileReferences(t *testing.T) {
|
||||
wordlistDigest := strings.Repeat("a", 64)
|
||||
rulesDigest := strings.Repeat("b", 64)
|
||||
hcstat2Digest := strings.Repeat("c", 64)
|
||||
files := map[clientpb.CrackFileType][]*clientpb.CrackFile{
|
||||
clientpb.CrackFileType_WORDLIST: {{ID: "wordlist-id", Name: "rockyou.txt", Sha2_256: wordlistDigest, Type: clientpb.CrackFileType_WORDLIST}},
|
||||
clientpb.CrackFileType_RULES: {{ID: "rules-id", Name: "best64.rule", Sha2_256: rulesDigest, Type: clientpb.CrackFileType_RULES}},
|
||||
clientpb.CrackFileType_MARKOV_HCSTAT2: {{
|
||||
ID: "hcstat2-id", Name: "hashcat.hcstat2", Sha2_256: hcstat2Digest, Type: clientpb.CrackFileType_MARKOV_HCSTAT2,
|
||||
}},
|
||||
}
|
||||
requested := make([]clientpb.CrackFileType, 0)
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
requested = append(requested, request.Type)
|
||||
return &clientpb.CrackFiles{Files: files[request.Type]}, nil
|
||||
}
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
PositionalArguments: []string{"rockyou.txt", "?d?d?d"},
|
||||
RulesFile: []byte("best64.rule"),
|
||||
RulesFilesV7: [][]byte{[]byte("best64.rule"), []byte(":")},
|
||||
MarkovHcstat2: []byte("hcstat2-id"),
|
||||
}
|
||||
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if want := []string{"crackfile://wordlist/" + wordlistDigest, "?d?d?d"}; !slices.Equal(command.PositionalArguments, want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, want)
|
||||
}
|
||||
wantRules := [][]byte{[]byte("crackfile://rules/" + rulesDigest), []byte(":")}
|
||||
if !slices.EqualFunc(command.RulesFilesV7, wantRules, func(left, right []byte) bool { return slices.Equal(left, right) }) {
|
||||
t.Fatalf("RulesFilesV7 = %#v, want %#v", command.RulesFilesV7, wantRules)
|
||||
}
|
||||
if !slices.Equal(command.RulesFile, wantRules[0]) {
|
||||
t.Fatalf("RulesFile = %q", command.RulesFile)
|
||||
}
|
||||
if want := "crackfile://hcstat2/" + hcstat2Digest; string(command.MarkovHcstat2) != want {
|
||||
t.Fatalf("MarkovHcstat2 = %q, want %q", command.MarkovHcstat2, want)
|
||||
}
|
||||
if want := []clientpb.CrackFileType{clientpb.CrackFileType_WORDLIST, clientpb.CrackFileType_RULES, clientpb.CrackFileType_MARKOV_HCSTAT2}; !slices.Equal(requested, want) {
|
||||
t.Fatalf("requested types = %#v, want %#v", requested, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRespectsAttackOperandRoles(t *testing.T) {
|
||||
digestA := strings.Repeat("a", 64)
|
||||
digestB := strings.Repeat("b", 64)
|
||||
files := []*clientpb.CrackFile{
|
||||
{ID: "wordlist-a", Name: "first", Sha2_256: digestA, Type: clientpb.CrackFileType_WORDLIST},
|
||||
{ID: "wordlist-b", Name: "second", Sha2_256: digestB, Type: clientpb.CrackFileType_WORDLIST},
|
||||
}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
if request.Type != clientpb.CrackFileType_WORDLIST {
|
||||
t.Fatalf("requested file type = %s, want WORDLIST", request.Type)
|
||||
}
|
||||
return &clientpb.CrackFiles{Files: files}, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attackMode clientpb.CrackAttackMode
|
||||
inputs []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "bruteforce mask collision",
|
||||
attackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
inputs: []string{"first"},
|
||||
want: []string{"first"},
|
||||
},
|
||||
{
|
||||
name: "wordlist mask hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "second"},
|
||||
},
|
||||
{
|
||||
name: "mask wordlist hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"first", "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "straight wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_STRAIGHT,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "combination wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_COMBINATION,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: test.attackMode,
|
||||
PositionalArguments: append([]string(nil), test.inputs...),
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if !slices.Equal(command.PositionalArguments, test.want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesPreservesInlineBytes(t *testing.T) {
|
||||
inline := []byte{0, 1, 2, 0xff}
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{append([]byte(nil), inline...)}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(command.RulesFilesV7) != 1 || !slices.Equal(command.RulesFilesV7[0], inline) {
|
||||
t.Fatalf("inline rules changed: %#v", command.RulesFilesV7)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesAcceptsCanonicalURIWithoutListing(t *testing.T) {
|
||||
uri := "crackfile://wordlist/" + strings.Repeat("d", 64)
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{uri}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
t.Fatal("canonical URI should not require a manifest lookup")
|
||||
return nil, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if command.PositionalArguments[0] != uri {
|
||||
t.Fatalf("URI changed to %q", command.PositionalArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsWrongURIType(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{[]byte("crackfile://wordlist/" + strings.Repeat("e", 64))}}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return nil, errors.New("must not be called")
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "expected \"rules\"") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsAmbiguousName(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{"duplicate.txt"}}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("a", 64), Type: request.Type},
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("b", 64), Type: request.Type},
|
||||
}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "ambiguous") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsInvalidDigest(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{MarkovHcstat2: []byte("broken.hcstat2")}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{{
|
||||
Name: "broken.hcstat2", Sha2_256: "not-a-digest", Type: request.Type,
|
||||
}}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid SHA-256") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -22,18 +22,36 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func localCrackFileInfo(path string) (os.FileInfo, error) {
|
||||
return localCrackFileInfoWith(path, os.Stat)
|
||||
}
|
||||
|
||||
func localCrackFileInfoWith(path string, stat func(string) (os.FileInfo, error)) (os.FileInfo, error) {
|
||||
info, err := stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat local crack file %q: %w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("local crack file %q is a directory", path)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// CrackWordlistsCmd - Manage GPU cracking stations.
|
||||
func CrackWordlistsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
@@ -173,9 +191,9 @@ func CrackWordlistsAddCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
wordlistStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || wordlistStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
wordlistStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid wordlist file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -217,9 +235,9 @@ func CrackRulesAddCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
rulesStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || rulesStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
rulesStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid rules file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -261,9 +279,9 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
hcstat2Stat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || hcstat2Stat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
hcstat2Stat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid hcstat2 file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -294,102 +312,176 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
}
|
||||
|
||||
func addCrackFile(localFile *os.File, crackFile *clientpb.CrackFile, con *console.SliverClient) {
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
|
||||
var chunkReaderErr error
|
||||
go func() {
|
||||
chunkReaderErr = chunkReader(wordlistReader, crackFile.ChunkSize, chunks)
|
||||
}()
|
||||
n := uint32(0)
|
||||
total := int64(0)
|
||||
errors := []error{}
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
total, err := uploadCrackFile(localFile, crackFile, con.Rpc, func(total int64, n uint32, chunkSize int) {
|
||||
con.PrintInfof("Uploading %s (chunk %d - %s) ...",
|
||||
util.ByteCountBinary(total),
|
||||
n,
|
||||
util.ByteCountBinary(int64(len(chunk))),
|
||||
util.ByteCountBinary(int64(chunkSize)),
|
||||
)
|
||||
_, err := con.Rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
n++
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if chunkReaderErr != nil {
|
||||
con.PrintErrorf("Failed to read file: %s\n", chunkReaderErr)
|
||||
return
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
con.PrintErrorf("Failed to upload chunk: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := con.Rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to complete file upload: %s\n", err)
|
||||
con.PrintErrorf("Failed to upload crack file: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Upload completed (compressed: %s)\n", util.ByteCountBinary(total))
|
||||
}
|
||||
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) error {
|
||||
func uploadCrackFile(localFile io.Reader, crackFile *clientpb.CrackFile, rpc rpcpb.SliverRPCClient, progress func(total int64, n uint32, chunkSize int)) (total int64, retErr error) {
|
||||
if localFile == nil {
|
||||
return 0, errors.New("local crack file reader is nil")
|
||||
}
|
||||
if crackFile == nil || crackFile.ID == "" {
|
||||
return 0, errors.New("crack file reservation is missing an ID")
|
||||
}
|
||||
if rpc == nil {
|
||||
return 0, errors.New("crack file RPC client is nil")
|
||||
}
|
||||
|
||||
completed := false
|
||||
defer func() {
|
||||
if retErr == nil || completed {
|
||||
return
|
||||
}
|
||||
_, cleanupErr := rpc.CrackFileDelete(context.Background(), &clientpb.CrackFile{ID: crackFile.ID})
|
||||
if cleanupErr != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("failed to remove crack file reservation %s: %w", crackFile.ID, cleanupErr))
|
||||
}
|
||||
}()
|
||||
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks, chunkReaderErrors := readCrackFileChunks(wordlistReader, crackFile.ChunkSize)
|
||||
n := uint32(0)
|
||||
var uploadErr error
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
if uploadErr == nil {
|
||||
if progress != nil {
|
||||
progress(total, n, len(chunk))
|
||||
}
|
||||
_, err := rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
if err != nil {
|
||||
uploadErr = fmt.Errorf("upload chunk %d: %w", n, err)
|
||||
}
|
||||
}
|
||||
n++
|
||||
}
|
||||
chunkReaderErr := <-chunkReaderErrors
|
||||
if chunkReaderErr != nil {
|
||||
uploadErr = errors.Join(uploadErr, fmt.Errorf("read and compress crack file: %w", chunkReaderErr))
|
||||
}
|
||||
if uploadErr != nil {
|
||||
return total, uploadErr
|
||||
}
|
||||
_, err := rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
if err != nil {
|
||||
return total, fmt.Errorf("complete crack file upload: %w", err)
|
||||
}
|
||||
completed = true
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func readCrackFileChunks(reader io.Reader, chunkSize int64) (<-chan []byte, <-chan error) {
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
errors := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errors)
|
||||
errors <- chunkReader(reader, chunkSize, chunks)
|
||||
}()
|
||||
return chunks, errors
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Compression, fixed-size emission, final remainder handling, and cleanup form one streaming state machine.
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) (retErr error) {
|
||||
defer close(chunks)
|
||||
if chunkSize < 1 {
|
||||
return fmt.Errorf("invalid crack file chunk size %d", chunkSize)
|
||||
}
|
||||
start := int64(0)
|
||||
tmpFile, err := os.CreateTemp("", "sliver-wordlist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
compressor, _ := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
for {
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return err
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
if err := tmpFile.Close(); retErr == nil && err != nil {
|
||||
retErr = err
|
||||
}
|
||||
_ = os.Remove(tmpPath)
|
||||
}()
|
||||
compressor, err := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed := false
|
||||
defer func() {
|
||||
if !compressorClosed {
|
||||
_ = compressor.Close()
|
||||
}
|
||||
}()
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
for {
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readN != 0 {
|
||||
_, err := compressor.Write(readBuf[:readN])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressor.Flush()
|
||||
tmpFileStat, err := os.Stat(tmpFile.Name())
|
||||
if err := compressor.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tmpFileStat.Size()-start >= chunkSize {
|
||||
stop := start + chunkSize
|
||||
for tmpFileStat.Size()-start >= chunkSize {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start = stop
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
}
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return readErr
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
return nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := compressor.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed = true
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for start < tmpFileStat.Size() {
|
||||
remaining := tmpFileStat.Size() - start
|
||||
readSize := chunkSize
|
||||
if remaining < readSize {
|
||||
readSize = remaining
|
||||
}
|
||||
chunk, err := readChunkAt(tmpFile, start, readSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readChunkAt(tmpFile *os.File, offset int64, chunkSize int64) ([]byte, error) {
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestLocalCrackFileInfoHandlesStatErrors(t *testing.T) {
|
||||
wantErr := errors.New("permission denied")
|
||||
info, err := localCrackFileInfoWith("/restricted/file", func(string) (os.FileInfo, error) {
|
||||
return nil, wantErr
|
||||
})
|
||||
if info != nil {
|
||||
t.Fatalf("localCrackFileInfoWith info = %#v, want nil", info)
|
||||
}
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("localCrackFileInfoWith error = %v, want wrapped %v", err, wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/restricted/file") {
|
||||
t.Fatalf("localCrackFileInfoWith error = %q, want path context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoRejectsDirectories(t *testing.T) {
|
||||
_, err := localCrackFileInfo(t.TempDir())
|
||||
if err == nil || !strings.Contains(err.Error(), "is a directory") {
|
||||
t.Fatalf("localCrackFileInfo directory error = %v, want directory error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoAcceptsRegularFile(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "crack-file-")
|
||||
if err != nil {
|
||||
t.Fatalf("create local crack file: %v", err)
|
||||
}
|
||||
const payload = "hashcat input"
|
||||
if _, err := file.WriteString(payload); err != nil {
|
||||
t.Fatalf("write local crack file: %v", err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatalf("close local crack file: %v", err)
|
||||
}
|
||||
|
||||
info, err := localCrackFileInfo(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("localCrackFileInfo: %v", err)
|
||||
}
|
||||
if info.Size() != int64(len(payload)) {
|
||||
t.Fatalf("localCrackFileInfo size = %d, want %d", info.Size(), len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileCleansUpPostCreateFailures(t *testing.T) {
|
||||
readErr := errors.New("read failed")
|
||||
chunkErr := errors.New("chunk upload failed")
|
||||
completeErr := errors.New("complete failed")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reader io.Reader
|
||||
configureRPC func(*fakeCrackFileUploadRPC)
|
||||
wantErr error
|
||||
wantChunkCalls int
|
||||
wantCompleteCalls int
|
||||
}{
|
||||
{
|
||||
name: "read failure",
|
||||
reader: errorReader{err: readErr},
|
||||
wantErr: readErr,
|
||||
},
|
||||
{
|
||||
name: "chunk upload failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.chunkErr = chunkErr
|
||||
},
|
||||
wantErr: chunkErr,
|
||||
wantChunkCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "completion failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.completeErr = completeErr
|
||||
},
|
||||
wantErr: completeErr,
|
||||
wantChunkCalls: 1,
|
||||
wantCompleteCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
if test.configureRPC != nil {
|
||||
test.configureRPC(rpc)
|
||||
}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(test.reader, reservation, rpc, nil)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped %v", err, test.wantErr)
|
||||
}
|
||||
if rpc.chunkCalls != test.wantChunkCalls {
|
||||
t.Fatalf("CrackFileChunkUpload calls = %d, want %d", rpc.chunkCalls, test.wantChunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != test.wantCompleteCalls {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want %d", rpc.completeCalls, test.wantCompleteCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
if rpc.deleted == nil || rpc.deleted.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileDelete request = %#v, want ID %q", rpc.deleted, reservation.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileReportsCleanupFailure(t *testing.T) {
|
||||
uploadErr := errors.New("chunk upload failed")
|
||||
cleanupErr := errors.New("delete failed")
|
||||
rpc := &fakeCrackFileUploadRPC{chunkErr: uploadErr, deleteErr: cleanupErr}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(bytes.NewReader([]byte("payload")), reservation, rpc, nil)
|
||||
if !errors.Is(err, uploadErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped upload error %v", err, uploadErr)
|
||||
}
|
||||
if !errors.Is(err, cleanupErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped cleanup error %v", err, cleanupErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), reservation.ID) {
|
||||
t.Fatalf("uploadCrackFile error = %q, want reservation ID", err)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileSuccessCompletesWithoutCleanup(t *testing.T) {
|
||||
payload := []byte("correct horse battery staple\n")
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 8}
|
||||
progressCalls := 0
|
||||
|
||||
total, err := uploadCrackFile(bytes.NewReader(payload), reservation, rpc, func(int64, uint32, int) {
|
||||
progressCalls++
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("uploadCrackFile: %v", err)
|
||||
}
|
||||
if total == 0 {
|
||||
t.Fatal("uploadCrackFile compressed byte count = 0, want non-zero")
|
||||
}
|
||||
if rpc.chunkCalls == 0 {
|
||||
t.Fatal("CrackFileChunkUpload calls = 0, want at least 1")
|
||||
}
|
||||
if progressCalls != rpc.chunkCalls {
|
||||
t.Fatalf("progress calls = %d, want %d", progressCalls, rpc.chunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != 1 {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want 1", rpc.completeCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 0 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 0", rpc.deleteCalls)
|
||||
}
|
||||
wantDigest := sha256.Sum256(payload)
|
||||
if rpc.completed == nil || rpc.completed.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileComplete request = %#v, want ID %q", rpc.completed, reservation.ID)
|
||||
}
|
||||
if rpc.completed.Sha2_256 != hex.EncodeToString(wantDigest[:]) {
|
||||
t.Fatalf("CrackFileComplete digest = %q, want %q", rpc.completed.Sha2_256, hex.EncodeToString(wantDigest[:]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderZstdRoundTrip(t *testing.T) {
|
||||
largePayload := make([]byte, 256*1024)
|
||||
if _, err := rand.New(rand.NewSource(42)).Read(largePayload); err != nil {
|
||||
t.Fatalf("create test payload: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
chunkSize int64
|
||||
wantMinChunks int
|
||||
}{
|
||||
{
|
||||
name: "single chunk",
|
||||
payload: []byte("correct horse battery staple\n"),
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 1,
|
||||
},
|
||||
{
|
||||
name: "many chunks from one read",
|
||||
payload: largePayload,
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader(test.payload), test.chunkSize)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
if chunkCount < test.wantMinChunks {
|
||||
t.Fatalf("chunk count = %d, want at least %d", chunkCount, test.wantMinChunks)
|
||||
}
|
||||
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, test.payload) {
|
||||
t.Fatalf("decoded payload does not match input: got %d bytes, want %d", len(decoded), len(test.payload))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderHandlesDataWithEOF(t *testing.T) {
|
||||
payload := []byte("reader returns data and EOF together")
|
||||
compressed, _, err := collectCompressedCrackFile(&dataEOFReader{data: payload}, 8)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, payload) {
|
||||
t.Fatalf("decoded payload = %q, want %q", decoded, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderRejectsInvalidChunkSize(t *testing.T) {
|
||||
for _, chunkSize := range []int64{0, -1} {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader([]byte("payload")), chunkSize)
|
||||
if err == nil {
|
||||
t.Fatalf("chunkReader accepted chunk size %d", chunkSize)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("invalid chunk size %d emitted %d chunks (%d bytes)", chunkSize, chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderPropagatesReadError(t *testing.T) {
|
||||
wantErr := errors.New("read failed")
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(errorReader{err: wantErr}, 1024)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("chunkReader error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("failed read emitted %d chunks (%d bytes)", chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
|
||||
func collectCompressedCrackFile(reader io.Reader, chunkSize int64) ([]byte, int, error) {
|
||||
chunks, errCh := readCrackFileChunks(reader, chunkSize)
|
||||
|
||||
compressed := bytes.Buffer{}
|
||||
chunkCount := 0
|
||||
invalidChunk := false
|
||||
for chunk := range chunks {
|
||||
if len(chunk) == 0 || int64(len(chunk)) > chunkSize {
|
||||
invalidChunk = true
|
||||
}
|
||||
_, _ = compressed.Write(chunk)
|
||||
chunkCount++
|
||||
}
|
||||
readerErr := <-errCh
|
||||
if invalidChunk {
|
||||
return nil, chunkCount, errors.New("chunkReader emitted an invalid chunk size")
|
||||
}
|
||||
return compressed.Bytes(), chunkCount, readerErr
|
||||
}
|
||||
|
||||
type dataEOFReader struct {
|
||||
data []byte
|
||||
done bool
|
||||
}
|
||||
|
||||
func (r *dataEOFReader) Read(buffer []byte) (int, error) {
|
||||
if r.done {
|
||||
return 0, io.EOF
|
||||
}
|
||||
r.done = true
|
||||
return copy(buffer, r.data), io.EOF
|
||||
}
|
||||
|
||||
type errorReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r errorReader) Read([]byte) (int, error) {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
type fakeCrackFileUploadRPC struct {
|
||||
rpcpb.SliverRPCClient
|
||||
|
||||
chunkErr error
|
||||
completeErr error
|
||||
deleteErr error
|
||||
|
||||
chunkCalls int
|
||||
completeCalls int
|
||||
deleteCalls int
|
||||
|
||||
uploaded []*clientpb.CrackFileChunk
|
||||
completed *clientpb.CrackFile
|
||||
deleted *clientpb.CrackFile
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileChunkUpload(_ context.Context, request *clientpb.CrackFileChunk, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.chunkCalls++
|
||||
f.uploaded = append(f.uploaded, &clientpb.CrackFileChunk{
|
||||
CrackFileID: request.CrackFileID,
|
||||
N: request.N,
|
||||
Data: append([]byte(nil), request.Data...),
|
||||
})
|
||||
if f.chunkErr != nil {
|
||||
return nil, f.chunkErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileComplete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.completeCalls++
|
||||
f.completed = &clientpb.CrackFile{ID: request.ID, Sha2_256: request.Sha2_256}
|
||||
if f.completeErr != nil {
|
||||
return nil, f.completeErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileDelete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.deleteCalls++
|
||||
f.deleted = &clientpb.CrackFile{ID: request.ID}
|
||||
if f.deleteErr != nil {
|
||||
return nil, f.deleteErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
@@ -3,15 +3,22 @@ package crack
|
||||
import "github.com/spf13/pflag"
|
||||
|
||||
func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.String("attack-mode", "", "crack attack mode (straight, combination, bruteforce, hybrid-wordlist-mask, hybrid-mask-wordlist, association, no-attack, or numeric)")
|
||||
f.String("hash-type", "", "hashcat hash type (number or enum name)")
|
||||
f.StringSlice("hash", nil, "hashes to crack (repeatable)")
|
||||
f.StringP("attack-mode", "a", "", "crack attack mode (straight, combination, bruteforce, pcfg, hybrid-wordlist-mask, hybrid-mask-wordlist, generic, association, no-attack, hybrid, or numeric)")
|
||||
f.StringP("hash-type", "m", "", "hashcat hash type (number or enum name)")
|
||||
f.Uint32("hash-mode", 0, "canonical numeric hashcat mode (overrides hash-type)")
|
||||
f.StringArray("hash", nil, "hashes to crack (repeatable)")
|
||||
f.StringArray("credential", nil, "server credential ID or ID prefix to crack (repeatable)")
|
||||
f.String("credential-collection", "", "server credential collection to crack")
|
||||
f.Bool("include-cracked-credentials", false, "include credentials already marked as cracked")
|
||||
f.StringArray("input", nil, "ordered positional hashcat inputs (repeatable)")
|
||||
f.String("stdin", "", "data supplied to hashcat standard input")
|
||||
f.Bool("quiet", false, "quiet mode")
|
||||
f.Bool("hex-charset", false, "treat charset as hex")
|
||||
f.Bool("hex-salt", false, "treat salt as hex")
|
||||
f.Bool("hex-wordlist", false, "treat wordlist as hex")
|
||||
f.Bool("force", false, "ignore warnings")
|
||||
f.Bool("deprecated-check-disable", false, "disable deprecated check")
|
||||
f.Bool("advice-disable", false, "disable hashcat advice")
|
||||
f.Bool("status", false, "enable status")
|
||||
f.Bool("status-json", false, "emit status in JSON")
|
||||
f.Uint32("status-timer", 0, "status timer interval (seconds)")
|
||||
@@ -30,12 +37,15 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Bool("restore", false, "restore session")
|
||||
f.Bool("restore-disable", false, "disable restore")
|
||||
f.String("restore-file", "", "restore file path or identifier")
|
||||
f.Bool("restore-position", false, "restore from the stored position")
|
||||
f.Bool("restore-show-command", false, "display the stored restore command and stop")
|
||||
f.String("outfile", "", "outfile path or identifier")
|
||||
f.StringSlice("outfile-format", nil, "outfile formats (hash-salt, plain, hex-plain, crack-pos, timestamp-absolute, timestamp-relative, or numeric)")
|
||||
f.Bool("outfile-json", false, "write outfile records as JSON")
|
||||
f.Bool("outfile-autohex-disable", false, "disable outfile autohex")
|
||||
f.Uint32("outfile-check-timer", 0, "outfile check timer interval (seconds)")
|
||||
f.Bool("wordlist-autohex-disable", false, "disable wordlist autohex")
|
||||
f.String("separator", "", "separator character")
|
||||
f.Bool("stdout", false, "stdout output")
|
||||
f.Bool("show", false, "show cracked hashes")
|
||||
f.Bool("left", false, "show uncracked hashes")
|
||||
f.Bool("username", false, "enable username handling")
|
||||
@@ -43,33 +53,52 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Uint32("remove-timer", 0, "remove timer interval (seconds)")
|
||||
f.Bool("potfile-disable", false, "disable potfile")
|
||||
f.String("potfile", "", "potfile path or identifier")
|
||||
f.String("encoding-from", "", "input encoding (iso-8859-15, utf-32le, or numeric)")
|
||||
f.String("encoding-to", "", "output encoding (iso-8859-15, utf-32le, or numeric)")
|
||||
f.String("encoding-from", "", "input encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.String("encoding-to", "", "output encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.Uint32("debug-mode", 0, "debug mode mask")
|
||||
f.String("debug-file", "", "debug file path or identifier")
|
||||
f.String("induction-dir", "", "induction directory path or identifier")
|
||||
f.String("outfile-check-dir", "", "outfile-check directory path or identifier")
|
||||
f.Bool("logfile-disable", false, "disable logfile")
|
||||
f.Uint32("hccapx-message-pair", 0, "hccapx message pair")
|
||||
f.Uint32("nonce-error-corrections", 0, "nonce error corrections")
|
||||
f.String("keyboard-layout-mapping", "", "keyboard layout mapping file or identifier")
|
||||
f.String("keyboard-layout-mapping", "", "local keyboard layout mapping file")
|
||||
f.String("truecrypt-keyfiles", "", "TrueCrypt keyfiles path or identifier")
|
||||
f.String("veracrypt-keyfiles", "", "VeraCrypt keyfiles path or identifier")
|
||||
f.Uint32("veracrypt-pim-start", 0, "VeraCrypt PIM start")
|
||||
f.Uint32("veracrypt-pim-stop", 0, "VeraCrypt PIM stop")
|
||||
f.Bool("benchmark", false, "run benchmark")
|
||||
f.Bool("benchmark-all", false, "run full benchmark")
|
||||
f.Uint32("benchmark-min", 0, "minimum benchmark runtime")
|
||||
f.Uint32("benchmark-max", 0, "maximum benchmark runtime")
|
||||
f.Bool("speed-only", false, "show speed only")
|
||||
f.Bool("progress-only", false, "show progress only")
|
||||
f.Uint32("segment-size", 0, "segment size")
|
||||
f.Uint32("segment-size", 0, "deprecated: removed in Hashcat v7")
|
||||
f.Bool("pipeline-stats", false, "show pipeline statistics")
|
||||
f.Bool("task-time-breakdown", false, "show task time breakdown")
|
||||
f.Uint32("bitmap-min", 0, "bitmap min")
|
||||
f.Uint32("bitmap-max", 0, "bitmap max")
|
||||
f.UintSlice("cpu-affinity", nil, "CPU affinity list")
|
||||
f.Uint32("hook-threads", 0, "hook threads")
|
||||
f.Bool("hash-info", false, "show hash information")
|
||||
f.Bool("hash-info", false, "show hash information from an idle crackstation (does not create a job)")
|
||||
f.Uint32("hash-info-level", 0, "hash information detail level: 1 or 2 (does not create a job)")
|
||||
f.Bool("backend-ignore-cuda", false, "ignore CUDA backend")
|
||||
f.Bool("backend-ignore-hip", false, "ignore HIP backend")
|
||||
f.Bool("backend-ignore-metal", false, "ignore Metal backend")
|
||||
f.Bool("backend-ignore-opencl", false, "ignore OpenCL backend")
|
||||
f.Bool("backend-info", false, "show backend info")
|
||||
f.Bool("backend-info", false, "show registered backend info for all connected crackstations (does not create a job)")
|
||||
f.Uint32("backend-info-level", 0, "registered backend information detail level: 1 or 2 (does not create a job)")
|
||||
f.UintSlice("backend-devices", nil, "backend device IDs")
|
||||
f.Uint32("backend-devices-virtmulti", 0, "virtual backend device multiplier")
|
||||
f.Uint32("backend-devices-virthost", 0, "virtual backend host count")
|
||||
f.UintSlice("opencl-device-types", nil, "OpenCL device types")
|
||||
f.Uint32("metal-compiler-runtime", 0, "Metal compiler runtime")
|
||||
f.Bool("optimized-kernel-enable", false, "enable optimized kernels")
|
||||
f.Bool("multiply-accel-disabled", false, "disable multiply accel")
|
||||
f.String("workload-profile", "", "workload profile (low, default, high, nightmare, or numeric)")
|
||||
f.Bool("multiply-accel-disable", false, "disable multiply accel")
|
||||
f.Bool("multiply-accel-disabled", false, "deprecated alias for multiply-accel-disable")
|
||||
_ = f.MarkDeprecated("multiply-accel-disabled", "use --multiply-accel-disable")
|
||||
_ = f.MarkHidden("multiply-accel-disabled")
|
||||
f.String("workload-profile", "", "deprecated: accepted but ignored by Hashcat v7")
|
||||
f.Uint32("kernel-accel", 0, "kernel acceleration")
|
||||
f.Uint32("kernel-loops", 0, "kernel loops")
|
||||
f.Uint32("kernel-threads", 0, "kernel threads")
|
||||
@@ -80,22 +109,47 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.Uint32("scrypt-tmto", 0, "scrypt tmto")
|
||||
f.Uint64("skip", 0, "skip count")
|
||||
f.Uint64("limit", 0, "limit count")
|
||||
f.Bool("keyspace", false, "show keyspace")
|
||||
f.String("rules-file", "", "rules file path or identifier")
|
||||
f.Bool("keyspace", false, "calculate keyspace on an idle crackstation and return it (does not create a job)")
|
||||
f.Bool("total-candidates", false, "calculate the total candidate count on an idle crackstation (does not create a job)")
|
||||
f.String("lookup", "", "locate a candidate in the attack on an idle crackstation (does not create a job)")
|
||||
f.String("crackstation", "", "target an exact crackstation name or host UUID for a synchronous query")
|
||||
f.Bool("dynamic-x", false, "enable dynamic-X mode")
|
||||
f.String("seekdb-path", "", "seek database path or identifier")
|
||||
f.String("rule-left", "", "left combinator rule")
|
||||
f.String("rule-right", "", "right combinator rule")
|
||||
f.StringArray("rules-file", nil, "rules file content or identifier (repeatable)")
|
||||
f.Uint32("generate-rules", 0, "generate rules")
|
||||
f.Uint32("generate-rules-fun-min", 0, "generate rules function min")
|
||||
f.Uint32("generate-rules-fun-max", 0, "generate rules function max")
|
||||
f.Uint32("generate-rules-func-min", 0, "generate rules function min")
|
||||
f.Uint32("generate-rules-func-max", 0, "generate rules function max")
|
||||
f.Uint32("generate-rules-fun-min", 0, "deprecated alias for generate-rules-func-min")
|
||||
f.Uint32("generate-rules-fun-max", 0, "deprecated alias for generate-rules-func-max")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-min", "use --generate-rules-func-min")
|
||||
_ = f.MarkHidden("generate-rules-fun-min")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-max", "use --generate-rules-func-max")
|
||||
_ = f.MarkHidden("generate-rules-fun-max")
|
||||
f.String("generate-rules-func-sel", "", "generate rules function selection")
|
||||
f.Int32("generate-rules-seed", 0, "generate rules seed")
|
||||
f.String("custom-charset1", "", "custom charset 1")
|
||||
f.String("custom-charset2", "", "custom charset 2")
|
||||
f.String("custom-charset3", "", "custom charset 3")
|
||||
f.String("custom-charset4", "", "custom charset 4")
|
||||
f.String("identify", "", "identify hash")
|
||||
f.String("custom-charset5", "", "custom charset 5")
|
||||
f.String("custom-charset6", "", "custom charset 6")
|
||||
f.String("custom-charset7", "", "custom charset 7")
|
||||
f.String("custom-charset8", "", "custom charset 8")
|
||||
f.String("identify", "", "deprecated: legacy positional input")
|
||||
f.Bool("identify-mode", false, "identify hash types on an idle crackstation (does not create a job)")
|
||||
f.Bool("increment", false, "incremental mode")
|
||||
f.Bool("increment-inverse", false, "inverse incremental mode")
|
||||
f.Uint32("increment-min", 0, "increment min")
|
||||
f.Uint32("increment-max", 0, "increment max")
|
||||
f.Uint32("bypass-delay", 0, "bypass delay")
|
||||
f.Uint32("bypass-threshold", 0, "bypass threshold")
|
||||
f.Bool("slow-candidates", false, "slow candidates")
|
||||
f.String("bridge-parameter1", "", "bridge parameter 1")
|
||||
f.String("bridge-parameter2", "", "bridge parameter 2")
|
||||
f.String("bridge-parameter3", "", "bridge parameter 3")
|
||||
f.String("bridge-parameter4", "", "bridge parameter 4")
|
||||
f.Bool("brain-server", false, "brain server mode")
|
||||
f.Uint32("brain-server-timer", 0, "brain server timer")
|
||||
f.Bool("brain-client", false, "brain client mode")
|
||||
@@ -105,4 +159,8 @@ func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.String("brain-password", "", "brain password")
|
||||
f.String("brain-session", "", "brain session")
|
||||
f.String("brain-session-whitelist", "", "brain session whitelist")
|
||||
f.Bool("brain-feed", false, "brain feed mode")
|
||||
f.Bool("color-cracked", false, "color cracked output")
|
||||
f.Bool("hash-copy", false, "copy hashes")
|
||||
f.String("encrypt-with-pubkey", "", "public key path or identifier used to encrypt output")
|
||||
}
|
||||
@@ -0,0 +1,988 @@
|
||||
package crack
|
||||
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type crackJobFetcher func(context.Context, string) (*clientpb.CrackJob, error)
|
||||
|
||||
var errNoCrackJobs = errors.New("no crack jobs")
|
||||
|
||||
// CrackJobsCmd lists all durable cracking jobs known to the server.
|
||||
//
|
||||
//nolint:revive // Keep the established exported command-handler name for compatibility.
|
||||
func CrackJobsCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
|
||||
jobs, err := con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||
con.PrintInfof("No crack jobs\n")
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", renderCrackJobs(jobs.Jobs, settings.GetTableStyle(con)))
|
||||
}
|
||||
|
||||
// CrackJobCmd displays one cracking job, optionally polling until completion.
|
||||
//
|
||||
//nolint:revive // Keep the established exported command-handler name for compatibility.
|
||||
func CrackJobCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
pollInterval, _ := cmd.Flags().GetDuration("poll-interval")
|
||||
if pollInterval <= 0 {
|
||||
con.PrintErrorf("--poll-interval must be greater than zero\n")
|
||||
return
|
||||
}
|
||||
jobID, err := resolveCrackJobID(args, func() (*clientpb.CrackJobs, error) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
return con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
|
||||
}, forms.CrackJobSelectForm)
|
||||
if errors.Is(err, errNoCrackJobs) {
|
||||
con.PrintInfof("No crack jobs\n")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
fetch := func(parent context.Context, id string) (*clientpb.CrackJob, error) {
|
||||
ctx := parent
|
||||
cancel := func() {}
|
||||
if timeoutSeconds > 0 {
|
||||
ctx, cancel = context.WithTimeout(parent, time.Duration(timeoutSeconds)*time.Second)
|
||||
}
|
||||
defer cancel()
|
||||
return con.Rpc.CrackJobByID(ctx, &clientpb.CrackJob{ID: id})
|
||||
}
|
||||
|
||||
watch, _ := cmd.Flags().GetBool("watch")
|
||||
if !watch {
|
||||
job, err := fetch(cmd.Context(), jobID)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", renderCrackJob(job, settings.GetTableStyle(con)))
|
||||
return
|
||||
}
|
||||
|
||||
first := true
|
||||
err = watchCrackJob(cmd.Context(), jobID, pollInterval, fetch, func(job *clientpb.CrackJob) {
|
||||
if !first {
|
||||
con.Println()
|
||||
}
|
||||
first = false
|
||||
con.Printf("%s\n", renderCrackJob(job, settings.GetTableStyle(con)))
|
||||
})
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCrackJobID(
|
||||
args []string,
|
||||
listJobs func() (*clientpb.CrackJobs, error),
|
||||
selectJob func([]*clientpb.CrackJob) (string, error),
|
||||
) (string, error) {
|
||||
if len(args) > 1 {
|
||||
return "", errors.New("only one crack job ID may be specified")
|
||||
}
|
||||
if len(args) == 1 {
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
if jobID == "" {
|
||||
return "", errors.New("job ID cannot be empty")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
if listJobs == nil {
|
||||
return "", errors.New("crack job lister is nil")
|
||||
}
|
||||
|
||||
jobs, err := listJobs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if jobs == nil || !hasSelectableCrackJob(jobs.GetJobs()) {
|
||||
return "", errNoCrackJobs
|
||||
}
|
||||
if selectJob == nil {
|
||||
return "", errors.New("crack job selector is nil")
|
||||
}
|
||||
jobID, err := selectJob(jobs.GetJobs())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jobID = strings.TrimSpace(jobID)
|
||||
if jobID == "" {
|
||||
return "", errors.New("no crack job selected")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
func hasSelectableCrackJob(jobs []*clientpb.CrackJob) bool {
|
||||
for _, job := range jobs {
|
||||
if job != nil && strings.TrimSpace(job.GetID()) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func crackCommandContext(parent context.Context, cmd *cobra.Command) (context.Context, context.CancelFunc) {
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
if timeoutSeconds <= 0 {
|
||||
return parent, func() {}
|
||||
}
|
||||
return context.WithTimeout(parent, time.Duration(timeoutSeconds)*time.Second)
|
||||
}
|
||||
|
||||
func watchCrackJob(ctx context.Context, jobID string, pollInterval time.Duration, fetch crackJobFetcher, onUpdate func(*clientpb.CrackJob)) error {
|
||||
if pollInterval <= 0 {
|
||||
return errors.New("poll interval must be greater than zero")
|
||||
}
|
||||
if fetch == nil {
|
||||
return errors.New("job fetcher is nil")
|
||||
}
|
||||
|
||||
var previous []byte
|
||||
for {
|
||||
job, err := fetch(ctx, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job == nil {
|
||||
return errors.New("server returned an empty crack job")
|
||||
}
|
||||
serialized, err := proto.MarshalOptions{Deterministic: true}.Marshal(job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode crack job: %w", err)
|
||||
}
|
||||
if !bytes.Equal(serialized, previous) {
|
||||
if onUpdate != nil {
|
||||
onUpdate(job)
|
||||
}
|
||||
previous = serialized
|
||||
}
|
||||
if crackJobTerminal(job) {
|
||||
return nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(pollInterval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func crackJobTerminal(job *clientpb.CrackJob) bool {
|
||||
if job == nil {
|
||||
return false
|
||||
}
|
||||
status := strings.ToUpper(job.Status.String())
|
||||
return strings.HasSuffix(status, "COMPLETED") || strings.HasSuffix(status, "FAILED") || strings.HasSuffix(status, "CANCELLED")
|
||||
}
|
||||
|
||||
func crackJobStatusStyle(status string) console.TextStyle {
|
||||
status = strings.ToUpper(strings.TrimSpace(status))
|
||||
switch {
|
||||
case strings.HasSuffix(status, "COMPLETED"):
|
||||
return console.StyleBoldSuccess
|
||||
case strings.HasSuffix(status, "FAILED"):
|
||||
return console.StyleBoldDanger
|
||||
case strings.HasSuffix(status, "CANCELLED"), strings.HasSuffix(status, "CANCELED"):
|
||||
return console.StyleBoldGray
|
||||
case status == "IN_PROGRESS", strings.HasSuffix(status, "RUNNING"), strings.HasSuffix(status, "LEASED"):
|
||||
return console.StyleBoldWarning
|
||||
case strings.HasSuffix(status, "QUEUED"), strings.HasSuffix(status, "PENDING"):
|
||||
return console.StyleBold
|
||||
default:
|
||||
return console.StyleBold
|
||||
}
|
||||
}
|
||||
|
||||
func renderCrackJobStatus(status string) string {
|
||||
status = safeCrackCell(status)
|
||||
return crackJobStatusStyle(status).Render(status)
|
||||
}
|
||||
|
||||
func renderCrackJobTitle(title string) string {
|
||||
return console.StyleBoldPrimary.Render(title)
|
||||
}
|
||||
|
||||
func renderCrackJobs(jobs []*clientpb.CrackJob, style table.Style) string {
|
||||
ordered := append([]*clientpb.CrackJob(nil), jobs...)
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
return crackJobSortTime(ordered[i]).After(crackJobSortTime(ordered[j]))
|
||||
})
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(style)
|
||||
tw.AppendHeader(table.Row{"ID", "Status", "Created", "Updated", "Keyspace", "Tasks", "Results"})
|
||||
for _, job := range ordered {
|
||||
if job == nil {
|
||||
continue
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
console.StyleBoldPrimary.Render(safeCrackCell(job.ID)),
|
||||
renderCrackJobStatus(job.Status.String()),
|
||||
formatCrackTime(job.CreatedAt),
|
||||
formatCrackTime(protoFieldText(job.ProtoReflect(), "UpdatedAt")),
|
||||
valueOrDash(protoFieldText(job.ProtoReflect(), "Keyspace")),
|
||||
strconv.Itoa(len(crackJobTasks(job))),
|
||||
crackJobResultCount(job),
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Keep the summary, task, device, and result sections in one deterministic job rendering pass.
|
||||
func renderCrackJob(job *clientpb.CrackJob, style table.Style) string {
|
||||
if job == nil {
|
||||
return "Empty crack job"
|
||||
}
|
||||
|
||||
sections := make([]string, 0, 4)
|
||||
summary := table.NewWriter()
|
||||
summary.SetStyle(style)
|
||||
summary.SetTitle(renderCrackJobTitle("Crack Job"))
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("ID"), console.StyleBoldPrimary.Render(safeCrackCell(job.ID))})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Status"), renderCrackJobStatus(job.Status.String())})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Created"), formatCrackTime(job.CreatedAt)})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Updated"), formatCrackTime(protoFieldText(job.ProtoReflect(), "UpdatedAt"))})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Keyspace"), valueOrDash(protoFieldText(job.ProtoReflect(), "Keyspace"))})
|
||||
if job.Err != "" {
|
||||
summary.AppendRow(table.Row{console.StyleBoldDanger.Render("Error"), console.StyleDanger.Render(safeCrackCell(job.Err))})
|
||||
}
|
||||
sections = append(sections, summary.Render())
|
||||
|
||||
tasks := crackJobTasks(job)
|
||||
if len(tasks) > 0 {
|
||||
taskTable := table.NewWriter()
|
||||
taskTable.SetStyle(style)
|
||||
taskTable.SetTitle(renderCrackJobTitle("Tasks"))
|
||||
taskTable.AppendHeader(table.Row{"ID", "Kind", "State", "Station", "Range", "Progress", "Speed", "Temp", "Updated"})
|
||||
devices := make([]crackDeviceView, 0)
|
||||
for _, task := range tasks {
|
||||
status, statusErr := crackTaskStatus(task)
|
||||
progress, speed, temperature := "-", "-", "-"
|
||||
if statusErr != nil {
|
||||
progress = console.StyleDanger.Render("invalid status JSON")
|
||||
} else if status != nil {
|
||||
progress = status.progressText()
|
||||
speed = status.speedText()
|
||||
temperature = status.temperatureText()
|
||||
for _, device := range status.Devices {
|
||||
device.TaskID = task.ID
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
taskTable.AppendRow(table.Row{
|
||||
safeCrackCell(task.ID),
|
||||
valueOrDash(protoFieldText(task.ProtoReflect(), "Kind")),
|
||||
renderCrackJobStatus(crackTaskState(task)),
|
||||
valueOrDash(task.HostUUID),
|
||||
crackTaskRange(task),
|
||||
progress,
|
||||
speed,
|
||||
temperature,
|
||||
formatCrackTime(protoFieldText(task.ProtoReflect(), "UpdatedAt", "LastHeartbeatAt")),
|
||||
})
|
||||
}
|
||||
sections = append(sections, taskTable.Render())
|
||||
|
||||
statisticsTable := table.NewWriter()
|
||||
statisticsTable.SetStyle(style)
|
||||
statisticsTable.SetTitle(renderCrackJobTitle("Hashcat Statistics"))
|
||||
statisticsTable.AppendHeader(table.Row{
|
||||
"Task", "Session", "Status", "Target", "Guess", "Progress", "Rejected", "Restore",
|
||||
"Recovered Hashes", "Recovered Salts", "Started", "Estimated Stop",
|
||||
})
|
||||
hasStatistics := false
|
||||
for _, task := range tasks {
|
||||
status, err := crackTaskStatus(task)
|
||||
if err != nil || status == nil {
|
||||
continue
|
||||
}
|
||||
hasStatistics = true
|
||||
statisticsTable.AppendRow(table.Row{
|
||||
safeCrackCell(task.ID),
|
||||
valueOrDash(status.Session),
|
||||
valueOrDash(status.Status),
|
||||
valueOrDash(status.Target),
|
||||
status.guessText(),
|
||||
status.progressText(),
|
||||
valueOrDash(status.Rejected),
|
||||
valueOrDash(status.RestorePoint),
|
||||
pairText(status.RecoveredHashesCurrent, status.RecoveredHashesTotal),
|
||||
pairText(status.RecoveredSaltsCurrent, status.RecoveredSaltsTotal),
|
||||
formatCrackTime(status.TimeStart),
|
||||
formatCrackTime(status.EstimatedStop),
|
||||
})
|
||||
}
|
||||
if hasStatistics {
|
||||
sections = append(sections, statisticsTable.Render())
|
||||
}
|
||||
|
||||
if len(devices) > 0 {
|
||||
deviceTable := table.NewWriter()
|
||||
deviceTable.SetStyle(style)
|
||||
deviceTable.SetTitle(renderCrackJobTitle("Devices"))
|
||||
deviceTable.AppendHeader(table.Row{"Task", "ID", "Name", "Type", "Speed", "Temp", "Util", "Fan", "Core", "Memory", "Bus", "Power"})
|
||||
for _, device := range devices {
|
||||
deviceTable.AppendRow(table.Row{
|
||||
safeCrackCell(device.TaskID),
|
||||
valueOrDash(device.ID),
|
||||
valueOrDash(device.Name),
|
||||
valueOrDash(device.Type),
|
||||
device.speedText(),
|
||||
unitValue(device.Temperature, " C"),
|
||||
unitValue(device.Utilization, "%"),
|
||||
unitValue(device.FanSpeed, "%"),
|
||||
unitValue(device.CoreSpeed, " MHz"),
|
||||
unitValue(device.MemorySpeed, " MHz"),
|
||||
unitValue(device.BusLanes, " lanes"),
|
||||
unitValue(device.Power, " mW"),
|
||||
})
|
||||
}
|
||||
sections = append(sections, deviceTable.Render())
|
||||
}
|
||||
}
|
||||
|
||||
results := crackJobResults(job)
|
||||
if len(results) > 0 {
|
||||
resultTable := table.NewWriter()
|
||||
resultTable.SetStyle(style)
|
||||
resultTable.SetTitle(console.StyleBoldSuccess.Render("Recovered Credentials"))
|
||||
resultTable.AppendHeader(table.Row{"Credential", "Hash", "Plaintext"})
|
||||
for _, result := range results {
|
||||
resultTable.AppendRow(table.Row{
|
||||
valueOrDash(result.CredentialID),
|
||||
valueOrDash(result.Hash),
|
||||
valueOrDash(result.Plaintext),
|
||||
})
|
||||
}
|
||||
sections = append(sections, resultTable.Render())
|
||||
}
|
||||
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func crackJobSortTime(job *clientpb.CrackJob) time.Time {
|
||||
if job == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
for _, value := range []string{protoFieldText(job.ProtoReflect(), "UpdatedAt"), job.CreatedAt} {
|
||||
if unix, err := strconv.ParseInt(value, 10, 64); err == nil && unix > 0 {
|
||||
return time.Unix(unix, 0)
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func crackJobTasks(job *clientpb.CrackJob) []*clientpb.CrackTask {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
list, ok := protoRepeatedMessages(job.ProtoReflect(), "Tasks")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
tasks := make([]*clientpb.CrackTask, 0, list.Len())
|
||||
for index := 0; index < list.Len(); index++ {
|
||||
if task, ok := list.Get(index).Message().Interface().(*clientpb.CrackTask); ok {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func crackTaskState(task *clientpb.CrackTask) string {
|
||||
if state := protoFieldText(task.ProtoReflect(), "State"); state != "" {
|
||||
return safeCrackCell(state)
|
||||
}
|
||||
if task.Err != "" {
|
||||
return "FAILED"
|
||||
}
|
||||
if task.CompletedAt != 0 {
|
||||
return "COMPLETED"
|
||||
}
|
||||
if task.StartedAt != 0 {
|
||||
return "RUNNING"
|
||||
}
|
||||
return "QUEUED"
|
||||
}
|
||||
|
||||
func crackTaskRange(task *clientpb.CrackTask) string {
|
||||
skip := protoFieldText(task.ProtoReflect(), "ShardSkip")
|
||||
limit := protoFieldText(task.ProtoReflect(), "ShardLimit")
|
||||
if skip == "" && task.Command != nil {
|
||||
skip = strconv.FormatUint(task.Command.Skip, 10)
|
||||
}
|
||||
if limit == "" && task.Command != nil {
|
||||
limit = strconv.FormatUint(task.Command.Limit, 10)
|
||||
}
|
||||
if skip == "" {
|
||||
skip = "0"
|
||||
}
|
||||
if limit == "" || limit == "0" {
|
||||
return skip + "+"
|
||||
}
|
||||
return skip + "+" + limit
|
||||
}
|
||||
|
||||
func crackTaskStatus(task *clientpb.CrackTask) (*crackStatusView, error) {
|
||||
raw := protoFieldBytes(task.ProtoReflect(), "LatestStatusJSON", "RawStatusJSON", "StatusJSON")
|
||||
if len(bytes.TrimSpace(raw)) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return parseCrackStatusJSON(raw)
|
||||
}
|
||||
|
||||
type crackStatusView struct {
|
||||
Session string
|
||||
Status string
|
||||
Target string
|
||||
ProgressCurrent string
|
||||
ProgressTotal string
|
||||
RestorePoint string
|
||||
RecoveredHashesCurrent string
|
||||
RecoveredHashesTotal string
|
||||
RecoveredSaltsCurrent string
|
||||
RecoveredSaltsTotal string
|
||||
Rejected string
|
||||
TimeStart string
|
||||
EstimatedStop string
|
||||
Guess crackGuessView
|
||||
Devices []crackDeviceView
|
||||
}
|
||||
|
||||
type crackGuessView struct {
|
||||
Mode string
|
||||
Base string
|
||||
BaseOffset string
|
||||
BaseCount string
|
||||
BasePercent string
|
||||
MaskLength string
|
||||
Mod string
|
||||
ModOffset string
|
||||
ModCount string
|
||||
ModPercent string
|
||||
}
|
||||
|
||||
func (status *crackStatusView) progressText() string {
|
||||
if status == nil || status.ProgressCurrent == "" || status.ProgressTotal == "" {
|
||||
return "-"
|
||||
}
|
||||
current, currentErr := strconv.ParseFloat(status.ProgressCurrent, 64)
|
||||
total, totalErr := strconv.ParseFloat(status.ProgressTotal, 64)
|
||||
if currentErr == nil && totalErr == nil && total > 0 {
|
||||
return fmt.Sprintf("%s/%s (%.1f%%)", status.ProgressCurrent, status.ProgressTotal, math.Min(100, current/total*100))
|
||||
}
|
||||
return status.ProgressCurrent + "/" + status.ProgressTotal
|
||||
}
|
||||
|
||||
func (status *crackStatusView) speedText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
var total uint64
|
||||
for _, device := range status.Devices {
|
||||
speed, err := strconv.ParseUint(device.Speed, 10, 64)
|
||||
if err == nil {
|
||||
total += speed
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return "-"
|
||||
}
|
||||
return humanizeHashRate(total)
|
||||
}
|
||||
|
||||
func (status *crackStatusView) temperatureText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
var maximum float64
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
temperature, err := strconv.ParseFloat(device.Temperature, 64)
|
||||
if err == nil && temperature >= 0 && (!found || temperature > maximum) {
|
||||
maximum = temperature
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprintf("%g C", maximum)
|
||||
}
|
||||
|
||||
func (status *crackStatusView) guessText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
parts := make([]string, 0, 3)
|
||||
if status.Guess.Mode != "" {
|
||||
parts = append(parts, "mode="+status.Guess.Mode)
|
||||
}
|
||||
if status.Guess.Base != "" {
|
||||
base := status.Guess.Base
|
||||
if status.Guess.BaseOffset != "" || status.Guess.BaseCount != "" {
|
||||
base += " [" + pairText(status.Guess.BaseOffset, status.Guess.BaseCount) + "]"
|
||||
}
|
||||
if status.Guess.BasePercent != "" {
|
||||
base += " (" + status.Guess.BasePercent + "%)"
|
||||
}
|
||||
parts = append(parts, "base="+base)
|
||||
}
|
||||
if status.Guess.MaskLength != "" {
|
||||
parts = append(parts, "mask-length="+status.Guess.MaskLength)
|
||||
}
|
||||
if status.Guess.Mod != "" {
|
||||
mod := status.Guess.Mod
|
||||
if status.Guess.ModOffset != "" || status.Guess.ModCount != "" {
|
||||
mod += " [" + pairText(status.Guess.ModOffset, status.Guess.ModCount) + "]"
|
||||
}
|
||||
if status.Guess.ModPercent != "" {
|
||||
mod += " (" + status.Guess.ModPercent + "%)"
|
||||
}
|
||||
parts = append(parts, "mod="+mod)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return safeCrackCell(strings.Join(parts, "; "))
|
||||
}
|
||||
|
||||
type crackDeviceView struct {
|
||||
TaskID string
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Speed string
|
||||
Temperature string
|
||||
Utilization string
|
||||
FanSpeed string
|
||||
CoreSpeed string
|
||||
MemorySpeed string
|
||||
BusLanes string
|
||||
Power string
|
||||
}
|
||||
|
||||
func (device crackDeviceView) speedText() string {
|
||||
speed, err := strconv.ParseUint(device.Speed, 10, 64)
|
||||
if err != nil || speed == 0 {
|
||||
return valueOrDash(device.Speed)
|
||||
}
|
||||
return humanizeHashRate(speed)
|
||||
}
|
||||
|
||||
func parseCrackStatusJSON(raw []byte) (*crackStatusView, error) {
|
||||
root, err := decodeLastJSONObject(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := &crackStatusView{
|
||||
Session: jsonText(firstJSONValue(root, "session")),
|
||||
Status: jsonText(firstJSONValue(root, "status_string", "status")),
|
||||
Target: jsonText(firstJSONValue(root, "target")),
|
||||
RestorePoint: jsonText(firstJSONValue(root, "restore_point")),
|
||||
Rejected: jsonText(firstJSONValue(root, "rejected")),
|
||||
TimeStart: jsonText(firstJSONValue(root, "time_start")),
|
||||
EstimatedStop: jsonText(firstJSONValue(root, "estimated_stop")),
|
||||
}
|
||||
status.ProgressCurrent, status.ProgressTotal = jsonPair(firstJSONValue(root, "progress"))
|
||||
status.RecoveredHashesCurrent, status.RecoveredHashesTotal = jsonPair(firstJSONValue(root, "recovered_hashes"))
|
||||
status.RecoveredSaltsCurrent, status.RecoveredSaltsTotal = jsonPair(firstJSONValue(root, "recovered_salts"))
|
||||
if guess, ok := firstJSONValue(root, "guess").(map[string]any); ok {
|
||||
status.Guess = crackGuessView{
|
||||
Mode: jsonText(firstJSONValue(guess, "guess_mode", "mode")),
|
||||
Base: jsonText(firstJSONValue(guess, "guess_base", "base")),
|
||||
BaseOffset: jsonText(firstJSONValue(guess, "guess_base_offset", "base_offset")),
|
||||
BaseCount: jsonText(firstJSONValue(guess, "guess_base_count", "base_count")),
|
||||
BasePercent: jsonText(firstJSONValue(guess, "guess_base_percent", "base_percent")),
|
||||
MaskLength: jsonText(firstJSONValue(guess, "guess_mask_length", "mask_length")),
|
||||
Mod: jsonText(firstJSONValue(guess, "guess_mod", "mod")),
|
||||
ModOffset: jsonText(firstJSONValue(guess, "guess_mod_offset", "mod_offset")),
|
||||
ModCount: jsonText(firstJSONValue(guess, "guess_mod_count", "mod_count")),
|
||||
ModPercent: jsonText(firstJSONValue(guess, "guess_mod_percent", "mod_percent")),
|
||||
}
|
||||
}
|
||||
if devices, ok := firstJSONValue(root, "devices").([]any); ok {
|
||||
for _, item := range devices {
|
||||
values, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
status.Devices = append(status.Devices, crackDeviceView{
|
||||
ID: jsonText(firstJSONValue(values, "device_id", "id")),
|
||||
Name: safeCrackCell(jsonText(firstJSONValue(values, "device_name", "name"))),
|
||||
Type: safeCrackCell(jsonText(firstJSONValue(values, "device_type", "type"))),
|
||||
Speed: jsonText(firstJSONValue(values, "speed")),
|
||||
Temperature: jsonText(firstJSONValue(values, "temp", "temperature")),
|
||||
Utilization: jsonText(firstJSONValue(values, "util", "utilization")),
|
||||
FanSpeed: jsonText(firstJSONValue(values, "fanspeed", "fan_speed")),
|
||||
CoreSpeed: jsonText(firstJSONValue(values, "corespeed", "core_speed")),
|
||||
MemorySpeed: jsonText(firstJSONValue(values, "memoryspeed", "memory_speed")),
|
||||
BusLanes: jsonText(firstJSONValue(values, "buslanes", "bus_lanes")),
|
||||
Power: jsonText(firstJSONValue(values, "power")),
|
||||
})
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func decodeLastJSONObject(raw []byte) (map[string]any, error) {
|
||||
decode := func(candidate []byte) (map[string]any, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(candidate))
|
||||
decoder.UseNumber()
|
||||
var last map[string]any
|
||||
for {
|
||||
value := map[string]any{}
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
break
|
||||
}
|
||||
last = value
|
||||
}
|
||||
if last == nil {
|
||||
return nil, errors.New("no JSON object")
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if value, err := decode(trimmed); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
lines := bytes.Split(trimmed, []byte{'\n'})
|
||||
for index := len(lines) - 1; index >= 0; index-- {
|
||||
candidate := bytes.TrimSpace(lines[index])
|
||||
if len(candidate) == 0 {
|
||||
continue
|
||||
}
|
||||
if value, err := decode(candidate); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("hashcat status did not contain a JSON object")
|
||||
}
|
||||
|
||||
type crackResultView struct {
|
||||
CredentialID string
|
||||
Hash string
|
||||
Plaintext string
|
||||
}
|
||||
|
||||
func crackJobResults(job *clientpb.CrackJob) []crackResultView {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
results := make([]crackResultView, 0)
|
||||
if list, ok := protoRepeatedMessages(job.ProtoReflect(), "Results"); ok {
|
||||
for index := 0; index < list.Len(); index++ {
|
||||
message := list.Get(index).Message()
|
||||
results = append(results, crackResultView{
|
||||
CredentialID: safeCrackCell(protoFieldText(message, "CredentialID")),
|
||||
Hash: safeCrackCell(protoFieldText(message, "Hash")),
|
||||
Plaintext: displayCrackPlaintext(protoFieldBytes(message, "Plaintext")),
|
||||
})
|
||||
}
|
||||
}
|
||||
// The job-level result rows are authoritative once the server has
|
||||
// materialized them. Fall back to worker JSON only while those rows have not
|
||||
// appeared yet, avoiding duplicate plaintexts in a watched job.
|
||||
if len(results) == 0 {
|
||||
for _, task := range crackJobTasks(job) {
|
||||
results = append(results, parseRecoveredResults(protoFieldBytes(task.ProtoReflect(), "RecoveredJSON"))...)
|
||||
}
|
||||
}
|
||||
|
||||
deduplicated := make([]crackResultView, 0, len(results))
|
||||
seen := map[string]struct{}{}
|
||||
for _, result := range results {
|
||||
key := result.CredentialID + "\x00" + result.Hash + "\x00" + result.Plaintext
|
||||
if _, ok := seen[key]; ok || (result.Hash == "" && result.Plaintext == "" && result.CredentialID == "") {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
deduplicated = append(deduplicated, result)
|
||||
}
|
||||
return deduplicated
|
||||
}
|
||||
|
||||
func crackJobResultCount(job *clientpb.CrackJob) string {
|
||||
if job == nil {
|
||||
return "-"
|
||||
}
|
||||
return valueOrDash(protoFieldText(job.ProtoReflect(), "ResultCount"))
|
||||
}
|
||||
|
||||
func displayCrackPlaintext(plaintext []byte) string {
|
||||
if !utf8.Valid(plaintext) || bytes.IndexByte(plaintext, 0) >= 0 {
|
||||
return "0x" + hex.EncodeToString(plaintext)
|
||||
}
|
||||
return safeCrackCell(string(plaintext))
|
||||
}
|
||||
|
||||
func parseRecoveredResults(raw []byte) []crackResultView {
|
||||
if len(bytes.TrimSpace(raw)) == 0 {
|
||||
return nil
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var items []map[string]any
|
||||
if err := decoder.Decode(&items); err != nil {
|
||||
return nil
|
||||
}
|
||||
results := make([]crackResultView, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, crackResultView{
|
||||
CredentialID: safeCrackCell(jsonText(firstJSONValue(item, "credential_id", "credentialID"))),
|
||||
Hash: safeCrackCell(jsonText(firstJSONValue(item, "hash"))),
|
||||
Plaintext: decodeRecoveredPlaintext(jsonText(firstJSONValue(item, "plaintext"))),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func decodeRecoveredPlaintext(value string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
// Older crackstations encoded plaintext as a JSON string. Preserve that
|
||||
// representation when it is not valid protobuf bytes JSON.
|
||||
return safeCrackCell(value)
|
||||
}
|
||||
if !utf8.Valid(decoded) || bytes.IndexByte(decoded, 0) >= 0 {
|
||||
return "0x" + hex.EncodeToString(decoded)
|
||||
}
|
||||
return safeCrackCell(string(decoded))
|
||||
}
|
||||
|
||||
func protoRepeatedMessages(message protoreflect.Message, names ...string) (protoreflect.List, bool) {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil || !field.IsList() || field.Kind() != protoreflect.MessageKind {
|
||||
return nil, false
|
||||
}
|
||||
return message.Get(field).List(), true
|
||||
}
|
||||
|
||||
func protoFieldBytes(message protoreflect.Message, names ...string) []byte {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil {
|
||||
return nil
|
||||
}
|
||||
switch field.Kind() {
|
||||
case protoreflect.BytesKind:
|
||||
return append([]byte(nil), message.Get(field).Bytes()...)
|
||||
case protoreflect.StringKind:
|
||||
return []byte(message.Get(field).String())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func protoFieldText(message protoreflect.Message, names ...string) string {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil {
|
||||
return ""
|
||||
}
|
||||
value := message.Get(field)
|
||||
switch field.Kind() {
|
||||
case protoreflect.StringKind:
|
||||
return safeCrackCell(value.String())
|
||||
case protoreflect.BytesKind:
|
||||
return safeCrackCell(string(value.Bytes()))
|
||||
case protoreflect.EnumKind:
|
||||
if descriptor := field.Enum().Values().ByNumber(value.Enum()); descriptor != nil {
|
||||
return string(descriptor.Name())
|
||||
}
|
||||
return strconv.FormatInt(int64(value.Enum()), 10)
|
||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
||||
return strconv.FormatInt(value.Int(), 10)
|
||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
||||
return strconv.FormatUint(value.Uint(), 10)
|
||||
case protoreflect.FloatKind:
|
||||
return strconv.FormatFloat(value.Float(), 'f', -1, 32)
|
||||
case protoreflect.DoubleKind:
|
||||
return strconv.FormatFloat(value.Float(), 'f', -1, 64)
|
||||
case protoreflect.BoolKind:
|
||||
return strconv.FormatBool(value.Bool())
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func findProtoField(message protoreflect.Message, names ...string) protoreflect.FieldDescriptor {
|
||||
if !message.IsValid() {
|
||||
return nil
|
||||
}
|
||||
wanted := map[string]struct{}{}
|
||||
for _, name := range names {
|
||||
wanted[normalizedProtoName(name)] = struct{}{}
|
||||
}
|
||||
fields := message.Descriptor().Fields()
|
||||
for index := 0; index < fields.Len(); index++ {
|
||||
field := fields.Get(index)
|
||||
if _, ok := wanted[normalizedProtoName(string(field.Name()))]; ok {
|
||||
return field
|
||||
}
|
||||
if _, ok := wanted[normalizedProtoName(field.JSONName())]; ok {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedProtoName(value string) string {
|
||||
value = strings.ToLower(value)
|
||||
return strings.NewReplacer("_", "", "-", "").Replace(value)
|
||||
}
|
||||
|
||||
func firstJSONValue(values map[string]any, names ...string) any {
|
||||
for _, name := range names {
|
||||
if value, ok := values[name]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func jsonPair(value any) (string, string) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) < 2 {
|
||||
return "", ""
|
||||
}
|
||||
return jsonText(items[0]), jsonText(items[1])
|
||||
}
|
||||
|
||||
func pairText(first, second string) string {
|
||||
if first == "" && second == "" {
|
||||
return "-"
|
||||
}
|
||||
return valueOrDash(first) + "/" + valueOrDash(second)
|
||||
}
|
||||
|
||||
func jsonText(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return safeCrackCell(typed)
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case bool:
|
||||
return strconv.FormatBool(typed)
|
||||
default:
|
||||
return safeCrackCell(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
|
||||
func formatCrackTime(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || value == "0" {
|
||||
return "-"
|
||||
}
|
||||
if unix, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return safeCrackCell(value)
|
||||
}
|
||||
|
||||
func unitValue(value, suffix string) string {
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
if numeric, err := strconv.ParseFloat(value, 64); err == nil && numeric < 0 {
|
||||
return "-"
|
||||
}
|
||||
return safeCrackCell(value) + suffix
|
||||
}
|
||||
|
||||
func valueOrDash(value string) string {
|
||||
value = safeCrackCell(value)
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func safeCrackCell(value string) string {
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if unicode.IsControl(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
value = strings.TrimSpace(value)
|
||||
const maximumRunes = 96
|
||||
runes := []rune(value)
|
||||
if len(runes) > maximumRunes {
|
||||
value = string(runes[:maximumRunes-1]) + "…"
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func TestCrackJobCommandsAreRegistered(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
jobs, _, err := root.Find([]string{"jobs"})
|
||||
if err != nil || jobs == root {
|
||||
t.Fatalf("find crack jobs: command=%v error=%v", jobs, err)
|
||||
}
|
||||
top, _, err := root.Find([]string{"top"})
|
||||
if err != nil || top == root {
|
||||
t.Fatalf("find crack top: command=%v error=%v", top, err)
|
||||
}
|
||||
if top.Use != "top" {
|
||||
t.Fatalf("crack top use = %q, want top", top.Use)
|
||||
}
|
||||
if err := top.Args(top, nil); err != nil {
|
||||
t.Fatalf("crack top rejected no arguments: %v", err)
|
||||
}
|
||||
if err := top.Args(top, []string{"unexpected"}); err == nil {
|
||||
t.Fatal("crack top accepted a positional argument")
|
||||
}
|
||||
pollInterval := top.Flags().Lookup("poll-interval")
|
||||
if pollInterval == nil {
|
||||
t.Fatal("crack top is missing --poll-interval")
|
||||
}
|
||||
if got, err := top.Flags().GetDuration("poll-interval"); err != nil || got != time.Second {
|
||||
t.Fatalf("crack top --poll-interval = %s, %v; want %s", got, err, time.Second)
|
||||
}
|
||||
if top.PersistentFlags().Lookup("poll-interval") != nil || root.PersistentFlags().Lookup("poll-interval") != nil {
|
||||
t.Fatal("--poll-interval must remain local to crack top")
|
||||
}
|
||||
if top.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack top must inherit --timeout")
|
||||
}
|
||||
job, _, err := root.Find([]string{"job", "job-id"})
|
||||
if err != nil || job == root {
|
||||
t.Fatalf("find crack job: command=%v error=%v", job, err)
|
||||
}
|
||||
if job.Flags().Lookup("watch") == nil || job.Flags().Lookup("poll-interval") == nil {
|
||||
t.Fatal("crack job is missing watch flags")
|
||||
}
|
||||
if job.Use != "job [id]" {
|
||||
t.Fatalf("crack job use = %q, want %q", job.Use, "job [id]")
|
||||
}
|
||||
if job.ValidArgsFunction == nil {
|
||||
t.Fatal("crack job is missing ID completion")
|
||||
}
|
||||
if err := job.Args(job, nil); err != nil {
|
||||
t.Fatalf("crack job rejected interactive selection: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"job-id"}); err != nil {
|
||||
t.Fatalf("crack job rejected one job ID: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"first", "second"}); err == nil {
|
||||
t.Fatal("crack job accepted more than one job ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDUsesExplicitIDWithoutListing(t *testing.T) {
|
||||
listed := false
|
||||
selected := false
|
||||
jobID, err := resolveCrackJobID([]string{" full-job-id "}, func() (*clientpb.CrackJobs, error) {
|
||||
listed = true
|
||||
return nil, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selected = true
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "full-job-id" {
|
||||
t.Fatalf("job ID = %q, want full-job-id", jobID)
|
||||
}
|
||||
if listed || selected {
|
||||
t.Fatalf("explicit ID unexpectedly listed=%v selected=%v", listed, selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDSelectsFromListedJobs(t *testing.T) {
|
||||
wantJobs := []*clientpb.CrackJob{{ID: "first"}, {ID: "selected-full-id"}}
|
||||
listCalls := 0
|
||||
selectCalls := 0
|
||||
jobID, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
listCalls++
|
||||
return &clientpb.CrackJobs{Jobs: wantJobs}, nil
|
||||
}, func(jobs []*clientpb.CrackJob) (string, error) {
|
||||
selectCalls++
|
||||
if len(jobs) != len(wantJobs) || jobs[1] != wantJobs[1] {
|
||||
t.Fatalf("selector jobs = %#v, want %#v", jobs, wantJobs)
|
||||
}
|
||||
return "selected-full-id", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "selected-full-id" {
|
||||
t.Fatalf("job ID = %q, want selected-full-id", jobID)
|
||||
}
|
||||
if listCalls != 1 || selectCalls != 1 {
|
||||
t.Fatalf("list calls = %d, select calls = %d; want 1 each", listCalls, selectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDHandlesEmptyAndInvalidSelections(t *testing.T) {
|
||||
selectorCalled := false
|
||||
for _, listedJobs := range [][]*clientpb.CrackJob{nil, {nil, {ID: " "}}} {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: listedJobs}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selectorCalled = true
|
||||
return "unused", nil
|
||||
})
|
||||
if !errors.Is(err, errNoCrackJobs) {
|
||||
t.Fatalf("empty job error = %v, want %v", err, errNoCrackJobs)
|
||||
}
|
||||
}
|
||||
if selectorCalled {
|
||||
t.Fatal("selector called with no jobs")
|
||||
}
|
||||
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return " ", nil
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "no crack job selected") {
|
||||
t.Fatalf("blank selection error = %v", err)
|
||||
}
|
||||
|
||||
_, err = resolveCrackJobID([]string{" "}, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "job ID cannot be empty") {
|
||||
t.Fatalf("blank explicit ID error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDPropagatesUserAbort(t *testing.T) {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return "", forms.ErrUserAborted
|
||||
})
|
||||
if !errors.Is(err, forms.ErrUserAborted) {
|
||||
t.Fatalf("selection error = %v, want user abort", err)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // One fixture verifies the complete newest-status telemetry contract and all presentation helpers.
|
||||
func TestParseCrackStatusJSONUsesNewestStatus(t *testing.T) {
|
||||
raw := []byte("{\"status\":1,\"progress\":[1,10]}\n" +
|
||||
"{\"session\":\"distributed-job\",\"status\":2,\"target\":\"hashes\",\"progress\":[5,10]," +
|
||||
"\"restore_point\":4,\"recovered_hashes\":[1,3],\"recovered_salts\":[1,2],\"rejected\":7," +
|
||||
"\"time_start\":1700000000,\"estimated_stop\":1700000100," +
|
||||
"\"guess\":{\"guess_mode\":3,\"guess_base\":\"?d?d\",\"guess_base_offset\":1,\"guess_base_count\":10," +
|
||||
"\"guess_base_percent\":25.5,\"guess_mask_length\":2,\"guess_mod\":\"rules\",\"guess_mod_offset\":2,\"guess_mod_count\":4,\"guess_mod_percent\":50},\"devices\":[" +
|
||||
"{\"device_id\":1,\"device_name\":\"GPU 0\",\"device_type\":\"GPU\",\"speed\":1500,\"temp\":65,\"util\":98,\"corespeed\":2100,\"memoryspeed\":9500,\"buslanes\":16}," +
|
||||
"{\"id\":2,\"name\":\"GPU 1\",\"type\":\"GPU\",\"speed\":500,\"temperature\":70,\"utilization\":97}]}\n")
|
||||
|
||||
status, err := parseCrackStatusJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCrackStatusJSON: %v", err)
|
||||
}
|
||||
if status.Status != "2" || status.ProgressCurrent != "5" || status.ProgressTotal != "10" {
|
||||
t.Fatalf("unexpected status: %#v", status)
|
||||
}
|
||||
if got := status.progressText(); got != "5/10 (50.0%)" {
|
||||
t.Fatalf("progressText = %q", got)
|
||||
}
|
||||
if got := status.speedText(); got != "2.00 kH/s" {
|
||||
t.Fatalf("speedText = %q", got)
|
||||
}
|
||||
if got := status.temperatureText(); got != "70 C" {
|
||||
t.Fatalf("temperatureText = %q", got)
|
||||
}
|
||||
if len(status.Devices) != 2 || status.Devices[1].Name != "GPU 1" {
|
||||
t.Fatalf("devices = %#v", status.Devices)
|
||||
}
|
||||
if status.Session != "distributed-job" || status.Target != "hashes" || status.RestorePoint != "4" ||
|
||||
status.RecoveredHashesCurrent != "1" || status.RecoveredHashesTotal != "3" ||
|
||||
status.RecoveredSaltsCurrent != "1" || status.RecoveredSaltsTotal != "2" || status.Rejected != "7" ||
|
||||
status.TimeStart != "1700000000" || status.EstimatedStop != "1700000100" {
|
||||
t.Fatalf("full status fields were not decoded: %#v", status)
|
||||
}
|
||||
if status.Guess.Mode != "3" || status.Guess.Base != "?d?d" || status.Guess.BasePercent != "25.5" || status.Guess.MaskLength != "2" || status.Guess.Mod != "rules" || status.Guess.ModPercent != "50" {
|
||||
t.Fatalf("guess = %#v", status.Guess)
|
||||
}
|
||||
if got := status.guessText(); !strings.Contains(got, "base=?d?d [1/10] (25.5%)") || !strings.Contains(got, "mask-length=2") || !strings.Contains(got, "mod=rules [2/4] (50%)") {
|
||||
t.Fatalf("guessText = %q", got)
|
||||
}
|
||||
if status.Devices[0].CoreSpeed != "2100" || status.Devices[0].MemorySpeed != "9500" || status.Devices[0].BusLanes != "16" {
|
||||
t.Fatalf("extended device telemetry = %#v", status.Devices[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrackStatusJSONRejectsMalformedOutput(t *testing.T) {
|
||||
if _, err := parseCrackStatusJSON([]byte("hashcat is still starting")); err == nil {
|
||||
t.Fatal("malformed status output was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableDeviceTelemetryRendersAsUnknown(t *testing.T) {
|
||||
status := &crackStatusView{Devices: []crackDeviceView{{Temperature: "-1"}}}
|
||||
if got := status.temperatureText(); got != "-" {
|
||||
t.Fatalf("temperatureText = %q, want -", got)
|
||||
}
|
||||
if got := unitValue("-1", " C"); got != "-" {
|
||||
t.Fatalf("unitValue = %q, want -", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobIncludesTasksProgressDevicesAndResults(t *testing.T) {
|
||||
status := []byte(`{"session":"job-session","status":2,"target":"hash-file","progress":[25,100],"restore_point":24,"recovered_hashes":[1,2],"recovered_salts":[1,1],"rejected":3,"time_start":1700000000,"estimated_stop":1700000100,"guess":{"guess_mode":3,"guess_base":"?d?d","guess_base_offset":0,"guess_base_count":1},"devices":[{"device_id":1,"device_name":"GPU 0","device_type":"GPU","speed":2500000,"temp":61,"util":99,"fanspeed":45,"corespeed":2100,"memoryspeed":9000,"buslanes":16,"power":125000}]}`)
|
||||
recovered := []byte(`[{"hash":"hash-one","plaintext":"cGxhaW4tb25l"}]`)
|
||||
task := &clientpb.CrackTask{
|
||||
ID: "task-id",
|
||||
HostUUID: "station-id",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
UpdatedAt: 1700000002,
|
||||
LatestStatusJSON: status,
|
||||
RecoveredJSON: recovered,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 50,
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000000",
|
||||
Tasks: []*clientpb.CrackTask{task},
|
||||
Results: []*clientpb.CrackResult{{
|
||||
Hash: "hash-one",
|
||||
Plaintext: []byte("plain-one"),
|
||||
}, {
|
||||
CredentialID: "credential-id",
|
||||
Hash: "hash-two",
|
||||
Plaintext: []byte("plain-two"),
|
||||
}},
|
||||
}
|
||||
|
||||
rawOutput := renderCrackJob(job, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{
|
||||
"job-id", "1000000", "task-id", "station-id", "100+50", "25/100 (25.0%)",
|
||||
"2.50 MH/s", "61 C", "GPU 0", "credential-id", "hash-one", "plain-one", "hash-two", "plain-two",
|
||||
"Hashcat Statistics", "job-session", "hash-file", "mode=3", "base=?d?d", "1/2", "1/1",
|
||||
"2100 MHz", "9000 MHz", "16 lanes", "125000 mW", "2023-11-14T22:13:20Z", "2023-11-14T22:15:00Z",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered job does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldPrimary.Render("Crack Job"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String()),
|
||||
console.StyleBoldPrimary.Render("Tasks"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackTaskState_CRACK_TASK_RUNNING.String()),
|
||||
console.StyleBoldPrimary.Render("Hashcat Statistics"),
|
||||
console.StyleBoldPrimary.Render("Devices"),
|
||||
console.StyleBoldSuccess.Render("Recovered Credentials"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered job is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
if strings.Contains(rawOutput, console.StylePrimary.Render("task-id")) {
|
||||
t.Fatalf("rendered task ID should use the default foreground color:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobsSummarizesExtendedFields(t *testing.T) {
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
Results: []*clientpb.CrackResult{{Hash: "hash", Plaintext: []byte("plain")}},
|
||||
}
|
||||
rawOutput := renderCrackJobs([]*clientpb.CrackJob{job}, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{"job-id", "IN_PROGRESS", "1000", "1"} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered jobs do not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rawOutput, console.StyleBoldPrimary.Render("job-id")) ||
|
||||
!strings.Contains(rawOutput, console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String())) {
|
||||
t.Fatalf("rendered jobs are missing Lip Gloss ID or status styling:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobStatusStyleUsesSemanticPalette(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
want console.TextStyle
|
||||
}{
|
||||
{status: "IN_PROGRESS", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_RUNNING", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_QUEUED", want: console.StyleBold},
|
||||
{status: "CRACK_TASK_LEASED", want: console.StyleBoldWarning},
|
||||
{status: "COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_FAILED", want: console.StyleBoldDanger},
|
||||
{status: "CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "CANCELED", want: console.StyleBoldGray},
|
||||
{status: "CRACK_TASK_CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "UNKNOWN", want: console.StyleBold},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.status, func(t *testing.T) {
|
||||
got := crackJobStatusStyle(test.status)
|
||||
if got.GetBold() != test.want.GetBold() {
|
||||
t.Fatalf("bold = %v, want %v", got.GetBold(), test.want.GetBold())
|
||||
}
|
||||
gotColor := got.GetForeground()
|
||||
wantColor := test.want.GetForeground()
|
||||
if (gotColor == nil) != (wantColor == nil) {
|
||||
t.Fatalf("foreground = %v, want %v", gotColor, wantColor)
|
||||
}
|
||||
if gotColor == nil {
|
||||
return
|
||||
}
|
||||
gotR, gotG, gotB, gotA := gotColor.RGBA()
|
||||
wantR, wantG, wantB, wantA := wantColor.RGBA()
|
||||
if gotR != wantR || gotG != wantG || gotB != wantB || gotA != wantA {
|
||||
t.Fatalf("foreground RGBA = (%d,%d,%d,%d), want (%d,%d,%d,%d)", gotR, gotG, gotB, gotA, wantR, wantG, wantB, wantA)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobStylesErrors(t *testing.T) {
|
||||
rawOutput := renderCrackJob(&clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
Status: clientpb.CrackJobStatus_FAILED,
|
||||
Err: "worker failed",
|
||||
}, settings.SliverDefault)
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldDanger.Render(clientpb.CrackJobStatus_FAILED.String()),
|
||||
console.StyleBoldDanger.Render("Error"),
|
||||
console.StyleDanger.Render("worker failed"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered error is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultCountUsesSummaryField(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{{Plaintext: []byte("not-a-summary-count")}}}
|
||||
field := findProtoField(job.ProtoReflect(), "ResultCount")
|
||||
want := "-"
|
||||
if field != nil {
|
||||
job.ProtoReflect().Set(field, protoreflect.ValueOfUint64(7))
|
||||
want = "7"
|
||||
}
|
||||
if got := crackJobResultCount(job); got != want {
|
||||
t.Fatalf("crackJobResultCount() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultsRenderAuthoritativePlaintextBytes(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{
|
||||
{Hash: "valid", Plaintext: []byte("line\ntext\x1b[31m")},
|
||||
{Hash: "binary", Plaintext: []byte{0xff, 0x00, 0x1b}},
|
||||
{Hash: "nul", Plaintext: []byte{'a', 0, 'b'}},
|
||||
}}
|
||||
results := crackJobResults(job)
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("crackJobResults() = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "linetext[31m" {
|
||||
t.Fatalf("valid UTF-8 plaintext = %q, want sanitized text", results[0].Plaintext)
|
||||
}
|
||||
if results[1].Plaintext != "0xff001b" {
|
||||
t.Fatalf("binary plaintext = %q, want 0xff001b", results[1].Plaintext)
|
||||
}
|
||||
if results[2].Plaintext != "0x610062" {
|
||||
t.Fatalf("NUL plaintext = %q, want 0x610062", results[2].Plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobPrintsChangesAndStopsAtTerminalState(t *testing.T) {
|
||||
inProgress := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}
|
||||
completed := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_COMPLETED}
|
||||
responses := []*clientpb.CrackJob{inProgress, inProgress, completed}
|
||||
requests := 0
|
||||
fetch := func(_ context.Context, id string) (*clientpb.CrackJob, error) {
|
||||
if id != "job-id" {
|
||||
t.Fatalf("job ID = %q", id)
|
||||
}
|
||||
response := responses[requests]
|
||||
requests++
|
||||
return response, nil
|
||||
}
|
||||
updates := make([]clientpb.CrackJobStatus, 0)
|
||||
err := watchCrackJob(context.Background(), "job-id", time.Millisecond, fetch, func(job *clientpb.CrackJob) {
|
||||
updates = append(updates, job.Status)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("watchCrackJob: %v", err)
|
||||
}
|
||||
if requests != 3 {
|
||||
t.Fatalf("requests = %d, want 3", requests)
|
||||
}
|
||||
if len(updates) != 2 || updates[0] != clientpb.CrackJobStatus_IN_PROGRESS || updates[1] != clientpb.CrackJobStatus_COMPLETED {
|
||||
t.Fatalf("updates = %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobCancellationAndValidation(t *testing.T) {
|
||||
fetch := func(_ context.Context, _ string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := watchCrackJob(ctx, "job-id", time.Hour, fetch, nil); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("watchCrackJob cancellation error = %v", err)
|
||||
}
|
||||
if err := watchCrackJob(context.Background(), "job-id", 0, fetch, nil); err == nil {
|
||||
t.Fatal("watchCrackJob accepted zero poll interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsStripTerminalControlCharacters(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash\u001b[31m","plaintext":"cGxhaW4KdGV4dA=="}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if strings.ContainsAny(results[0].Hash, "\x1b\n\r") || strings.ContainsAny(results[0].Plaintext, "\x1b\n\r") {
|
||||
t.Fatalf("control characters were not stripped: %#v", results[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsRenderNULPlaintextAsHex(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash","plaintext":"YQBi"}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "0x610062" {
|
||||
t.Fatalf("plaintext = %q, want 0x610062", results[0].Plaintext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const crackTopWarningRunes = 256
|
||||
|
||||
// crackTopRPC is the complete wire-level dependency of the crack top client.
|
||||
// Keeping it narrow also makes it difficult for the dashboard to accidentally
|
||||
// fall back to RPCs that expose crack targets or recovered material.
|
||||
type crackTopRPC interface {
|
||||
CrackTop(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
type crackTopTaskTelemetry struct {
|
||||
status *crackStatusView
|
||||
parseErr bool
|
||||
progressExact bool
|
||||
}
|
||||
|
||||
// crackTopSnapshot is the client-side representation consumed by the existing
|
||||
// dashboard telemetry code. Jobs and stations contain only fields supplied by
|
||||
// the narrow CrackTop RPC; parsed status is kept separately so no sensitive raw
|
||||
// status JSON has to be reconstructed.
|
||||
type crackTopSnapshot struct {
|
||||
Jobs []*clientpb.CrackJob
|
||||
Stations []*clientpb.Crackstation
|
||||
RefreshedAt time.Time
|
||||
taskTelemetry map[string]crackTopTaskTelemetry
|
||||
}
|
||||
|
||||
// CrackTop snapshots are atomic. A successful refresh replaces all previously
|
||||
// displayed server state, while a failed refresh returns no snapshot and leaves
|
||||
// the model's existing value untouched.
|
||||
func mergeCrackTopSnapshot(_ *crackTopSnapshot, incoming *crackTopSnapshot) *crackTopSnapshot {
|
||||
return incoming
|
||||
}
|
||||
|
||||
func loadCrackTopSnapshot(ctx context.Context, rpc crackTopRPC, perCallTimeout time.Duration, completionTime time.Time) (*crackTopSnapshot, error) {
|
||||
if rpc == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("rpc client is unavailable"))
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if perCallTimeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, perCallTimeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
wire, err := rpc.CrackTop(ctx, &commonpb.Empty{})
|
||||
if completionTime.IsZero() {
|
||||
completionTime = time.Now()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", err)
|
||||
}
|
||||
if wire == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("server returned an empty response"))
|
||||
}
|
||||
|
||||
refreshedAt := completionTime
|
||||
if wire.GetObservedAt() != 0 {
|
||||
refreshedAt = time.Unix(wire.GetObservedAt(), 0)
|
||||
}
|
||||
snapshot := &crackTopSnapshot{
|
||||
Jobs: make([]*clientpb.CrackJob, len(wire.GetJobs())),
|
||||
Stations: make([]*clientpb.Crackstation, len(wire.GetCrackstations())),
|
||||
RefreshedAt: refreshedAt,
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
for index, job := range wire.GetJobs() {
|
||||
snapshot.Jobs[index] = crackTopJobFromWire(job, snapshot.taskTelemetry)
|
||||
}
|
||||
for index, station := range wire.GetCrackstations() {
|
||||
snapshot.Stations[index] = crackTopStationFromWire(station)
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func crackTopJobFromWire(wire *clientpb.CrackTopJob, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackJob {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: wire.GetAttackMode(),
|
||||
HashType: wire.GetHashType(),
|
||||
}
|
||||
if wire.HashMode != nil {
|
||||
hashMode := wire.GetHashMode()
|
||||
command.HashMode = &hashMode
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: wire.GetID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Status: wire.GetStatus(),
|
||||
Command: command,
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
ResultCount: wire.GetResultCount(),
|
||||
Tasks: make([]*clientpb.CrackTask, len(wire.GetTasks())),
|
||||
}
|
||||
for index, task := range wire.GetTasks() {
|
||||
job.Tasks[index] = crackTopTaskFromWire(task, telemetry)
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
func crackTopTaskFromWire(wire *clientpb.CrackTopTask, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackTask {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
task := &clientpb.CrackTask{
|
||||
ID: wire.GetID(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
StartedAt: wire.GetStartedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Kind: wire.GetKind(),
|
||||
State: wire.GetState(),
|
||||
Attempt: wire.GetAttempt(),
|
||||
LeaseExpiresAt: wire.GetLeaseExpiresAt(),
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
LastHeartbeatAt: wire.GetLastHeartbeatAt(),
|
||||
ShardSkip: wire.GetShardSkip(),
|
||||
ShardLimit: wire.GetShardLimit(),
|
||||
}
|
||||
|
||||
taskID := strings.TrimSpace(task.GetID())
|
||||
if telemetry == nil || taskID == "" {
|
||||
return task
|
||||
}
|
||||
metadata := crackTopTaskTelemetry{
|
||||
parseErr: wire.GetStatusParseError(),
|
||||
progressExact: wire.GetProgressExact(),
|
||||
}
|
||||
if wire.GetStatusAvailable() && !metadata.parseErr {
|
||||
metadata.status = &crackStatusView{Devices: crackTopDevicesFromWire(wire.GetDevices())}
|
||||
if metadata.progressExact || wire.GetShardSkip() == 0 {
|
||||
metadata.status.ProgressCurrent = wire.GetProgressCurrent()
|
||||
metadata.status.ProgressTotal = wire.GetProgressTotal()
|
||||
}
|
||||
}
|
||||
if metadata.status != nil || metadata.parseErr {
|
||||
telemetry[taskID] = metadata
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func crackTopDevicesFromWire(devices []*clientpb.CrackTopDevice) []crackDeviceView {
|
||||
converted := make([]crackDeviceView, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
view := crackDeviceView{ID: device.GetID()}
|
||||
if device.GetSpeedAvailable() {
|
||||
view.Speed = strconv.FormatUint(device.GetSpeed(), 10)
|
||||
}
|
||||
if device.GetTemperatureAvailable() {
|
||||
view.Temperature = strconv.FormatFloat(device.GetTemperature(), 'f', -1, 64)
|
||||
}
|
||||
if device.GetUtilizationAvailable() {
|
||||
view.Utilization = strconv.FormatFloat(device.GetUtilization(), 'f', -1, 64)
|
||||
}
|
||||
converted = append(converted, view)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func crackTopStationFromWire(wire *clientpb.CrackTopStation) *clientpb.Crackstation {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
station := &clientpb.Crackstation{
|
||||
ID: wire.GetID(),
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
}
|
||||
if wire.GetStatusAvailable() {
|
||||
station.Status = &clientpb.CrackstationStatus{
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
State: wire.GetState(),
|
||||
CurrentCrackJobID: wire.GetCurrentCrackJobID(),
|
||||
IsSyncing: wire.GetIsSyncing(),
|
||||
}
|
||||
}
|
||||
return station
|
||||
}
|
||||
|
||||
func crackTopContextError(scope string, err error) error {
|
||||
message := crackTopSanitizeError(err)
|
||||
if strings.TrimSpace(scope) == "" {
|
||||
return errors.New(message)
|
||||
}
|
||||
return fmt.Errorf("%s: %s", strings.TrimSpace(scope), message)
|
||||
}
|
||||
|
||||
func crackTopWarningText(message string) string {
|
||||
return crackTopSanitizeError(errors.New(message))
|
||||
}
|
||||
|
||||
func crackTopSanitizeError(err error) string {
|
||||
if err == nil {
|
||||
return "unknown error"
|
||||
}
|
||||
message := ansi.Strip(err.Error())
|
||||
message = strings.Map(func(character rune) rune {
|
||||
if unicode.IsSpace(character) {
|
||||
return ' '
|
||||
}
|
||||
if !unicode.IsPrint(character) {
|
||||
return -1
|
||||
}
|
||||
return character
|
||||
}, message)
|
||||
message = strings.Join(strings.Fields(message), " ")
|
||||
if message == "" {
|
||||
return "unknown error"
|
||||
}
|
||||
runes := []rune(message)
|
||||
if len(runes) > crackTopWarningRunes {
|
||||
message = string(runes[:crackTopWarningRunes-1]) + "…"
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type crackTopRPCTestClient struct {
|
||||
calls atomic.Int32
|
||||
call func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
func (client *crackTopRPCTestClient) CrackTop(ctx context.Context, request *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error) {
|
||||
client.calls.Add(1)
|
||||
if client.call == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return client.call(ctx, request)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally validates every field in one complete lean-snapshot conversion.
|
||||
func TestLoadCrackTopSnapshotUsesOneRPCAndConvertsLeanSnapshot(t *testing.T) {
|
||||
hashMode := uint32(5600)
|
||||
wire := &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: 1_700_000_123,
|
||||
Jobs: []*clientpb.CrackTopJob{
|
||||
{
|
||||
ID: "job-one",
|
||||
CreatedAt: "2026-09-12T10:00:00Z",
|
||||
CompletedAt: "",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
UpdatedAt: 1_700_000_120,
|
||||
ResultCount: 7,
|
||||
AttackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
HashType: clientpb.HashType_NET_NTLM_V2,
|
||||
HashMode: &hashMode,
|
||||
Tasks: []*clientpb.CrackTopTask{
|
||||
{
|
||||
ID: "task-live",
|
||||
HostUUID: "worker-one",
|
||||
CreatedAt: 11,
|
||||
StartedAt: 12,
|
||||
CompletedAt: 13,
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
Attempt: 2,
|
||||
LeaseExpiresAt: 1_700_000_500,
|
||||
UpdatedAt: 14,
|
||||
LastHeartbeatAt: 15,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 200,
|
||||
ProgressCurrent: "125",
|
||||
ProgressTotal: "300",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: false,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{
|
||||
{
|
||||
ID: "gpu-0",
|
||||
Speed: 9_876_543,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 71.5,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 99.25,
|
||||
UtilizationAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "gpu-1", Speed: 123, Temperature: 45, Utilization: 67},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{
|
||||
ID: "task-malformed",
|
||||
HostUUID: "worker-two",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
ProgressCurrent: "sensitive-raw-data-must-not-be-rebuilt",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{ID: "job-two", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
},
|
||||
Crackstations: []*clientpb.CrackTopStation{
|
||||
{
|
||||
ID: "station-one",
|
||||
Name: "rig-one",
|
||||
HostUUID: "worker-one",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: "job-one",
|
||||
IsSyncing: false,
|
||||
StatusAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "station-two", Name: "rig-two", HostUUID: "worker-two"},
|
||||
},
|
||||
}
|
||||
wireBefore := proto.Clone(wire).(*clientpb.CrackTopSnapshot)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, request *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
if ctx == nil {
|
||||
t.Fatal("CrackTop received a nil context")
|
||||
}
|
||||
if request == nil {
|
||||
t.Fatal("CrackTop received a nil request")
|
||||
}
|
||||
return wire, nil
|
||||
},
|
||||
}
|
||||
|
||||
fallback := time.Unix(1_600_000_000, 987_654_321)
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if want := time.Unix(wire.GetObservedAt(), 0); !snapshot.RefreshedAt.Equal(want) {
|
||||
t.Fatalf("RefreshedAt = %s, want server observation time %s", snapshot.RefreshedAt, want)
|
||||
}
|
||||
if !proto.Equal(wire, wireBefore) {
|
||||
t.Fatal("loader mutated the wire snapshot")
|
||||
}
|
||||
|
||||
if len(snapshot.Jobs) != 3 || snapshot.Jobs[1] != nil || snapshot.Jobs[2].GetID() != "job-two" {
|
||||
t.Fatalf("jobs did not preserve wire order and nil entries: %#v", snapshot.Jobs)
|
||||
}
|
||||
job := snapshot.Jobs[0]
|
||||
if job.GetID() != "job-one" || job.GetCreatedAt() != wire.Jobs[0].GetCreatedAt() || job.GetCompletedAt() != wire.Jobs[0].GetCompletedAt() ||
|
||||
job.GetStatus() != clientpb.CrackJobStatus_IN_PROGRESS || job.GetUpdatedAt() != 1_700_000_120 || job.GetResultCount() != 7 {
|
||||
t.Fatalf("converted job metadata = %#v", job)
|
||||
}
|
||||
if job.GetCommand() == nil || job.GetCommand().GetAttackMode() != clientpb.CrackAttackMode_BRUTEFORCE ||
|
||||
job.GetCommand().GetHashType() != clientpb.HashType_NET_NTLM_V2 || job.GetCommand().HashMode == nil || job.GetCommand().GetHashMode() != hashMode {
|
||||
t.Fatalf("converted job command = %#v", job.GetCommand())
|
||||
}
|
||||
if job.GetErr() != "" || job.GetResultFileID() != "" || job.GetKeyspace() != "" || len(job.GetResults()) != 0 ||
|
||||
len(job.GetCommand().GetHashes()) != 0 || len(job.GetCommand().GetPositionalArguments()) != 0 || len(job.GetCommand().GetCredentialIDs()) != 0 {
|
||||
t.Fatalf("lean job conversion populated target or result fields: %#v", job)
|
||||
}
|
||||
if len(job.GetTasks()) != 3 || job.GetTasks()[1] != nil {
|
||||
t.Fatalf("tasks did not preserve wire order and nil entries: %#v", job.GetTasks())
|
||||
}
|
||||
task := job.GetTasks()[0]
|
||||
if task.GetID() != "task-live" || task.GetHostUUID() != "worker-one" || task.GetCreatedAt() != 11 || task.GetStartedAt() != 12 ||
|
||||
task.GetCompletedAt() != 13 || task.GetKind() != clientpb.CrackTaskKind_CRACK_TASK_CRACK ||
|
||||
task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING || task.GetAttempt() != 2 ||
|
||||
task.GetLeaseExpiresAt() != 1_700_000_500 || task.GetUpdatedAt() != 14 || task.GetLastHeartbeatAt() != 15 ||
|
||||
task.GetShardSkip() != 100 || task.GetShardLimit() != 200 {
|
||||
t.Fatalf("converted task metadata = %#v", task)
|
||||
}
|
||||
if task.GetErr() != "" || len(task.GetStdout()) != 0 || len(task.GetStderr()) != 0 || task.GetCommand() != nil ||
|
||||
task.GetLeaseToken() != "" || task.GetKeyspace() != "" || len(task.GetLatestStatusJSON()) != 0 || len(task.GetRecoveredJSON()) != 0 {
|
||||
t.Fatalf("lean task conversion populated execution or raw telemetry fields: %#v", task)
|
||||
}
|
||||
|
||||
metadata, ok := snapshot.taskTelemetry["task-live"]
|
||||
if !ok || metadata.parseErr || metadata.status == nil {
|
||||
t.Fatalf("live task telemetry = %#v, present=%v", metadata, ok)
|
||||
}
|
||||
if !metadata.progressExact {
|
||||
t.Fatal("server-declared exact progress was discarded")
|
||||
}
|
||||
if metadata.status.ProgressCurrent != "125" || metadata.status.ProgressTotal != "300" || len(metadata.status.Devices) != 2 {
|
||||
t.Fatalf("converted status = %#v", metadata.status)
|
||||
}
|
||||
firstDevice := metadata.status.Devices[0]
|
||||
if firstDevice.ID != "gpu-0" || firstDevice.Speed != "9876543" || firstDevice.Temperature != "71.5" || firstDevice.Utilization != "99.25" {
|
||||
t.Fatalf("converted available device telemetry = %#v", firstDevice)
|
||||
}
|
||||
secondDevice := metadata.status.Devices[1]
|
||||
if secondDevice.ID != "gpu-1" || secondDevice.Speed != "" || secondDevice.Temperature != "" || secondDevice.Utilization != "" {
|
||||
t.Fatalf("unavailable device telemetry was treated as available: %#v", secondDevice)
|
||||
}
|
||||
malformed, ok := snapshot.taskTelemetry["task-malformed"]
|
||||
if !ok || !malformed.parseErr || malformed.status != nil {
|
||||
t.Fatalf("malformed task telemetry = %#v, present=%v", malformed, ok)
|
||||
}
|
||||
|
||||
if len(snapshot.Stations) != 3 || snapshot.Stations[1] != nil {
|
||||
t.Fatalf("stations did not preserve wire order and nil entries: %#v", snapshot.Stations)
|
||||
}
|
||||
station := snapshot.Stations[0]
|
||||
if station.GetID() != "station-one" || station.GetName() != "rig-one" || station.GetHostUUID() != "worker-one" || station.GetStatus() == nil ||
|
||||
station.GetStatus().GetName() != "rig-one" || station.GetStatus().GetHostUUID() != "worker-one" ||
|
||||
station.GetStatus().GetState() != clientpb.States_CRACKING || station.GetStatus().GetCurrentCrackJobID() != "job-one" || station.GetStatus().GetIsSyncing() {
|
||||
t.Fatalf("converted station = %#v", station)
|
||||
}
|
||||
if station.GetOperatorName() != "" || station.GetGOOS() != "" || station.GetGOARCH() != "" || station.GetVersion() != "" ||
|
||||
len(station.GetBenchmarks()) != 0 || len(station.GetCUDA()) != 0 || len(station.GetMetal()) != 0 || len(station.GetOpenCL()) != 0 ||
|
||||
len(station.GetHIP()) != 0 || len(station.GetCapabilities()) != 0 {
|
||||
t.Fatalf("lean station conversion populated private inventory fields: %#v", station)
|
||||
}
|
||||
if snapshot.Stations[2].GetStatus() != nil {
|
||||
t.Fatalf("station without available status got a fabricated status: %#v", snapshot.Stations[2])
|
||||
}
|
||||
|
||||
*snapshot.Jobs[0].Command.HashMode = 1000
|
||||
snapshot.Jobs[0].Tasks[0].ID = "changed-task"
|
||||
snapshot.Stations[0].Status.Name = "changed-station"
|
||||
metadata.status.Devices[0].ID = "changed-device"
|
||||
if wire.Jobs[0].GetHashMode() != 5600 || wire.Jobs[0].Tasks[0].GetID() != "task-live" ||
|
||||
wire.Crackstations[0].GetName() != "rig-one" || wire.Jobs[0].Tasks[0].Devices[0].GetID() != "gpu-0" {
|
||||
t.Fatal("converted snapshot aliases mutable wire data")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally covers the full loader-to-dashboard telemetry path.
|
||||
func TestLoadCrackTopSnapshotFeedsDashboardTelemetry(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: now.Unix(),
|
||||
Jobs: []*clientpb.CrackTopJob{{
|
||||
ID: crackTopTestJobID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTopTask{{
|
||||
ID: "task-wire",
|
||||
HostUUID: "worker-wire",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
LeaseExpiresAt: now.Add(time.Minute).Unix(),
|
||||
LastHeartbeatAt: now.Unix(),
|
||||
ProgressCurrent: "25",
|
||||
ProgressTotal: "100",
|
||||
StatusAvailable: true,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{{
|
||||
ID: "gpu-0",
|
||||
Speed: 125_000,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 67,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 98,
|
||||
UtilizationAvailable: true,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
Crackstations: []*clientpb.CrackTopStation{{
|
||||
ID: "station-wire",
|
||||
Name: "Wire Worker",
|
||||
HostUUID: "worker-wire",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: crackTopTestJobID,
|
||||
StatusAvailable: true,
|
||||
}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
dashboard := buildCrackTopDashboard(snapshot)
|
||||
if len(dashboard.Jobs) != 1 || !dashboard.Jobs[0].ProgressKnown || dashboard.Jobs[0].Progress != 0.25 {
|
||||
t.Fatalf("job progress from narrow telemetry = %#v", dashboard.Jobs)
|
||||
}
|
||||
if !dashboard.ClusterRateKnown || !dashboard.ClusterComplete || dashboard.ClusterRate != 125_000 {
|
||||
t.Fatalf("cluster telemetry = rate:%d known:%v complete:%v", dashboard.ClusterRate, dashboard.ClusterRateKnown, dashboard.ClusterComplete)
|
||||
}
|
||||
if len(dashboard.Workers) != 1 {
|
||||
t.Fatalf("workers = %#v, want one", dashboard.Workers)
|
||||
}
|
||||
worker := dashboard.Workers[0]
|
||||
if !worker.TelemetryLive || !worker.RateKnown || worker.Rate != 125_000 || worker.Devices != 1 ||
|
||||
!worker.HasTemp || worker.Temperature != 67 || !worker.HasUtil || worker.Utilization != 98 {
|
||||
t.Fatalf("worker telemetry from narrow snapshot = %#v", worker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotUsesCompletionTimeOnlyWithoutObservedAt(t *testing.T) {
|
||||
fallback := time.Unix(1_700_000_000, 123_456_789)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 0, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if !snapshot.RefreshedAt.Equal(fallback) {
|
||||
t.Fatalf("RefreshedAt = %s, want deterministic completion time %s", snapshot.RefreshedAt, fallback)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesDeadlineAndCancelsCallContext(t *testing.T) {
|
||||
var callContext context.Context
|
||||
var deadline time.Time
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
callContext = ctx
|
||||
var ok bool
|
||||
deadline, ok = ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("CrackTop context has no deadline")
|
||||
}
|
||||
return &clientpb.CrackTopSnapshot{ObservedAt: 1}, nil
|
||||
},
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
timeout := time.Second
|
||||
_, err := loadCrackTopSnapshot(t.Context(), client, timeout, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if deadline.Before(started) || deadline.After(started.Add(timeout+100*time.Millisecond)) {
|
||||
t.Fatalf("RPC deadline = %s, want about %s after %s", deadline, timeout, started)
|
||||
}
|
||||
select {
|
||||
case <-callContext.Done():
|
||||
if !errors.Is(callContext.Err(), context.Canceled) {
|
||||
t.Fatalf("completed call context error = %v, want context canceled", callContext.Err())
|
||||
}
|
||||
default:
|
||||
t.Fatal("completed call context was not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotReturnsNilOnTimeout(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 10*time.Millisecond, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("timed-out load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context deadline exceeded" {
|
||||
t.Fatalf("timed-out load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesParentCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(ctx, client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("canceled load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context canceled" {
|
||||
t.Fatalf("canceled load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotSanitizesRPCErrorAndRejectsEmptyResponse(t *testing.T) {
|
||||
t.Run("rpc error", func(t *testing.T) {
|
||||
raw := errors.New("bad\r\n\x1b[31mserver\x1b[0m\tfailure")
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return nil, raw
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("failed load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: bad server failure" {
|
||||
t.Fatalf("sanitized error = %v", err)
|
||||
}
|
||||
if errors.Is(err, raw) {
|
||||
t.Fatal("sanitized error retained the raw RPC error in its unwrap chain")
|
||||
}
|
||||
if strings.ContainsAny(err.Error(), "\r\n\t\x1b") {
|
||||
t.Fatalf("error still contains terminal controls: %q", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil response", func(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{}
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("empty load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: server returned an empty response" {
|
||||
t.Fatalf("empty response error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const crackTopMaxProgressTextSize = 148
|
||||
|
||||
type crackTopDashboard struct {
|
||||
Jobs []crackTopJobRow
|
||||
Workers []crackTopWorkerRow
|
||||
ActiveJobs int
|
||||
OnlineWorkers int
|
||||
Unavailable int
|
||||
Recovered uint64
|
||||
ClusterRate uint64
|
||||
ClusterRateKnown bool
|
||||
ClusterComplete bool
|
||||
ExpectedWorkers int
|
||||
ReportingWorkers int
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressJobs int
|
||||
}
|
||||
|
||||
type crackTopJobRow struct {
|
||||
ID string
|
||||
Status clientpb.CrackJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressComplete bool
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Workers int
|
||||
Reporting int
|
||||
Results uint64
|
||||
Mode string
|
||||
TelemetryErrors int
|
||||
}
|
||||
|
||||
type crackTopWorkerRow struct {
|
||||
ID string
|
||||
Name string
|
||||
State string
|
||||
Online bool
|
||||
Syncing bool
|
||||
JobID string
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Devices int
|
||||
Temperature float64
|
||||
HasTemp bool
|
||||
Utilization float64
|
||||
HasUtil bool
|
||||
LastSeen time.Time
|
||||
TelemetryLive bool
|
||||
}
|
||||
|
||||
type crackTopTaskSample struct {
|
||||
jobID string
|
||||
task *clientpb.CrackTask
|
||||
status *crackStatusView
|
||||
statusErr error
|
||||
rate uint64
|
||||
rateKnown bool
|
||||
devices int
|
||||
temperature float64
|
||||
hasTemp bool
|
||||
utilization float64
|
||||
hasUtil bool
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
type crackTopProgressAccumulator struct {
|
||||
completed *big.Rat
|
||||
total *big.Rat
|
||||
}
|
||||
|
||||
func newCrackTopProgressAccumulator() *crackTopProgressAccumulator {
|
||||
return &crackTopProgressAccumulator{completed: new(big.Rat), total: new(big.Rat)}
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) add(fraction, weight *big.Rat) {
|
||||
if a == nil || fraction == nil || weight == nil || weight.Sign() <= 0 {
|
||||
return
|
||||
}
|
||||
clamped := new(big.Rat).Set(fraction)
|
||||
if clamped.Sign() < 0 {
|
||||
clamped.SetInt64(0)
|
||||
}
|
||||
if clamped.Cmp(big.NewRat(1, 1)) > 0 {
|
||||
clamped.SetInt64(1)
|
||||
}
|
||||
a.completed.Add(a.completed, new(big.Rat).Mul(clamped, weight))
|
||||
a.total.Add(a.total, weight)
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) value() (float64, bool) {
|
||||
if a == nil || a.total.Sign() <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
fraction, _ := new(big.Rat).Quo(a.completed, a.total).Float64()
|
||||
if math.IsNaN(fraction) || math.IsInf(fraction, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return math.Max(0, math.Min(1, fraction)), true
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Jobs, task telemetry, and connected workers are normalized in one immutable snapshot pass.
|
||||
func buildCrackTopDashboard(snapshot *crackTopSnapshot) crackTopDashboard {
|
||||
dashboard := crackTopDashboard{}
|
||||
if snapshot == nil {
|
||||
return dashboard
|
||||
}
|
||||
|
||||
stations := make(map[string]*clientpb.Crackstation, len(snapshot.Stations))
|
||||
for _, station := range snapshot.Stations {
|
||||
if station == nil {
|
||||
continue
|
||||
}
|
||||
hostID := crackTopStationID(station)
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := stations[hostID]; !exists {
|
||||
stations[hostID] = station
|
||||
}
|
||||
}
|
||||
dashboard.OnlineWorkers = len(stations)
|
||||
|
||||
jobs := make([]*clientpb.CrackJob, 0, len(snapshot.Jobs))
|
||||
for _, job := range snapshot.Jobs {
|
||||
if job != nil && strings.TrimSpace(job.GetID()) != "" {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(jobs, func(i, j int) bool {
|
||||
left := crackJobSortTime(jobs[i])
|
||||
right := crackJobSortTime(jobs[j])
|
||||
if left.Equal(right) {
|
||||
return jobs[i].GetID() < jobs[j].GetID()
|
||||
}
|
||||
return left.After(right)
|
||||
})
|
||||
|
||||
globalProgress := newCrackTopProgressAccumulator()
|
||||
workerSamples := map[string]crackTopTaskSample{}
|
||||
for _, job := range jobs {
|
||||
row, samples, progress := crackTopJobTelemetry(job, stations, snapshot.RefreshedAt, snapshot.taskTelemetry)
|
||||
dashboard.Jobs = append(dashboard.Jobs, row)
|
||||
dashboard.Recovered = saturatingUint64Add(dashboard.Recovered, row.Results)
|
||||
if !crackJobTerminal(job) {
|
||||
dashboard.ActiveJobs++
|
||||
if row.ProgressComplete {
|
||||
dashboard.ProgressJobs++
|
||||
}
|
||||
if progress != nil {
|
||||
globalProgress.completed.Add(globalProgress.completed, progress.completed)
|
||||
globalProgress.total.Add(globalProgress.total, progress.total)
|
||||
}
|
||||
for hostID, sample := range samples {
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if current, ok := workerSamples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], snapshot.RefreshedAt) {
|
||||
workerSamples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dashboard.Progress, dashboard.ProgressKnown = globalProgress.value()
|
||||
dashboard.ProgressKnown = dashboard.ProgressKnown && dashboard.ProgressJobs == dashboard.ActiveJobs
|
||||
if dashboard.ActiveJobs == 0 {
|
||||
dashboard.Progress = 0
|
||||
dashboard.ProgressKnown = false
|
||||
}
|
||||
|
||||
workerIDs := make(map[string]struct{}, len(stations)+len(workerSamples))
|
||||
for hostID := range stations {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerSamples {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerIDs {
|
||||
station := stations[hostID]
|
||||
sample, hasSample := workerSamples[hostID]
|
||||
worker := crackTopWorkerRow{ID: hostID, Online: station != nil}
|
||||
if station != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetName())
|
||||
if worker.Name == "" && station.GetStatus() != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetStatus().GetName())
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
worker.State = status.GetState().String()
|
||||
worker.Syncing = status.GetIsSyncing()
|
||||
worker.JobID = strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
}
|
||||
}
|
||||
if worker.Name == "" {
|
||||
worker.Name = crackTopShortID(hostID)
|
||||
}
|
||||
if worker.State == "" {
|
||||
if worker.Online {
|
||||
worker.State = "ONLINE"
|
||||
} else {
|
||||
worker.State = "OFFLINE"
|
||||
}
|
||||
}
|
||||
if crackTopStationExpectsTelemetry(station) {
|
||||
dashboard.ExpectedWorkers++
|
||||
}
|
||||
if hasSample {
|
||||
if worker.JobID == "" && !worker.Online {
|
||||
worker.JobID = sample.jobID
|
||||
}
|
||||
worker.LastSeen = sample.lastSeen
|
||||
if crackTopTaskSampleIsLive(sample, station, snapshot.RefreshedAt) {
|
||||
worker.TelemetryLive = true
|
||||
worker.Rate = sample.rate
|
||||
worker.RateKnown = sample.rateKnown
|
||||
worker.Devices = sample.devices
|
||||
worker.Temperature = sample.temperature
|
||||
worker.HasTemp = sample.hasTemp
|
||||
worker.Utilization = sample.utilization
|
||||
worker.HasUtil = sample.hasUtil
|
||||
}
|
||||
}
|
||||
if !worker.Online {
|
||||
dashboard.Unavailable++
|
||||
}
|
||||
if worker.RateKnown {
|
||||
dashboard.ClusterRate = saturatingUint64Add(dashboard.ClusterRate, worker.Rate)
|
||||
dashboard.ClusterRateKnown = true
|
||||
dashboard.ReportingWorkers++
|
||||
}
|
||||
dashboard.Workers = append(dashboard.Workers, worker)
|
||||
}
|
||||
dashboard.ClusterComplete = dashboard.ReportingWorkers == dashboard.ExpectedWorkers
|
||||
dashboard.ClusterRateKnown = dashboard.ReportingWorkers > 0 || dashboard.ClusterComplete
|
||||
|
||||
sort.SliceStable(dashboard.Workers, func(i, j int) bool {
|
||||
left, right := dashboard.Workers[i], dashboard.Workers[j]
|
||||
if left.Online != right.Online {
|
||||
return left.Online
|
||||
}
|
||||
if left.RateKnown != right.RateKnown {
|
||||
return left.RateKnown
|
||||
}
|
||||
if left.Rate != right.Rate {
|
||||
return left.Rate > right.Rate
|
||||
}
|
||||
leftName := strings.ToLower(left.Name)
|
||||
rightName := strings.ToLower(right.Name)
|
||||
if leftName != rightName {
|
||||
return leftName < rightName
|
||||
}
|
||||
return left.ID < right.ID
|
||||
})
|
||||
return dashboard
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Job telemetry combines task state, liveness, progress, and device metrics in one aggregation pass.
|
||||
func crackTopJobTelemetry(
|
||||
job *clientpb.CrackJob,
|
||||
stations map[string]*clientpb.Crackstation,
|
||||
now time.Time,
|
||||
taskTelemetry map[string]crackTopTaskTelemetry,
|
||||
) (crackTopJobRow, map[string]crackTopTaskSample, *crackTopProgressAccumulator) {
|
||||
row := crackTopJobRow{
|
||||
ID: strings.TrimSpace(job.GetID()),
|
||||
Status: job.GetStatus(),
|
||||
CreatedAt: crackTopParseTime(job.GetCreatedAt()),
|
||||
UpdatedAt: crackTopParseUnix(job.GetUpdatedAt()),
|
||||
Results: job.GetResultCount(),
|
||||
Mode: crackTopJobMode(job),
|
||||
}
|
||||
progress := newCrackTopProgressAccumulator()
|
||||
samples := map[string]crackTopTaskSample{}
|
||||
workers := map[string]struct{}{}
|
||||
workTasks := 0
|
||||
knownProgressTasks := 0
|
||||
allProgressWeightsKnown := true
|
||||
|
||||
for _, task := range crackJobTasks(job) {
|
||||
if !crackTopCrackTask(task, taskTelemetry) {
|
||||
continue
|
||||
}
|
||||
workTasks++
|
||||
hostID := strings.TrimSpace(task.GetHostUUID())
|
||||
if hostID != "" {
|
||||
workers[hostID] = struct{}{}
|
||||
}
|
||||
status, statusErr := crackTaskStatus(task)
|
||||
progressExact := task.GetShardSkip() == 0
|
||||
if metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]; ok {
|
||||
status = metadata.status
|
||||
statusErr = nil
|
||||
progressExact = metadata.progressExact || task.GetShardSkip() == 0
|
||||
if metadata.parseErr {
|
||||
statusErr = errors.New("crack status telemetry could not be parsed")
|
||||
}
|
||||
}
|
||||
if statusErr != nil {
|
||||
row.TelemetryErrors++
|
||||
}
|
||||
fraction, weight, known := crackTopTaskProgress(task, status, progressExact)
|
||||
if known {
|
||||
progress.add(fraction, weight)
|
||||
knownProgressTasks++
|
||||
} else if weight := crackTopAssignedShardWeight(task); weight != nil {
|
||||
// Unknown work may already be complete, so zero is only a lower
|
||||
// bound. Including its weight prevents known small shards from
|
||||
// overstating the job's completion percentage.
|
||||
progress.add(new(big.Rat), weight)
|
||||
} else {
|
||||
allProgressWeightsKnown = false
|
||||
}
|
||||
sample := crackTopTaskSample{
|
||||
jobID: row.ID,
|
||||
task: task,
|
||||
status: status,
|
||||
statusErr: statusErr,
|
||||
lastSeen: crackTopTaskTime(task),
|
||||
}
|
||||
if status != nil {
|
||||
sample.rate, sample.rateKnown = crackTopStatusRate(status)
|
||||
sample.devices = len(status.Devices)
|
||||
sample.temperature, sample.hasTemp = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Temperature })
|
||||
sample.utilization, sample.hasUtil = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Utilization })
|
||||
}
|
||||
if hostID != "" {
|
||||
if current, ok := samples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], now) {
|
||||
samples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row.Workers = len(workers)
|
||||
row.Progress, row.ProgressKnown = progress.value()
|
||||
row.ProgressKnown = row.ProgressKnown && workTasks > 0 && allProgressWeightsKnown
|
||||
row.ProgressComplete = row.ProgressKnown && knownProgressTasks == workTasks
|
||||
if job.GetStatus() == clientpb.CrackJobStatus_COMPLETED {
|
||||
row.Progress = 1
|
||||
row.ProgressKnown = true
|
||||
row.ProgressComplete = true
|
||||
}
|
||||
for hostID, sample := range samples {
|
||||
if crackTopTaskSampleIsLive(sample, stations[hostID], now) && sample.rateKnown {
|
||||
row.Rate = saturatingUint64Add(row.Rate, sample.rate)
|
||||
row.RateKnown = true
|
||||
row.Reporting++
|
||||
}
|
||||
}
|
||||
return row, samples, progress
|
||||
}
|
||||
|
||||
func crackTopTaskProgress(task *clientpb.CrackTask, status *crackStatusView, progressExact bool) (*big.Rat, *big.Rat, bool) {
|
||||
if task == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
weight := crackTopAssignedShardWeight(task)
|
||||
if weight == nil {
|
||||
weight = new(big.Rat)
|
||||
}
|
||||
state := task.GetState()
|
||||
if state == clientpb.CrackTaskState_CRACK_TASK_COMPLETED {
|
||||
if weight.Sign() <= 0 && status != nil {
|
||||
_, total, ok := crackTopProgressPair(status)
|
||||
if ok {
|
||||
weight.Set(total)
|
||||
}
|
||||
}
|
||||
if weight.Sign() > 0 {
|
||||
return big.NewRat(1, 1), weight, true
|
||||
}
|
||||
}
|
||||
|
||||
current, total, ok := crackTopProgressPair(status)
|
||||
if ok && (task.GetShardSkip() == 0 || progressExact) {
|
||||
fraction := new(big.Rat).Quo(current, total)
|
||||
if weight.Sign() <= 0 {
|
||||
weight.Set(total)
|
||||
}
|
||||
return fraction, weight, weight.Sign() > 0
|
||||
}
|
||||
if weight.Sign() > 0 && (state == clientpb.CrackTaskState_CRACK_TASK_QUEUED || state == clientpb.CrackTaskState_CRACK_TASK_LEASED) {
|
||||
return new(big.Rat), weight, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func crackTopAssignedShardWeight(task *clientpb.CrackTask) *big.Rat {
|
||||
if task == nil || task.GetShardLimit() == 0 {
|
||||
return nil
|
||||
}
|
||||
return new(big.Rat).SetInt(new(big.Int).SetUint64(task.GetShardLimit()))
|
||||
}
|
||||
|
||||
func crackTopProgressPair(status *crackStatusView) (*big.Rat, *big.Rat, bool) {
|
||||
if status == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
current, currentOK := crackTopRat(status.ProgressCurrent)
|
||||
total, totalOK := crackTopRat(status.ProgressTotal)
|
||||
if !currentOK || !totalOK || total.Sign() <= 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return current, total, true
|
||||
}
|
||||
|
||||
func crackTopRat(value string) (*big.Rat, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || len(value) > crackTopMaxProgressTextSize {
|
||||
return nil, false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < '0' || character > '9' {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
result := new(big.Rat)
|
||||
if _, ok := result.SetString(value); !ok {
|
||||
return nil, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func crackTopStatusRate(status *crackStatusView) (uint64, bool) {
|
||||
if status == nil {
|
||||
return 0, false
|
||||
}
|
||||
var total uint64
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
rate, err := strconv.ParseUint(strings.TrimSpace(device.Speed), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
total = saturatingUint64Add(total, rate)
|
||||
}
|
||||
return total, found
|
||||
}
|
||||
|
||||
func crackTopStatusMaximum(status *crackStatusView, value func(crackDeviceView) string) (float64, bool) {
|
||||
if status == nil || value == nil {
|
||||
return 0, false
|
||||
}
|
||||
maximum := 0.0
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
number, err := strconv.ParseFloat(strings.TrimSpace(value(device)), 64)
|
||||
if err != nil || number < 0 || math.IsNaN(number) || math.IsInf(number, 0) {
|
||||
continue
|
||||
}
|
||||
if !found || number > maximum {
|
||||
maximum = number
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return maximum, found
|
||||
}
|
||||
|
||||
func crackTopTaskSampleIsLive(sample crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
if station == nil || sample.task == nil || sample.statusErr != nil || sample.task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING {
|
||||
return false
|
||||
}
|
||||
leaseExpiresAt := sample.task.GetLeaseExpiresAt()
|
||||
if now.IsZero() || leaseExpiresAt <= now.Unix() {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
return false
|
||||
}
|
||||
if status.GetIsSyncing() || status.GetState() != clientpb.States_CRACKING {
|
||||
return false
|
||||
}
|
||||
currentJobID := strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
return currentJobID != "" && currentJobID == sample.jobID
|
||||
}
|
||||
|
||||
func crackTopStationExpectsTelemetry(station *clientpb.Crackstation) bool {
|
||||
if station == nil {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
// The station is connected, but without status we cannot prove it is
|
||||
// idle. Keep the cluster total unknown instead of reporting a false zero.
|
||||
return true
|
||||
}
|
||||
if status.GetIsSyncing() {
|
||||
return false
|
||||
}
|
||||
return status.GetState() == clientpb.States_CRACKING
|
||||
}
|
||||
|
||||
func crackTopSamplePreferred(left, right crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
leftLive := crackTopTaskSampleIsLive(left, station, now)
|
||||
rightLive := crackTopTaskSampleIsLive(right, station, now)
|
||||
if leftLive != rightLive {
|
||||
return leftLive
|
||||
}
|
||||
return crackTopSampleNewer(left, right)
|
||||
}
|
||||
|
||||
func crackTopSampleNewer(left, right crackTopTaskSample) bool {
|
||||
if !left.lastSeen.Equal(right.lastSeen) {
|
||||
return left.lastSeen.After(right.lastSeen)
|
||||
}
|
||||
leftRunning := left.task != nil && left.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
rightRunning := right.task != nil && right.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
if leftRunning != rightRunning {
|
||||
return leftRunning
|
||||
}
|
||||
if left.task == nil || right.task == nil {
|
||||
return left.task != nil
|
||||
}
|
||||
return left.task.GetAttempt() > right.task.GetAttempt()
|
||||
}
|
||||
|
||||
func crackTopCrackTask(task *clientpb.CrackTask, taskTelemetry map[string]crackTopTaskTelemetry) bool {
|
||||
if task == nil {
|
||||
return false
|
||||
}
|
||||
switch task.GetKind() {
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_CRACK:
|
||||
return true
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED:
|
||||
if task.GetShardLimit() != 0 || len(task.GetLatestStatusJSON()) != 0 {
|
||||
return true
|
||||
}
|
||||
metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]
|
||||
return ok && (metadata.status != nil || metadata.parseErr)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func crackTopStationID(station *clientpb.Crackstation) string {
|
||||
if station == nil {
|
||||
return ""
|
||||
}
|
||||
if hostID := strings.TrimSpace(station.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
if hostID := strings.TrimSpace(status.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(station.GetID())
|
||||
}
|
||||
|
||||
func crackTopTaskTime(task *clientpb.CrackTask) time.Time {
|
||||
if task == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
for _, value := range []int64{task.GetLastHeartbeatAt(), task.GetUpdatedAt(), task.GetStartedAt(), task.GetCreatedAt()} {
|
||||
if value > 0 {
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func crackTopParseTime(value string) time.Time {
|
||||
parsed, _ := time.Parse(time.RFC3339, strings.TrimSpace(value))
|
||||
return parsed
|
||||
}
|
||||
|
||||
func crackTopParseUnix(value int64) time.Time {
|
||||
if value <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
|
||||
func crackTopJobMode(job *clientpb.CrackJob) string {
|
||||
if job == nil || job.GetCommand() == nil {
|
||||
return ""
|
||||
}
|
||||
mode := int32(job.GetCommand().GetHashType())
|
||||
if job.GetCommand().HashMode != nil {
|
||||
mode = int32(job.GetCommand().GetHashMode())
|
||||
}
|
||||
if name, ok := hashcatHashTypeName(mode); ok {
|
||||
return name
|
||||
}
|
||||
return "mode " + strconv.FormatInt(int64(mode), 10)
|
||||
}
|
||||
|
||||
func crackTopShortID(value string) string {
|
||||
value = crackTopSafeCell(value)
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if index := strings.IndexByte(value, '-'); index > 0 {
|
||||
value = value[:index]
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) > 8 {
|
||||
value = string(runes[:8])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func crackTopSafeCell(value string) string {
|
||||
value = ansi.Strip(value)
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if !unicode.IsPrint(r) {
|
||||
return -1
|
||||
}
|
||||
if unicode.IsSpace(r) && r != ' ' {
|
||||
return ' '
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
value = strings.TrimSpace(value)
|
||||
const maximumRunes = 96
|
||||
runes := []rune(value)
|
||||
if len(runes) > maximumRunes {
|
||||
value = string(runes[:maximumRunes-1]) + "…"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func saturatingUint64Add(left, right uint64) uint64 {
|
||||
if math.MaxUint64-left < right {
|
||||
return math.MaxUint64
|
||||
}
|
||||
return left + right
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+241
-51
@@ -22,7 +22,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -31,23 +32,47 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
)
|
||||
|
||||
// CrackCmd - GPU password cracking interface
|
||||
func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunCrack(cmd, args) {
|
||||
invocation, err := classifyCrackInvocation(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationBackendInfo {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
return
|
||||
}
|
||||
printCrackersWithBackendInfoLevel(crackers, con, false, invocation.backendInfoLevel)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationJob || invocation.isSynchronousQuery() {
|
||||
crackCmd, err := buildCrackCommand(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
ctx := context.Background()
|
||||
if timeoutSeconds > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
if err := resolveManagedCrackFileReferences(ctx, crackCmd, func(ctx context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return con.Rpc.CrackFilesList(ctx, request)
|
||||
}); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := con.Rpc.Crack(ctx, crackCmd)
|
||||
@@ -55,6 +80,16 @@ func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if invocation.isSynchronousQuery() {
|
||||
value, stderr, err := crackQueryResponse(resp, invocation)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
printCrackQueryStderr(con, stderr)
|
||||
printCrackQueryValue(con, value)
|
||||
return
|
||||
}
|
||||
if resp == nil || resp.Job == nil {
|
||||
con.PrintInfof("Crack request submitted\n")
|
||||
return
|
||||
@@ -89,21 +124,139 @@ func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackKeyspace(response *clientpb.CrackResponse) (string, error) {
|
||||
return crackQueryResponseValue(response, crackInvocation{mode: crackInvocationKeyspace})
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) isSynchronousQuery() bool {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup, crackInvocationIdentify, crackInvocationHashInfo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) queryMode() clientpb.CrackQueryMode {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE
|
||||
case crackInvocationTotalCandidates:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES
|
||||
case crackInvocationLookup:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP
|
||||
case crackInvocationIdentify:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY
|
||||
case crackInvocationHashInfo:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO
|
||||
default:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func crackQueryResponseValue(response *clientpb.CrackResponse, invocation crackInvocation) (string, error) {
|
||||
value, _, err := crackQueryResponse(response, invocation)
|
||||
return value, err
|
||||
}
|
||||
|
||||
func crackQueryResponse(response *clientpb.CrackResponse, invocation crackInvocation) (string, string, error) {
|
||||
if response == nil {
|
||||
return "", "", fmt.Errorf("server returned an empty %s response", crackInvocationName(invocation.mode))
|
||||
}
|
||||
expectedMode := invocation.queryMode()
|
||||
if expectedMode == clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED {
|
||||
return "", "", fmt.Errorf("invalid synchronous crack query mode")
|
||||
}
|
||||
query := response.GetQuery()
|
||||
if query == nil {
|
||||
// Servers predating typed query results returned only Keyspace. Do not use
|
||||
// that legacy field for a selected station because it cannot prove which
|
||||
// station produced the result.
|
||||
if invocation.mode == crackInvocationKeyspace && invocation.crackstation == "" {
|
||||
value, err := canonicalCrackDecimal(response.GetKeyspace(), "keyspace")
|
||||
return value, "", err
|
||||
}
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
if query.GetMode() != expectedMode {
|
||||
return "", "", fmt.Errorf("server returned %s for %s query", query.GetMode().String(), crackInvocationName(invocation.mode))
|
||||
}
|
||||
value := query.GetValue()
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates:
|
||||
value, err := canonicalCrackDecimal(value, crackInvocationName(invocation.mode))
|
||||
return value, query.GetStderr(), err
|
||||
default:
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
return value, query.GetStderr(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackDecimal(value string, name string) (string, error) {
|
||||
raw := strings.TrimSpace(value)
|
||||
keyspace, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("server returned invalid %s %q", name, value)
|
||||
}
|
||||
return strconv.FormatUint(keyspace, 10), nil
|
||||
}
|
||||
|
||||
func printCrackQueryValue(con *console.SliverClient, value string) {
|
||||
con.Printf("%s", value)
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
con.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printCrackQueryStderr(con *console.SliverClient, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
value += "\n"
|
||||
}
|
||||
con.PrintWarnf("%s", value)
|
||||
}
|
||||
|
||||
// CrackStationsCmd - Manage GPU cracking stations
|
||||
func CrackStationsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
crackers, err := con.Rpc.Crackstations(context.Background(), &commonpb.Empty{})
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
showBenchmarks, _ := cmd.Flags().GetBool("show-benchmarks")
|
||||
if len(crackers.Crackstations) == 0 {
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
} else {
|
||||
PrintCrackers(crackers.Crackstations, con, showBenchmarks)
|
||||
PrintCrackers(crackers, con, showBenchmarks)
|
||||
}
|
||||
}
|
||||
|
||||
func connectedCrackstations(ctx context.Context, rpc rpcpb.SliverRPCClient) ([]*clientpb.Crackstation, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
response, err := rpc.Crackstations(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
crackers := make([]*clientpb.Crackstation, 0, len(response.Crackstations))
|
||||
for _, cracker := range response.Crackstations {
|
||||
if cracker != nil {
|
||||
crackers = append(crackers, cracker)
|
||||
}
|
||||
}
|
||||
return crackers, nil
|
||||
}
|
||||
|
||||
func PrintNoCrackstations(con *console.SliverClient) {
|
||||
con.PrintInfof("No crackstations connected to server\n")
|
||||
}
|
||||
@@ -117,59 +270,96 @@ func AreCrackersOnline(con *console.SliverClient) bool {
|
||||
}
|
||||
|
||||
func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool) {
|
||||
sort.Slice(crackers, func(i, j int) bool {
|
||||
return crackers[i].Name < crackers[j].Name
|
||||
printCrackersWithBackendInfoLevel(crackers, con, showBenchmarks, 1)
|
||||
}
|
||||
|
||||
func printCrackersWithBackendInfoLevel(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
ordered := make([]*clientpb.Crackstation, 0, len(crackers))
|
||||
for _, cracker := range crackers {
|
||||
if cracker != nil {
|
||||
ordered = append(ordered, cracker)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
if ordered[i].Name == ordered[j].Name {
|
||||
return ordered[i].HostUUID < ordered[j].HostUUID
|
||||
}
|
||||
return ordered[i].Name < ordered[j].Name
|
||||
})
|
||||
for index, cracker := range crackers {
|
||||
printCracker(cracker, index, con, showBenchmarks)
|
||||
if index < len(crackers)-1 {
|
||||
for index, cracker := range ordered {
|
||||
printCracker(cracker, index, con, showBenchmarks, backendInfoLevel)
|
||||
if index < len(ordered)-1 {
|
||||
con.Println()
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName)) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), cracker.HashcatVersion})
|
||||
if 0 < len(cracker.CUDA) {
|
||||
for _, cuda := range cracker.CUDA {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("CUDA Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", cuda.Name, cuda.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", cuda.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", cuda.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.Metal) {
|
||||
for _, metal := range cracker.Metal {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Metal Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", metal.Name, metal.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", metal.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", metal.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.OpenCL) {
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Device"), console.StyleBoldGreen.Render(fmt.Sprintf("%s (%s)", openCL.Name, openCL.Version))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", openCL.Clock)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", openCL.Processors)})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
con.Printf("%s\n", renderCracker(cracker, index, settings.GetTableStyle(con), backendInfoLevel))
|
||||
if showBenchmarks {
|
||||
con.Println()
|
||||
printBenchmarks(cracker, con)
|
||||
}
|
||||
}
|
||||
|
||||
func renderCracker(cracker *clientpb.Crackstation, index int, style table.Style, backendInfoLevel uint32) string {
|
||||
if cracker == nil {
|
||||
return ""
|
||||
}
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(style)
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, safeCrackCell(cracker.Name), safeCrackCell(cracker.OperatorName))) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", valueOrDash(cracker.GOOS), valueOrDash(cracker.GOARCH))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), valueOrDash(cracker.HashcatVersion)})
|
||||
if backendInfoLevel >= 2 {
|
||||
appendCrackstationDetailRows(tw, cracker)
|
||||
}
|
||||
for _, cuda := range cracker.CUDA {
|
||||
appendCUDABackendRows(tw, cuda, backendInfoLevel)
|
||||
}
|
||||
for _, hip := range cracker.HIP {
|
||||
appendHIPBackendRows(tw, hip, backendInfoLevel)
|
||||
}
|
||||
for _, metal := range cracker.Metal {
|
||||
appendMetalBackendRows(tw, metal, backendInfoLevel)
|
||||
}
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
appendOpenCLBackendRows(tw, openCL, backendInfoLevel)
|
||||
}
|
||||
if !hasReportedBackendDevice(cracker) {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Devices"), "No backend devices reported"})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func formatCUDADevice(device *clientpb.CUDABackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetCUDAVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatHIPDevice(device *clientpb.HIPBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetHIPVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatMetalDevice(device *clientpb.MetalBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetMetalVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatOpenCLDevice(device *clientpb.OpenCLBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetOpenCLVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatBackendDevice(name string, versions ...string) string {
|
||||
for _, version := range versions {
|
||||
if version != "" {
|
||||
return fmt.Sprintf("%s (%s)", name, version)
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestFormatBackendDevice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
device string
|
||||
versions []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "specific runtime version wins",
|
||||
device: "Apple M5",
|
||||
versions: []string{"Metal 410.7", "legacy-generic"},
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy generic fallback",
|
||||
device: "NVIDIA GPU",
|
||||
versions: []string{"", "13.0"},
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "no empty version suffix",
|
||||
device: "AMD GPU",
|
||||
versions: []string{"", ""},
|
||||
want: "AMD GPU",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := formatBackendDevice(test.device, test.versions...); got != test.want {
|
||||
t.Fatalf("formatBackendDevice() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendSpecificDisplayVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "CUDA version",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", CUDAVersion: "13.0", Version: "legacy",
|
||||
}),
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "HIP version",
|
||||
got: formatHIPDevice(&clientpb.HIPBackendInfo{
|
||||
Name: "AMD GPU", HIPVersion: "7.0.51831", Version: "legacy",
|
||||
}),
|
||||
want: "AMD GPU (7.0.51831)",
|
||||
},
|
||||
{
|
||||
name: "Metal version",
|
||||
got: formatMetalDevice(&clientpb.MetalBackendInfo{
|
||||
Name: "Apple M5", MetalVersion: "Metal 410.7", Version: "legacy",
|
||||
}),
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy CUDA fallback",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", Version: "12.9",
|
||||
}),
|
||||
want: "NVIDIA GPU (12.9)",
|
||||
},
|
||||
{
|
||||
name: "OpenCL version",
|
||||
got: formatOpenCLDevice(&clientpb.OpenCLBackendInfo{
|
||||
Name: "Portable GPU", OpenCLVersion: "OpenCL 3.0", Version: "legacy",
|
||||
}),
|
||||
want: "Portable GPU (OpenCL 3.0)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.got != test.want {
|
||||
t.Fatalf("backend display = %q, want %q", test.got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelTwo(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
OperatorName: "operator",
|
||||
HostUUID: "host-uuid",
|
||||
Version: "station-v1",
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
HashcatVersion: "v7.1.2",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "CUDA GPU", CUDAVersion: "CUDA 13", Version: "device-v1", Type: "GPU",
|
||||
Vendor: "NVIDIA", VendorID: 4318, BackendDeviceID: 1, BackendDeviceAlias: &zero,
|
||||
PreferredThreadSize: &zero, MemoryUnified: &falseValue, LocalMemory: "64 KB",
|
||||
CacheSize: "8 MB", PCIAddress: "01:00.0", MemoryFree: "8 GB", MemoryTotal: "12 GB",
|
||||
}},
|
||||
HIP: []*clientpb.HIPBackendInfo{{Name: "HIP GPU", HIPVersion: "HIP 7", BackendDeviceID: 2}},
|
||||
Metal: []*clientpb.MetalBackendInfo{{
|
||||
Name: "Metal GPU", MetalVersion: "Metal 410", BackendDeviceID: 3,
|
||||
RegistryID: &zero, GPUHeadless: &falseValue, GPULowPower: &falseValue, GPURemovable: &falseValue,
|
||||
PhysicalLocation: "built-in", MaxTXRate: "100", MemoryAllocPerBlock: "4 GB",
|
||||
}},
|
||||
OpenCL: []*clientpb.OpenCLBackendInfo{{
|
||||
Name: "OpenCL GPU", OpenCLVersion: "OpenCL 3.0", OpenCLDriverVersion: "driver-v1",
|
||||
BackendDeviceID: 4, PlatformVendor: "Portable", PlatformName: "Platform",
|
||||
PlatformVersion: "platform-v1", MemoryAllocPerBlock: "2 GB",
|
||||
}},
|
||||
}
|
||||
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 2)
|
||||
for _, expected := range []string{
|
||||
"station", "operator", "host-uuid", "station-v1", "linux/amd64", "v7.1.2",
|
||||
"CUDA GPU", "HIP GPU", "Metal GPU", "OpenCL GPU", "OpenCL 3.0", "driver-v1",
|
||||
"Backend Device ID", "Backend Device Alias", "Preferred Thread Size", "Unified Memory",
|
||||
"NVIDIA", "4318", "device-v1", "64 KB", "8 MB", "01:00.0",
|
||||
"Registry ID", "Headless", "Low Power", "Removable", "built-in", "100",
|
||||
"OpenCL Platform ID", "Portable", "Platform", "platform-v1", "2 GB",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("level-2 backend info does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelOneOmitsDetails(t *testing.T) {
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "GPU", BackendDeviceID: 7, PCIAddress: "01:00.0",
|
||||
}},
|
||||
}
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 1)
|
||||
for _, unexpected := range []string{"Host UUID", "Backend Device ID", "PCI Address"} {
|
||||
if strings.Contains(output, unexpected) {
|
||||
t.Errorf("level-1 backend info unexpectedly contains %q:\n%s", unexpected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerPreservesOptionalPresence(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
withPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
BackendDeviceAlias: &zero,
|
||||
MemoryUnified: &falseValue,
|
||||
}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if !strings.Contains(withPresence, "Backend Device Alias") || !strings.Contains(withPresence, "Unified Memory") {
|
||||
t.Fatalf("present zero/false optional fields were omitted:\n%s", withPresence)
|
||||
}
|
||||
|
||||
withoutPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if strings.Contains(withoutPresence, "Backend Device Alias") || strings.Contains(withoutPresence, "Unified Memory") {
|
||||
t.Fatalf("absent optional fields were rendered:\n%s", withoutPresence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerReportsNoBackendDevices(t *testing.T) {
|
||||
output := renderCracker(&clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{nil},
|
||||
}, 0, settings.SliverDefault, 1)
|
||||
if !strings.Contains(output, "No backend devices reported") {
|
||||
t.Fatalf("missing no-device message:\n%s", output)
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,95 @@ package crack
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type crackJobCompletionEntry struct {
|
||||
id string
|
||||
description string
|
||||
}
|
||||
|
||||
// CrackJobIDCompleter completes durable crack job IDs.
|
||||
//
|
||||
//nolint:revive // Keep the established exported completer name for compatibility.
|
||||
func CrackJobIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
entries, err := crackJobCompletionEntries(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack jobs: %s", err.Error())
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(entries)*2)
|
||||
for _, entry := range entries {
|
||||
values = append(values, entry.id, entry.description)
|
||||
}
|
||||
return carapace.ActionValuesDescribed(values...).Tag("crack jobs")
|
||||
})
|
||||
}
|
||||
|
||||
func registerCrackJobIDCompletion(cmd *cobra.Command, con *console.SliverClient) {
|
||||
if cmd == nil || cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
entries, err := crackJobCompletionEntries(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
values := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.id, toComplete) {
|
||||
values = append(values, fmt.Sprintf("%s\t%s", entry.id, entry.description))
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func crackJobCompletionEntries(con *console.SliverClient) ([]crackJobCompletionEntry, error) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
jobs, err := con.Rpc.CrackJobs(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if jobs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entries := make([]crackJobCompletionEntry, 0, len(jobs.GetJobs()))
|
||||
for _, job := range jobs.GetJobs() {
|
||||
if job == nil || strings.TrimSpace(job.GetID()) == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, crackJobCompletionEntry{
|
||||
id: job.GetID(),
|
||||
description: fmt.Sprintf(
|
||||
"%s, created %s, keyspace %s, %d task(s), %d result(s)",
|
||||
safeCrackCell(job.GetStatus().String()),
|
||||
formatCrackTime(job.GetCreatedAt()),
|
||||
valueOrDash(job.GetKeyspace()),
|
||||
len(job.GetTasks()),
|
||||
job.GetResultCount(),
|
||||
),
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
@@ -35,14 +117,14 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range wordlists.Files {
|
||||
if file.Type != clientpb.CrackFileType_WORDLIST {
|
||||
continue
|
||||
}
|
||||
@@ -59,14 +141,14 @@ func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
rules, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_RULES})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range rules.Files {
|
||||
if file.Type != clientpb.CrackFileType_RULES {
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +159,7 @@ func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
results = append(results, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("wordlists")
|
||||
return carapace.ActionValuesDescribed(results...).Tag("rules")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
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/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type crackFilesListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
requests []clientpb.CrackFileType
|
||||
}
|
||||
|
||||
type crackJobsListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
jobs *clientpb.CrackJobs
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (capture *crackJobsListCapture) CrackJobs(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackJobs, error) {
|
||||
capture.calls++
|
||||
return capture.jobs, capture.err
|
||||
}
|
||||
|
||||
func (capture *crackFilesListCapture) CrackFilesList(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
|
||||
capture.requests = append(capture.requests, request.GetType())
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
|
||||
func TestCrackFileCompletersRequestTheirOwnFileType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want clientpb.CrackFileType
|
||||
action func(*console.SliverClient) carapace.Action
|
||||
}{
|
||||
{name: "wordlists", want: clientpb.CrackFileType_WORDLIST, action: CrackWordlistCompleter},
|
||||
{name: "rules", want: clientpb.CrackFileType_RULES, action: CrackRulesCompleter},
|
||||
{name: "hcstat2", want: clientpb.CrackFileType_MARKOV_HCSTAT2, action: CrackHcstat2Completer},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackFilesListCapture{}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = test.action(client).Invoke(carapace.NewContext(""))
|
||||
if len(capture.requests) != 1 || capture.requests[0] != test.want {
|
||||
t.Fatalf("completion requests = %v, want [%s]", capture.requests, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompleterRequestsDurableJobs(t *testing.T) {
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{
|
||||
ID: "12345678-1234-1234-1234-123456789abc",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
}}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = CrackJobIDCompleter(client).Invoke(carapace.NewContext(""))
|
||||
if capture.calls != 1 {
|
||||
t.Fatalf("CrackJobs calls = %d, want 1", capture.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCobraCompletionUsesFullIDsAndFiltersPrefix(t *testing.T) {
|
||||
firstID := "12345678-1234-1234-1234-123456789abc"
|
||||
secondID := "abcdefab-cdef-cdef-cdef-abcdefabcdef"
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{
|
||||
{
|
||||
ID: firstID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
},
|
||||
{ID: secondID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
nil,
|
||||
{},
|
||||
}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
|
||||
values, directive := job.ValidArgsFunction(job, nil, "1234")
|
||||
if directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion directive = %v, want no-file-completion", directive)
|
||||
}
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("completion values = %#v, want one filtered result", values)
|
||||
}
|
||||
if !strings.HasPrefix(values[0], firstID+"\t") {
|
||||
t.Fatalf("completion = %q, want full ID %q", values[0], firstID)
|
||||
}
|
||||
for _, want := range []string{"IN_PROGRESS", "2026-09-11T12:00:00Z", "keyspace 1000", "1 task(s)", "2 result(s)"} {
|
||||
if !strings.Contains(values[0], want) {
|
||||
t.Errorf("completion %q does not contain %q", values[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
values, directive = job.ValidArgsFunction(job, []string{firstID}, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("second positional completion = %#v, %v", values, directive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompletionHandlesRPCFailuresAndNilData(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
capture *crackJobsListCapture
|
||||
}{
|
||||
{name: "rpc error", capture: &crackJobsListCapture{err: errors.New("rpc failed")}},
|
||||
{name: "nil response", capture: &crackJobsListCapture{}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &console.SliverClient{Rpc: test.capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
values, directive := job.ValidArgsFunction(job, nil, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion = %#v, %v", values, directive)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+598
-139
@@ -1,143 +1,602 @@
|
||||
package creds
|
||||
|
||||
// List of all available hash-types, for completions.
|
||||
// Unfortunately this list has to be maintained manually,
|
||||
// until someone devises a way to do so automatically.
|
||||
// hashTypes maps protobuf enum identifiers to the exact human-readable
|
||||
// names reported by the bundled Hashcat v7.1.2-armory.1 modules at commit
|
||||
// deadd52de22e2753e9caaff6d1b92e5a22a35626.
|
||||
var hashTypes = map[string]string{
|
||||
"MD5": "MD5", // Enum 0 in proto file.
|
||||
|
||||
"MD4": "MD4",
|
||||
"SHA1": "SHA1",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"SIPHASH": "SipHash",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
|
||||
// ... TODO fill in the rest ...,
|
||||
|
||||
"CRC32": "CRC32",
|
||||
"CRC32C": "CRC32B",
|
||||
"CRC64Jones": "CRC64-Jones",
|
||||
"JAVA_OBJECT": "Java object",
|
||||
"MURMUR": "MurmurHash",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"SCRYPT": "scrypt",
|
||||
"PHPASS": "phpass",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96 / HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"RACF": "Resource Access Control Facility",
|
||||
"AIX_SMD5": "AIX MD5",
|
||||
"AIX_SSHA1": "AIX SHA-1",
|
||||
"AIX_SSHA256": "AIX SHA-256",
|
||||
"AIX_SSHA512": "AIX SHA-512",
|
||||
"LM": "LM",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"NTLM": "NTLM",
|
||||
"RADMIN2": "Radmin2",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/Password",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"CISCO_PIX_MD5": "Cisco-Pix MD5",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
|
||||
// ...
|
||||
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
|
||||
// ...
|
||||
|
||||
// INVALID = 9999; // Invalid hash type
|
||||
|
||||
// Out of order
|
||||
"BCRYPT_UNIX": "bcrypt",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"MD5": "MD5",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"JOOMLA_2_5_18": "Joomla < 2.5.18",
|
||||
"POSTGRESQL": "PostgreSQL",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"OSCOMMERCE_XT_COMMERCE": "osCommerce, xt:Commerce",
|
||||
"JUNIPER_NETSCREEN_SSG_SCREENOS": "Juniper NetScreen/SSG (ScreenOS)",
|
||||
"SKYPE": "Skype",
|
||||
"SOLARWINDS_SERV_U": "SolarWinds Serv-U",
|
||||
"MD5_UTF16LE_PASS_SALT": "md5(utf16le($pass).$salt)",
|
||||
"MD5_SALT_UTF16LE_PASS": "md5($salt.utf16le($pass))",
|
||||
"HMAC_MD5_KEY_PASS": "HMAC-MD5 (key = $pass)",
|
||||
"HMAC_MD5_KEY_SALT": "HMAC-MD5 (key = $salt)",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1": "SHA1",
|
||||
"NSLDAP_SHA_1_BASE64_NETSCAPE_LDAP_SHA": "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
"SHA1_PASS_SALT": "sha1($pass.$salt)",
|
||||
"NSLDAPS_SSHA_1_BASE64_NETSCAPE_LDAP_SSHA": "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
"ORACLE_S_TYPE_ORACLE_11": "Oracle S: Type (Oracle 11+)",
|
||||
"SHA1_SALT_PASS": "sha1($salt.$pass)",
|
||||
"SMF_SIMPLE_MACHINES_FORUM_V1_1": "SMF (Simple Machines Forum) > v1.1",
|
||||
"MACOS_V10_4_MACOS_V10_5_MACOS_V10_6": "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
"DJANGO_SHA_1": "Django (SHA-1)",
|
||||
"ARUBAOS": "ArubaOS",
|
||||
"SHA1_UTF16LE_PASS_SALT": "sha1(utf16le($pass).$salt)",
|
||||
"MSSQL_2000": "MSSQL (2000)",
|
||||
"MSSQL_2005": "MSSQL (2005)",
|
||||
"PEOPLESOFT": "PeopleSoft",
|
||||
"SHA1_SALT_UTF16LE_PASS": "sha1($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_141": "Episerver 6.x < .NET 4",
|
||||
"HMAC_SHA1_KEY_PASS": "HMAC-SHA1 (key = $pass)",
|
||||
"HMAC_SHA1_KEY_SALT": "HMAC-SHA1 (key = $salt)",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"MYSQL323": "MySQL323",
|
||||
"MYSQL4_1_MYSQL5": "MySQL4.1/MySQL5",
|
||||
"PHPASS": "phpass",
|
||||
"MD5CRYPT_MD5_UNIX_CISCO_IOS_1_MD5": "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
"JUNIPER_IVE": "Juniper IVE",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD4": "MD4",
|
||||
"NTLM": "NTLM",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA224_PASS_SALT": "sha224($pass.$salt)",
|
||||
"SHA224_SALT_PASS": "sha224($salt.$pass)",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA256_PASS_SALT": "sha256($pass.$salt)",
|
||||
"SSHA_256_BASE64_LDAP_SSHA256": "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
"SHA256_SALT_PASS": "sha256($salt.$pass)",
|
||||
"HMAILSERVER": "hMailServer",
|
||||
"SHA256_UTF16LE_PASS_SALT": "sha256(utf16le($pass).$salt)",
|
||||
"SHA256_SALT_UTF16LE_PASS": "sha256($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_1441": "Episerver 6.x >= .NET 4",
|
||||
"HMAC_SHA256_KEY_PASS": "HMAC-SHA256 (key = $pass)",
|
||||
"HMAC_SHA256_KEY_SALT": "HMAC-SHA256 (key = $salt)",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"DESCRYPT_DES_UNIX_TRADITIONAL_DES": "descrypt, DES (Unix), Traditional DES",
|
||||
"APACHE_APR1_MD5_MD5APR1_MD5_APR": "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA512_PASS_SALT": "sha512($pass.$salt)",
|
||||
"SSHA_512_BASE64_LDAP_SSHA512": "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
"SHA512_SALT_PASS": "sha512($salt.$pass)",
|
||||
"MACOS_V10_7": "macOS v10.7",
|
||||
"SHA512_UTF16LE_PASS_SALT": "sha512(utf16le($pass).$salt)",
|
||||
"MSSQL_2012_2014": "MSSQL (2012, 2014)",
|
||||
"SHA512_SALT_UTF16LE_PASS": "sha512($salt.utf16le($pass))",
|
||||
"HMAC_SHA512_KEY_PASS": "HMAC-SHA512 (key = $pass)",
|
||||
"HMAC_SHA512_KEY_SALT": "HMAC-SHA512 (key = $salt)",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"STDOUT": "STDOUT",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
"CISCO_PIX_MD5": "Cisco-PIX MD5",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"MD5_MD5_PASS": "md5(md5($pass))",
|
||||
"VBULLETIN_V3_8_5_MODE_2611": "vBulletin < v3.8.5",
|
||||
"PHPS": "PHPS",
|
||||
"MD5_MD5_PASS_SALT": "md5(md5($pass.$salt))",
|
||||
"VBULLETIN_V3_8_5_MODE_2711": "vBulletin >= v3.8.5",
|
||||
"MYBB_1_2_IPB2_INVISION_POWER_BOARD": "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
"LM": "LM",
|
||||
"ORACLE_H_TYPE_ORACLE_7": "Oracle H: Type (Oracle 7+)",
|
||||
"BCRYPT_UNIX": "bcrypt $2*$, Blowfish (Unix)",
|
||||
"MD5_MD5_MD5_PASS": "md5(md5(md5($pass)))",
|
||||
"MD5_MD5_MD5_PASS_SALT": "md5(md5(md5($pass)).$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
"MEDIAWIKI_B_TYPE": "MediaWiki B type",
|
||||
"MD5_SALT1_STRTOUPPER_MD5_SALT2_PASS": "md5($salt1.strtoupper(md5($salt2.$pass)))",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_MD5_PASS_MD5_SALT": "md5(md5($pass).md5($salt))",
|
||||
"MD5_SALT_MD5_SALT_PASS": "md5($salt.md5($salt.$pass))",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_4110": "md5($salt.md5($pass.$salt))",
|
||||
"MD5_STRTOUPPER_MD5_PASS": "md5(strtoupper(md5($pass)))",
|
||||
"MD5_SHA1_PASS": "md5(sha1($pass))",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4410": "md5(sha1($pass).$salt)",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4420": "md5(sha1($pass.$salt))",
|
||||
"MD5_SHA1_SALT_PASS": "md5(sha1($salt.$pass))",
|
||||
"SHA1_SHA1_PASS": "sha1(sha1($pass))",
|
||||
"SHA1_SHA1_PASS_SALT": "sha1(sha1($pass).$salt)",
|
||||
"SHA1_SALT_SHA1_PASS": "sha1($salt.sha1($pass))",
|
||||
"REDMINE": "Redmine",
|
||||
"PUNBB": "PunBB",
|
||||
"SHA1_MD5_PASS": "sha1(md5($pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_4710": "sha1(md5($pass).$salt)",
|
||||
"HUAWEI_SHA1_MD5_PASS_SALT": "Huawei sha1(md5($pass).$salt)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"SHA1_SALT_PASS_SALT": "sha1($salt.$pass.$salt)",
|
||||
"SHA1_SHA1_SALT_PASS_SALT": "sha1(sha1($salt.$pass.$salt))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"PASSWORD_SAFE_V3": "Password Safe v3",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"CISCO_IOS_TYPE_4_SHA256": "Cisco-IOS type 4 (SHA256)",
|
||||
"CISCO_ISE_HASHED_PASSWORD_SHA256": "Cisco-ISE Hashed Password (SHA256)",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"HMAC_RIPEMD160_KEY_PASS": "HMAC-RIPEMD160 (key = $pass)",
|
||||
"HMAC_RIPEMD160_KEY_SALT": "HMAC-RIPEMD160 (key = $salt)",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT_LEGACY": "TrueCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"AIX_SMD5": "AIX {smd5}",
|
||||
"AIX_SSHA256": "AIX {ssha256}",
|
||||
"AIX_SSHA512": "AIX {ssha512}",
|
||||
"HASHCAT_1PASSWORD_AGILEKEYCHAIN": "1Password, agilekeychain",
|
||||
"AIX_SSHA1": "AIX {ssha1}",
|
||||
"LASTPASS_LASTPASS_SNIFFED": "LastPass + LastPass sniffed",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"FORTIGATE_FORTIOS": "FortiGate (FortiOS)",
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"IPMI2_RAKP_HMAC_MD5": "IPMI2 RAKP HMAC-MD5",
|
||||
"SHA256CRYPT_5_SHA256_UNIX": "sha256crypt $5$, SHA256 (Unix)",
|
||||
"MYSQL_A_SHA256CRYPT": "MySQL $A$ (sha256crypt)",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"SAP_CODVN_B_BCODE": "SAP CODVN B (BCODE)",
|
||||
"SAP_CODVN_B_BCODE_FROM_RFC_READ_TABLE": "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
"SAP_CODVN_F_G_PASSCODE": "SAP CODVN F/G (PASSCODE)",
|
||||
"SAP_CODVN_F_G_PASSCODE_FROM_RFC_READ_TABLE": "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
"DRUPAL7": "Drupal7",
|
||||
"SYBASE_ASE": "Sybase ASE",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"HASHCAT_1PASSWORD_CLOUDKEYCHAIN": "1Password, cloudkeychain",
|
||||
"DNSSEC_NSEC3": "DNSSEC (NSEC3)",
|
||||
"WBB3_WOLTLAB_BURNING_BOARD": "WBB3 (Woltlab Burning Board)",
|
||||
"RACF": "RACF",
|
||||
"AS_400_DES": "AS/400 DES",
|
||||
"LOTUS_NOTES_DOMINO_5": "Lotus Notes/Domino 5",
|
||||
"LOTUS_NOTES_DOMINO_6": "Lotus Notes/Domino 6",
|
||||
"ANDROID_FDE_4_3": "Android FDE <= 4.3",
|
||||
"SCRYPT": "scrypt",
|
||||
"PASSWORD_SAFE_V2": "Password Safe v2",
|
||||
"LOTUS_NOTES_DOMINO_8": "Lotus Notes/Domino 8",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"MS_OFFICE_2007": "MS Office 2007",
|
||||
"MS_OFFICE_2010": "MS Office 2010",
|
||||
"MS_OFFICE_2013": "MS Office 2013",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4": "MS Office <= 2003 $0/$1, MD5 + RC4",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_1": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_2": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
"MS_OFFICE_2003_3_4_SHA1_RC4": "MS Office <= 2003 $3/$4, SHA1 + RC4",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_1": "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_2": "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
"RADMIN2": "Radmin2",
|
||||
"DJANGO_PBKDF2_SHA256": "Django (PBKDF2-SHA256)",
|
||||
"SIPHASH": "SipHash",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA_1": "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4": "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_1": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_2": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8": "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
"PDF_1_3_1_6_ACROBAT_4_8_W_RC4_40": "PDF 1.3 - 1.6 (Acrobat 4 - 8) w/ RC4-40",
|
||||
"PDF_1_7_LEVEL_3_ACROBAT_9": "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
"PDF_1_7_LEVEL_8_ACROBAT_10_11": "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA384_PASS_SALT": "sha384($pass.$salt)",
|
||||
"SHA384_SALT_PASS": "sha384($salt.$pass)",
|
||||
"SHA384_UTF16LE_PASS_SALT": "sha384(utf16le($pass).$salt)",
|
||||
"SHA384_SALT_UTF16LE_PASS": "sha384($salt.utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"REDHAT_389_DS_LDAP_PBKDF2_HMAC_SHA256": "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
"PRESTASHOP": "PrestaShop",
|
||||
"POSTGRESQL_CRAM_MD5": "PostgreSQL CRAM (MD5)",
|
||||
"MYSQL_CRAM_SHA1": "MySQL CRAM (SHA1)",
|
||||
"BITCOIN_LITECOIN_WALLET_DAT": "Bitcoin/Litecoin wallet.dat",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"CRC32": "CRC32",
|
||||
"HASHCAT_7_ZIP": "7-Zip",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"ATLASSIAN_PBKDF2_HMAC_SHA1": "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"APACHE_SHIRO_1_SHA_512": "Apache Shiro 1 SHA-512",
|
||||
"ECRYPTFS": "eCryptfs",
|
||||
"ORACLE_T_TYPE_ORACLE_12": "Oracle T: Type (Oracle 12+)",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"RAR3_HP": "RAR3-hp",
|
||||
"COLDFUSION_10": "ColdFusion 10+",
|
||||
"BLOCKCHAIN_MY_WALLET": "Blockchain, My Wallet",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"ANDROID_FDE_SAMSUNG_DEK": "Android FDE (Samsung DEK)",
|
||||
"RAR5": "RAR5",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"AXCRYPT_1": "AxCrypt 1",
|
||||
"AXCRYPT_1_IN_MEMORY_SHA1": "AxCrypt 1 in-memory SHA1",
|
||||
"KEEPASS_KDBX_V2_V3": "KeePass (KDBX v2/v3)",
|
||||
"PEOPLESOFT_PS_TOKEN": "PeopleSoft PS_TOKEN",
|
||||
"WINZIP": "WinZip",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT_LEGACY": "VeraCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_LEGACY": "VeraCrypt SHA256 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/password",
|
||||
"OPENCART": "OpenCart",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"RACF_KDFAES": "RACF KDFAES",
|
||||
"SHA1_CX": "sha1(CX)",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"LUKS_V1_LEGACY": "LUKS v1 (legacy)",
|
||||
"ITUNES_BACKUP_10_0_MODE_14700": "iTunes backup < 10.0",
|
||||
"ITUNES_BACKUP_10_0_MODE_14800": "iTunes backup >= 10.0",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"FILEZILLA_SERVER_0_9_55": "FileZilla Server >= 0.9.55",
|
||||
"JUNIPER_NETBSD_SHA1CRYPT": "Juniper/NetBSD sha1crypt",
|
||||
"BLOCKCHAIN_MY_WALLET_V2": "Blockchain, My Wallet, V2",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"JKS_JAVA_KEY_STORE_PRIVATE_KEYS_SHA1": "JKS Java Key Store Private Keys (SHA1)",
|
||||
"ETHEREUM_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
"ETHEREUM_WALLET_SCRYPT": "Ethereum Wallet, SCRYPT",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1 and 2)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"TRIPCODE": "Tripcode",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"APPLE_SECURE_NOTES": "Apple Secure Notes",
|
||||
"ETHEREUM_PRE_SALE_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
"CRAM_MD5_DOVECOT": "CRAM-MD5 Dovecot",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"PERL_MOJOLICIOUS_SESSION_COOKIE_HMAC_SHA256_V9_19": "Perl Mojolicious session cookie (HMAC-SHA256, >= v9.19)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_1_3": "Electrum Wallet (Salt-Type 1-3)",
|
||||
"FILEVAULT_2": "FileVault 2",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"ANSIBLE_VAULT": "Ansible Vault",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_512_PASS": "GPG (AES-128/AES-256 (SHA-512($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_256_PASS": "GPG (AES-128/AES-256 (SHA-256($pass)))",
|
||||
"GPG_CAST5_SHA_1_PASS": "GPG (CAST5 (SHA-1($pass)))",
|
||||
"GPG_AES_OCB_128_SHA_1_PASS": "GPG (AES-OCB-128 (SHA-1($pass)))",
|
||||
"PKZIP_COMPRESSED": "PKZIP (Compressed)",
|
||||
"PKZIP_UNCOMPRESSED": "PKZIP (Uncompressed)",
|
||||
"PKZIP_COMPRESSED_MULTI_FILE": "PKZIP (Compressed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE": "PKZIP (Mixed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE_CHECKSUM_ONLY": "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"TOTP_HMAC_SHA1": "TOTP (HMAC-SHA1)",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"APPLE_FILE_SYSTEM_APFS": "Apple File System (APFS)",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_2_SHA_256_AES": "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
"SHA1_MD5_MD5_PASS": "sha1(md5(md5($pass)))",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_1_SHA_1_BLOWFISH": "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
"JAVA_OBJECT": "Java Object hashCode()",
|
||||
"BLOCKCHAIN_MY_WALLET_SECOND_PASSWORD_SHA256": "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
"ANDROID_BACKUP": "Android Backup",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"QNX_7_ETC_SHADOW_SHA512": "QNX 7 /etc/shadow (SHA512)",
|
||||
"SHA1_SALT1_PASS_SALT2": "sha1($salt1.$pass.$salt2)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTHENTICATION": "Ruby on Rails Restful-Authentication",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"DISKCRYPTOR_SHA512_XTS_512_BIT": "DiskCryptor SHA512 + XTS 512 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1024_BIT": "DiskCryptor SHA512 + XTS 1024 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1536_BIT": "DiskCryptor SHA512 + XTS 1536 bit",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA512": "Python passlib pbkdf2-sha512",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA256": "Python passlib pbkdf2-sha256",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA1": "Python passlib pbkdf2-sha1",
|
||||
"PKZIP_MASTER_KEY": "PKZIP Master Key",
|
||||
"PKZIP_MASTER_KEY_6_BYTE_OPTIMIZATION": "PKZIP Master Key (6 byte optimization)",
|
||||
"ORACLE_TRANSPORTATION_MANAGEMENT_SHA256": "Oracle Transportation Management (SHA256)",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20710": "sha256(sha256($pass).$salt)",
|
||||
"AUTHME_SHA256": "AuthMe sha256",
|
||||
"RSA_SECURITY_ANALYTICS_NETWITNESS_SHA256": "RSA Security Analytics / NetWitness (sha256)",
|
||||
"SHA256_SALT_SHA256_PASS": "sha256($salt.sha256($pass))",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20730": "sha256(sha256($pass.$salt))",
|
||||
"SHA256_MD5_PASS": "sha256(md5($pass))",
|
||||
"MD5_SHA1_PASS_MD5_PASS_SHA1_PASS": "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
"BITSHARES_V0_X_SHA512_SHA512_BIN_PASS": "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_21100": "sha1(md5($pass.$salt))",
|
||||
"MD5_SHA1_SALT_MD5_PASS": "md5(sha1($salt).md5($pass))",
|
||||
"MD5_SALT_SHA1_SALT_PASS": "md5($salt.sha1($salt.$pass))",
|
||||
"MD5_SALT1_SHA1_SALT2_PASS": "md5($salt1.sha1($salt2.$pass))",
|
||||
"SHA256_SHA256_BIN_PASS": "sha256(sha256_bin($pass))",
|
||||
"SHA256_SALT_SHA256_BIN_PASS": "sha256($salt.sha256_bin($pass))",
|
||||
"SOLARWINDS_ORION": "SolarWinds Orion",
|
||||
"SOLARWINDS_ORION_V2": "SolarWinds Orion v2",
|
||||
"WEB2PY_PBKDF2_SHA512": "Web2py pbkdf2-sha512",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_4": "Electrum Wallet (Salt-Type 4)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_5": "Electrum Wallet (Salt-Type 5)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900": "md5(md5(md5($pass.$salt1)).$salt2)",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"BITLOCKER": "BitLocker",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"SHA256_SALT_PASS_SALT": "sha256($salt.$pass.$salt)",
|
||||
"TELEGRAM_MOBILE_APP_PASSCODE_SHA256": "Telegram Mobile App Passcode (SHA256)",
|
||||
"AES_CRYPT_SHA256": "AES Crypt (SHA256)",
|
||||
"MULTIBIT_CLASSIC_KEY_MD5": "MultiBit Classic .key (MD5)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA1": "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
"MULTIBIT_HD_SCRYPT": "MultiBit HD (scrypt)",
|
||||
"SIMPLA_CMS_MD5_SALT_PASS_MD5_PASS": "Simpla CMS - md5($salt.$pass.md5($pass))",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_0": "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_6": "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_1_3": "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_4": "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_5": "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
"SECUREZIP_AES_128": "SecureZIP AES-128",
|
||||
"SECUREZIP_AES_192": "SecureZIP AES-192",
|
||||
"SECUREZIP_AES_256": "SecureZIP AES-256",
|
||||
"APPLE_KEYCHAIN": "Apple Keychain",
|
||||
"XMPP_SCRAM_PBKDF2_SHA1": "XMPP SCRAM PBKDF2-SHA1",
|
||||
"APPLE_IWORK": "Apple iWork",
|
||||
"BITWARDEN": "Bitwarden",
|
||||
"AXCRYPT_2_AES_128": "AxCrypt 2 AES-128",
|
||||
"AXCRYPT_2_AES_256": "AxCrypt 2 AES-256",
|
||||
"RAR3_P_UNCOMPRESSED": "RAR3-p (Uncompressed)",
|
||||
"RAR3_P_COMPRESSED": "RAR3-p (Compressed)",
|
||||
"BESTCRYPT_V3_VOLUME_ENCRYPTION": "BestCrypt v3 Volume Encryption",
|
||||
"BESTCRYPT_V4_VOLUME_ENCRYPTION": "BestCrypt v4 Volume Encryption",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_1": "MongoDB ServerKey SCRAM-SHA-1",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_256": "MongoDB ServerKey SCRAM-SHA-256",
|
||||
"SHA1_SALT_SHA1_PASS_SALT": "sha1($salt.sha1($pass.$salt))",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA1_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA256_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA512": "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
"SQLCIPHER": "SQLCipher",
|
||||
"STUFFIT5": "Stuffit5",
|
||||
"UMBRACO_HMAC_SHA1": "Umbraco HMAC-SHA1",
|
||||
"DAHUA_AUTHENTICATION_MD5": "Dahua Authentication MD5",
|
||||
"BESDER_AUTHENTICATION_MD5": "Besder Authentication MD5",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96/HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"MS_OFFICE_2016_SHEETPROTECTION": "MS Office 2016 - SheetProtection",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8_USER_AND_OWNER_PASS": "PDF 1.4 - 1.6 (Acrobat 5 - 8) - user and owner pass",
|
||||
"STARGAZER_STELLAR_WALLET_XLM": "Stargazer Stellar Wallet XLM",
|
||||
"BCRYPT_MD5_PASS": "bcrypt(md5($pass))",
|
||||
"MURMUR": "MurmurHash",
|
||||
"BCRYPT_SHA1_PASS": "bcrypt(sha1($pass))",
|
||||
"KNX_IP_SECURE_DEVICE_AUTHENTICATION_CODE": "KNX IP Secure - Device Authentication Code",
|
||||
"MOZILLA_KEY3_DB": "Mozilla key3.db",
|
||||
"MOZILLA_KEY4_DB": "Mozilla key4.db",
|
||||
"OPENEDGE_PROGRESS_ENCODE": "OpenEdge Progress Encode",
|
||||
"FORTIGATE256_FORTIOS256": "FortiGate256 (FortiOS256)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"IPHONE_PASSCODE_UID_KEY_SYSTEM_KEYBAG": "iPhone passcode (UID key + System Keybag)",
|
||||
"METAMASK_WALLET_NEEDS_ALL_DATA_CHECKS_AES_GCM_TAG": "MetaMask Wallet (needs all data, checks AES-GCM tag)",
|
||||
"METAMASK_WALLET_SHORT_HASH_PLAINTEXT_CHECK": "MetaMask Wallet (short hash, plaintext check)",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTH_ONE_ROUND_NO_SITEKEY": "Ruby on Rails Restful Auth (one round, no sitekey)",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"VMWARE_VMX_PBKDF2_HMAC_SHA1_AES_256_CBC": "VMware VMX (PBKDF2-HMAC-SHA1 + AES-256-CBC)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_128_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-128-XTS)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_256_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-256-XTS)",
|
||||
"MULTIBIT_CLASSIC_WALLET_SCRYPT": "MultiBit Classic .wallet (scrypt)",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"CRC32C": "CRC32C",
|
||||
"CRC64Jones": "CRC64Jones",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"EXODUS_DESKTOP_WALLET_SCRYPT": "Exodus Desktop Wallet (scrypt)",
|
||||
"TEAMSPEAK_3_CHANNEL_HASH": "Teamspeak 3 (channel hash)",
|
||||
"BCRYPT_SHA512_PASS": "bcrypt(sha512($pass))",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin WIF private key (P2PKH), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin WIF private key (P2PKH), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), uncompressed",
|
||||
"POSTGRESQL_SCRAM_SHA_256": "PostgreSQL SCRAM-SHA-256",
|
||||
"AMAZON_AWS_SIGNATURE_VERSION_4": "Amazon AWS Signature Version 4",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"SHA1_SALT_SHA1_UTF16LE_USERNAME_UTF16LE_PASS": "sha1($salt.sha1(utf16le($username).':'.utf16le($pass)))",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT": "TrueCrypt RIPEMD160 + XTS 512 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT": "TrueCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT": "TrueCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT": "TrueCrypt SHA512 + XTS 512 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT": "TrueCrypt SHA512 + XTS 1024 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT": "TrueCrypt SHA512 + XTS 1536 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT": "TrueCrypt Whirlpool + XTS 512 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT": "TrueCrypt Whirlpool + XTS 1024 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT": "TrueCrypt Whirlpool + XTS 1536 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT": "VeraCrypt RIPEMD160 + XTS 512 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT": "VeraCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT": "VeraCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT": "VeraCrypt SHA512 + XTS 512 bit",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT": "VeraCrypt SHA512 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT": "VeraCrypt SHA512 + XTS 1536 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT": "VeraCrypt Whirlpool + XTS 512 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT": "VeraCrypt Whirlpool + XTS 1024 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT": "VeraCrypt Whirlpool + XTS 1536 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT": "VeraCrypt SHA256 + XTS 512 bit",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT": "VeraCrypt SHA256 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT": "VeraCrypt SHA256 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT": "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT": "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT": "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode",
|
||||
"LUKS_V1_SHA_1_AES": "LUKS v1 SHA-1 + AES",
|
||||
"LUKS_V1_SHA_1_SERPENT": "LUKS v1 SHA-1 + Serpent",
|
||||
"LUKS_V1_SHA_1_TWOFISH": "LUKS v1 SHA-1 + Twofish",
|
||||
"LUKS_V1_SHA_256_AES": "LUKS v1 SHA-256 + AES",
|
||||
"LUKS_V1_SHA_256_SERPENT": "LUKS v1 SHA-256 + Serpent",
|
||||
"LUKS_V1_SHA_256_TWOFISH": "LUKS v1 SHA-256 + Twofish",
|
||||
"LUKS_V1_SHA_512_AES": "LUKS v1 SHA-512 + AES",
|
||||
"LUKS_V1_SHA_512_SERPENT": "LUKS v1 SHA-512 + Serpent",
|
||||
"LUKS_V1_SHA_512_TWOFISH": "LUKS v1 SHA-512 + Twofish",
|
||||
"LUKS_V1_RIPEMD_160_AES": "LUKS v1 RIPEMD-160 + AES",
|
||||
"LUKS_V1_RIPEMD_160_SERPENT": "LUKS v1 RIPEMD-160 + Serpent",
|
||||
"LUKS_V1_RIPEMD_160_TWOFISH": "LUKS v1 RIPEMD-160 + Twofish",
|
||||
"TERRA_STATION_WALLET_AES256_CBC_PBKDF2_PASS": "Terra Station Wallet (AES256-CBC(PBKDF2($pass)))",
|
||||
"KEEPASS_KDBX_V2_V3_KEYFILE_ONLY": "KeePass (KDBX v2/v3) - keyfile only",
|
||||
"BISQ_WALLET_SCRYPT": "Bisq .wallet (scrypt)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_NO_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_NO_KEYCHAIN": "ENCsecurity Datavault (MD5/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_KEYCHAIN": "ENCsecurity Datavault (MD5/keychain)",
|
||||
"PYTHON_WERKZEUG_MD5_HMAC_MD5_KEY_SALT": "Python Werkzeug MD5 (HMAC-MD5 (key = $salt))",
|
||||
"PYTHON_WERKZEUG_SHA256_HMAC_SHA256_KEY_SALT": "Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt))",
|
||||
"DANE_RFC7929_RFC8162_SHA2_256": "DANE RFC7929/RFC8162 SHA2-256",
|
||||
"MD5_MD5_SALT_MD5_MD5_PASS": "md5(md5($salt).md5(md5($pass)))",
|
||||
"BCRYPT_SHA256_PASS": "bcrypt(sha256($pass))",
|
||||
"BCRYPT_HMAC_SHA256_PASS": "bcrypt(HMAC-SHA256($pass))",
|
||||
"ANOPE_IRC_SERVICES_ENC_SHA256": "Anope IRC Services (enc_sha256)",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin raw private key (P2PKH), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin raw private key (P2PKH), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), uncompressed",
|
||||
"BLAKE2S_256": "BLAKE2s-256",
|
||||
"SHANGMI_3_SM3": "ShangMi 3 (SM3)",
|
||||
"VEEAM_VBK": "Veeam VBK",
|
||||
"MS_SNTP": "MS SNTP",
|
||||
"SECURECRT_MASTERPASSPHRASE_V2": "SecureCRT MasterPassphrase v2",
|
||||
"DOMAIN_CACHED_CREDENTIALS_DCC_MS_CACHE_NT": "Domain Cached Credentials (DCC), MS Cache (NT)",
|
||||
"DOMAIN_CACHED_CREDENTIALS_2_DCC2_MS_CACHE_2_NT": "Domain Cached Credentials 2 (DCC2), MS Cache 2, (NT)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700": "md5(md5(md5($pass).$salt1).$salt2)",
|
||||
"HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8": "1Password, mobilekeychain (1Password 8)",
|
||||
"METAMASK_MOBILE_WALLET": "MetaMask Mobile Wallet",
|
||||
"NETIQ_SSPR_MD5": "NetIQ SSPR (MD5)",
|
||||
"NETIQ_SSPR_SHA1": "NetIQ SSPR (SHA1)",
|
||||
"NETIQ_SSPR_SHA_1_WITH_SALT": "NetIQ SSPR (SHA-1 with Salt)",
|
||||
"NETIQ_SSPR_SHA_256_WITH_SALT": "NetIQ SSPR (SHA-256 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_256_WITH_SALT": "Adobe AEM (SSPR, SHA-256 with Salt)",
|
||||
"NETIQ_SSPR_SHA_512_WITH_SALT": "NetIQ SSPR (SHA-512 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_512_WITH_SALT": "Adobe AEM (SSPR, SHA-512 with Salt)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA1": "NetIQ SSPR (PBKDF2WithHmacSHA1)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA256": "NetIQ SSPR (PBKDF2WithHmacSHA256)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA512": "NetIQ SSPR (PBKDF2WithHmacSHA512)",
|
||||
"KERBEROS_5_ETYPE_17_AS_REP": "Kerberos 5, etype 17, AS-REP",
|
||||
"KERBEROS_5_ETYPE_18_AS_REP": "Kerberos 5, etype 18, AS-REP",
|
||||
"EMPIRE_CMS_ADMIN_PASSWORD": "Empire CMS (Admin password)",
|
||||
"SHA512_SHA512_PASS_SALT": "sha512(sha512($pass).$salt)",
|
||||
"SHA512_SHA512_BIN_PASS_SALT": "sha512(sha512_bin($pass).$salt)",
|
||||
"DOGECHAIN_INFO_WALLET": "Dogechain.info Wallet",
|
||||
"CUBECART_WHIRLPOOL_SALT_PASS_SALT": "CubeCart (whirlpool($salt.$pass.$salt))",
|
||||
"KREMLIN_ENCRYPT_3_0_W_NEWDES": "Kremlin Encrypt 3.0 w/NewDES",
|
||||
"MD5_SHA1_MD5_PASS": "md5(sha1(md5($pass)))",
|
||||
"PBKDF1_SHA1": "PBKDF1-SHA1",
|
||||
"MD5_SALT1_PASS_SALT2": "md5($salt1.$pass.$salt2)",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_33100": "md5($salt.md5($pass).$salt)",
|
||||
"HMAC_BLAKE2S_KEY_PASS": "HMAC-BLAKE2S (key = $pass)",
|
||||
"MEGA_NZ_PASSWORD_PROTECTED_LINK_PBKDF2_HMAC_SHA512": "mega.nz password-protected link (PBKDF2-HMAC-SHA512)",
|
||||
"RC4_40_BIT_DROPN": "RC4 40-bit DropN",
|
||||
"RC4_72_BIT_DROPN": "RC4 72-bit DropN",
|
||||
"RC4_104_BIT_DROPN": "RC4 104-bit DropN",
|
||||
"RIPEMD_320": "RIPEMD-320",
|
||||
"HMAC_RIPEMD320_KEY_PASS": "HMAC-RIPEMD320 (key = $pass)",
|
||||
"HMAC_RIPEMD320_KEY_SALT": "HMAC-RIPEMD320 (key = $salt)",
|
||||
"MICROSOFT_ONLINE_ACCOUNT_PBKDF2_HMAC_SHA256_AES256": "Microsoft Online Account (PBKDF2-HMAC-SHA256 + AES256)",
|
||||
"WBB4_WOLTLAB_BURNING_BOARD_BCRYPT_BCRYPT_PASS": "WBB4 (Woltlab Burning Board) [bcrypt(bcrypt($pass))]",
|
||||
"CITRIX_NETSCALER_PBKDF2_HMAC_SHA256": "Citrix NetScaler (PBKDF2-HMAC-SHA256)",
|
||||
"ARGON2": "Argon2",
|
||||
"LUKS_V2_ARGON2_SHA_256_AES": "LUKS v2 argon2 + SHA-256 + AES",
|
||||
"MURMURHASH64A": "MurmurHash64A",
|
||||
"MURMURHASH64A_ZERO_SEED": "MurmurHash64A (zero seed)",
|
||||
"MURMURHASH64A_TRUNCATED_ZERO_SEED": "MurmurHash64A truncated (zero seed)",
|
||||
"KEEPASS_ARGON2_KDBX_V4": "KeePass Argon2 (KDBX v4)",
|
||||
"KEEPASS_AESKDF_KDBX_V4": "KeePass AESKDF (KDBX v4)",
|
||||
"SHA224_SHA224_PASS": "sha224(sha224($pass))",
|
||||
"SHA224_SHA1_PASS": "sha224(sha1($pass))",
|
||||
"MD6_256": "MD6 (256)",
|
||||
"BLOCKCHAIN_MY_WALLET_LEGACY_WALLETS": "Blockchain, My Wallet, Legacy Wallets",
|
||||
"BLAKE2B_256_MODE_34800": "BLAKE2b-256",
|
||||
"BLAKE2B_256_PASS_SALT": "BLAKE2b-256($pass.$salt)",
|
||||
"BLAKE2B_256_SALT_PASS": "BLAKE2b-256($salt.$pass)",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA512": "SAP CODVN H (PWDSALTEDHASH) isSHA512",
|
||||
"SM3CRYPT_SM3_SM3_UNIX": "sm3crypt $sm3$, SM3 (Unix)",
|
||||
"AS_400_SSHA1": "AS/400 SSHA1",
|
||||
"KERBEROS_5_ETYPE_23_TGS_REP_NT": "Kerberos 5, etype 23, TGS-REP (NT)",
|
||||
"KERBEROS_5_ETYPE_23_AS_REP_NT": "Kerberos 5, etype 23, AS-REP (NT)",
|
||||
"WORDPRESS_BCRYPT_HMAC_SHA384_PASS": "Wordpress bcrypt(hmac-sha384($pass))",
|
||||
"GOST12512CRYPT_GOST12512HASH_UNIX": "gost12512crypt [$gost12512hash$] (Unix)",
|
||||
"PHPASS_MD5_PASS": "phpass(md5($pass))",
|
||||
"SYMFONY_LEGACY_SHA256": "Symfony Legacy SHA256",
|
||||
"YESCRYPT": "yescrypt",
|
||||
"GOST_YESCRYPT": "gost-yescrypt",
|
||||
"SCRYPTCRYPT_SCRYPT_UNIX": "scryptcrypt, scrypt (Unix)",
|
||||
"KDE_KWALLET_4_13_PBKDF2_HMAC_SHA512_BLOWFISH": "KDE KWallet 4.13+ (PBKDF2-HMAC-SHA512, Blowfish)",
|
||||
"KDE_KWALLET_4_13_SHA_1_BLOWFISH": "KDE KWallet < 4.13 (SHA-1, Blowfish)",
|
||||
"ARGON2ID_BRIDGED_REFERENCE_IMPLEMENTATION_TUNINGS": "Argon2id [Bridged: reference implementation + tunings]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_JANE_SMIX": "scrypt [Bridged: Scrypt-Jane SMix]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_YESCRYPT": "scrypt [Bridged: Scrypt-Yescrypt]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_FREE_THREADING": "Generic Hash [Bridged: Python Interpreter free-threading]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_WITH_GIL": "Generic Hash [Bridged: Python Interpreter with GIL]",
|
||||
"GENERIC_HASH_BRIDGED_RUST": "Generic Hash [Bridged: Rust]",
|
||||
"PLAINTEXT": "Plaintext",
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const (
|
||||
pinnedHashcatVersion = "v7.1.2-armory.1"
|
||||
pinnedHashcatCommit = "deadd52de22e2753e9caaff6d1b92e5a22a35626"
|
||||
pinnedHashcatModuleCount = 595
|
||||
pinnedHashcatCatalogSHA256 = "1c041b6370a37ccf07ae71ff29205c56c67eeb7dbe1f7bbc134e0373ecd19e4f"
|
||||
pinnedHashcatModeIDsSHA256 = "bb206df105890129937d0c688101a3065aad2ca45eea7765bfda60c40f232a63"
|
||||
pinnedHashcatEnumsSHA256 = "989144b3d2a57f9659bc5aa53df42403ba78b08e5a6fab616d43c14e0352e023"
|
||||
)
|
||||
|
||||
type hashcatCatalogEntry struct {
|
||||
mode int32
|
||||
enumName string
|
||||
description string
|
||||
}
|
||||
|
||||
func TestHashcatV7CatalogCoverage(t *testing.T) {
|
||||
version := pinnedHashcatVersion + "@" + pinnedHashcatCommit
|
||||
validateHashcatCatalogCardinality(t, version)
|
||||
catalog := collectHashcatCatalog(t, version)
|
||||
validateHashTypeValueRoundTrips(t)
|
||||
validateHashTypeDescriptionParity(t)
|
||||
validateHashcatCatalogDigests(t, version, catalog)
|
||||
}
|
||||
|
||||
func validateHashcatCatalogCardinality(t *testing.T, version string) {
|
||||
t.Helper()
|
||||
|
||||
if got := len(clientpb.HashType_name); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_name contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(clientpb.HashType_value); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_value contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(hashTypes); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s hashTypes contains %d descriptions, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectHashcatCatalog(t *testing.T, version string) []hashcatCatalogEntry {
|
||||
t.Helper()
|
||||
|
||||
catalog := make([]hashcatCatalogEntry, 0, pinnedHashcatModuleCount)
|
||||
seenDescriptions := make(map[string]int32, pinnedHashcatModuleCount)
|
||||
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
if enumName != "INVALID" || mode != 9999 {
|
||||
t.Errorf("invalid sentinel is %d -> %q, want 9999 -> INVALID", mode, enumName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if mode < 0 {
|
||||
t.Errorf("HashType_name contains negative mode %d (%s)", mode, enumName)
|
||||
continue
|
||||
}
|
||||
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Errorf("hashTypes is missing %d -> %s", mode, enumName)
|
||||
continue
|
||||
}
|
||||
if previous, duplicate := seenDescriptions[description]; duplicate {
|
||||
t.Errorf("Hashcat name %q is shared by modes %d and %d", description, previous, mode)
|
||||
}
|
||||
seenDescriptions[description] = mode
|
||||
if got, ok := clientpb.HashType_value[enumName]; !ok || got != mode {
|
||||
t.Errorf("HashType_value[%q] = %d, %v; want %d, true", enumName, got, ok, mode)
|
||||
}
|
||||
if got := clientpb.HashType(mode).String(); got != enumName {
|
||||
t.Errorf("HashType(%d).String() = %q, want %q", mode, got, enumName)
|
||||
}
|
||||
catalog = append(catalog, hashcatCatalogEntry{mode: mode, enumName: enumName, description: description})
|
||||
}
|
||||
if got := len(catalog); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s catalog contains %d real modes, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func validateHashTypeValueRoundTrips(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, mode := range clientpb.HashType_value {
|
||||
canonical, ok := clientpb.HashType_name[mode]
|
||||
if !ok || canonical != enumName {
|
||||
t.Errorf("HashType alias or orphan %q -> %d; canonical name is %q, %v", enumName, mode, canonical, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashTypeDescriptionParity(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, description := range hashTypes {
|
||||
mode, ok := clientpb.HashType_value[enumName]
|
||||
if !ok || mode == int32(clientpb.HashType_INVALID) {
|
||||
t.Errorf("hashTypes contains non-catalog key %q -> %q", enumName, description)
|
||||
}
|
||||
if description == "" {
|
||||
t.Errorf("hashTypes[%q] has an empty description", enumName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashcatCatalogDigests(t *testing.T, version string, catalog []hashcatCatalogEntry) {
|
||||
t.Helper()
|
||||
|
||||
sort.Slice(catalog, func(i, j int) bool { return catalog[i].mode < catalog[j].mode })
|
||||
var catalogRows strings.Builder
|
||||
var modeRows strings.Builder
|
||||
var enumRows strings.Builder
|
||||
for _, entry := range catalog {
|
||||
fmt.Fprintf(&catalogRows, "%d\t%s\n", entry.mode, entry.description)
|
||||
fmt.Fprintf(&modeRows, "%05d\n", entry.mode)
|
||||
fmt.Fprintf(&enumRows, "%d\t%s\t%s\n", entry.mode, entry.enumName, entry.description)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(catalogRows.String()))); got != pinnedHashcatCatalogSHA256 {
|
||||
t.Errorf("%s mode/name catalog SHA-256 = %s, want %s", version, got, pinnedHashcatCatalogSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(modeRows.String()))); got != pinnedHashcatModeIDsSHA256 {
|
||||
t.Errorf("%s mode ID SHA-256 = %s, want %s", version, got, pinnedHashcatModeIDsSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(enumRows.String()))); got != pinnedHashcatEnumsSHA256 {
|
||||
t.Errorf("%s mode/enum/name SHA-256 = %s, want %s", version, got, pinnedHashcatEnumsSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashcatIdentifierCollisionResolution(t *testing.T) {
|
||||
want := map[int32]string{
|
||||
141: "EPISERVER_6_X_NET_4_MODE_141",
|
||||
600: "BLAKE2B_256",
|
||||
1441: "EPISERVER_6_X_NET_4_MODE_1441",
|
||||
2611: "VBULLETIN_V3_8_5_MODE_2611",
|
||||
2711: "VBULLETIN_V3_8_5_MODE_2711",
|
||||
4110: "MD5_SALT_MD5_PASS_SALT_MODE_4110",
|
||||
4410: "MD5_SHA1_PASS_SALT_MODE_4410",
|
||||
4420: "MD5_SHA1_PASS_SALT_MODE_4420",
|
||||
4710: "SHA1_MD5_PASS_SALT_MODE_4710",
|
||||
14700: "ITUNES_BACKUP_10_0_MODE_14700",
|
||||
14800: "ITUNES_BACKUP_10_0_MODE_14800",
|
||||
20710: "SHA256_SHA256_PASS_SALT_MODE_20710",
|
||||
20730: "SHA256_SHA256_PASS_SALT_MODE_20730",
|
||||
21100: "SHA1_MD5_PASS_SALT_MODE_21100",
|
||||
21900: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900",
|
||||
31700: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700",
|
||||
31800: "HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8",
|
||||
33100: "MD5_SALT_MD5_PASS_SALT_MODE_33100",
|
||||
34800: "BLAKE2B_256_MODE_34800",
|
||||
6600: "HASHCAT_1PASSWORD_AGILEKEYCHAIN",
|
||||
8200: "HASHCAT_1PASSWORD_CLOUDKEYCHAIN",
|
||||
11600: "HASHCAT_7_ZIP",
|
||||
}
|
||||
for mode, enumName := range want {
|
||||
if got := clientpb.HashType_name[mode]; got != enumName {
|
||||
t.Errorf("HashType_name[%d] = %q, want collision-safe %q", mode, got, enumName)
|
||||
}
|
||||
}
|
||||
|
||||
groups := map[string][]int32{}
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
continue
|
||||
}
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Fatalf("hashTypes is missing collision input %d -> %s", mode, enumName)
|
||||
}
|
||||
base := normalizeHashcatIdentifier(description)
|
||||
groups[base] = append(groups[base], mode)
|
||||
}
|
||||
collisions := 0
|
||||
for _, modes := range groups {
|
||||
if len(modes) > 1 {
|
||||
collisions++
|
||||
}
|
||||
}
|
||||
if collisions != 8 {
|
||||
t.Errorf("normalized catalog contains %d collision groups, want 8", collisions)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHashcatIdentifier(name string) string {
|
||||
var builder strings.Builder
|
||||
underscore := false
|
||||
for _, r := range strings.ToUpper(name) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
underscore = false
|
||||
continue
|
||||
}
|
||||
if builder.Len() > 0 && !underscore {
|
||||
builder.WriteByte('_')
|
||||
underscore = true
|
||||
}
|
||||
}
|
||||
return strings.Trim(builder.String(), "_")
|
||||
}
|
||||
@@ -228,6 +228,14 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, chromeExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -241,7 +249,7 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func selectDebugTarget(targets []overlord.ChromeDebugTarget, con *console.Sliver
|
||||
if selected == "" {
|
||||
return nil
|
||||
}
|
||||
selectedID := strings.Split(selected, " ")[0]
|
||||
selectedID, _, _ := strings.Cut(selected, " ")
|
||||
selectedTarget := id2target[selectedID]
|
||||
return &selectedTarget
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package cursed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
@@ -190,6 +191,14 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, electronExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -202,7 +211,7 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -20,20 +20,47 @@ package cursed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const cursedStartCleanupTimeout = 30 * time.Second
|
||||
|
||||
func terminateStartedCursedProcess(con *console.SliverClient, sessionID string, pid uint32) error {
|
||||
const maxTerminatePID = uint32(1<<31 - 1)
|
||||
if con == nil || con.Rpc == nil || sessionID == "" || pid == 0 || pid > maxTerminatePID {
|
||||
return errors.New("cannot terminate incompletely tracked cursed process")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cursedStartCleanupTimeout)
|
||||
defer cancel()
|
||||
response, err := con.Rpc.Terminate(ctx, &sliverpb.TerminateReq{
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
Pid: int32(pid),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("terminate started cursed process %d: %w", pid, err)
|
||||
}
|
||||
if response.GetResponse().GetErr() != "" {
|
||||
return fmt.Errorf("terminate started cursed process %d: %s", pid, response.GetResponse().GetErr())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursedChromeCmd - Execute a .NET assembly in-memory.
|
||||
func CursedCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// Collect existing curses from core
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the docs command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the docs command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newDocsCommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.DocsStr,
|
||||
Short: "Browse the embedded Sliver docs in a TUI",
|
||||
Long: help.GetHelpFor([]string{consts.DocsStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
DocsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// DocsCmd launches the embedded docs browser TUI.
|
||||
func DocsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
entries, err := loadDocEntries()
|
||||
if err != nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Failed to load embedded docs: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
model := newDocsModel(entries)
|
||||
|
||||
width, height := 100, 32
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
model,
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
)
|
||||
if _, err := program.Run(); err != nil && con != nil {
|
||||
con.PrintErrorf("Docs TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func sampleDocs() []docEntry {
|
||||
return []docEntry{
|
||||
{
|
||||
Name: "Getting Started",
|
||||
Content: "# Getting Started\n\n- first step\n- second step",
|
||||
Description: "Getting Started",
|
||||
},
|
||||
{
|
||||
Name: "Operators",
|
||||
Content: "# Operators\n\nTeam workflows",
|
||||
Description: "Team workflows",
|
||||
},
|
||||
{
|
||||
Name: "Networking",
|
||||
Content: "# Networking\n\nTransport details",
|
||||
Description: "Transport details",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMarkdownWithGlowRendersBulletLists(t *testing.T) {
|
||||
rendered, err := renderMarkdownWithGlow(48, "# Title\n\n- first item")
|
||||
if err != nil {
|
||||
t.Fatalf("renderMarkdownWithGlow(): %v", err)
|
||||
}
|
||||
|
||||
plain := ansi.Strip(rendered)
|
||||
if !strings.Contains(plain, "Title") {
|
||||
t.Fatalf("expected heading text, got %q", plain)
|
||||
}
|
||||
if !strings.Contains(plain, "• first item") {
|
||||
t.Fatalf("expected glow-style bullet rendering, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownPrefersFirstMeaningfulLine(t *testing.T) {
|
||||
summary := summarizeMarkdown("# Title\n\n- Bullet\n\nParagraph")
|
||||
if summary != "Title" {
|
||||
t.Fatalf("expected first heading summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownStripsMarkdownAndUnicodeSymbols(t *testing.T) {
|
||||
summary := summarizeMarkdown("⚠️ **IMPORTANT:** This content was NOT created by the Sliver authors.")
|
||||
if summary != "IMPORTANT: This content was NOT created by the Sliver authors." {
|
||||
t.Fatalf("expected sanitized browser summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelDefaultsToGettingStarted(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
if model.currentDocName != "Getting Started" {
|
||||
t.Fatalf("expected Getting Started to be selected, got %q", model.currentDocName)
|
||||
}
|
||||
plain := ansi.Strip(model.viewer.View())
|
||||
if !strings.Contains(plain, "first step") {
|
||||
t.Fatalf("expected initial viewer content to render selected doc, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelSlashFocusesBrowserAndStartsFiltering(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
model.focus = docsFocusViewer
|
||||
|
||||
updated, cmd := model.Update(tea.KeyPressMsg{Text: "/", Code: '/'})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected slash to focus the search input")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusBrowser {
|
||||
t.Fatalf("expected slash to move focus to browser, got %v", updatedModel.focus)
|
||||
}
|
||||
if updatedModel.filterState != docsFilterStateFiltering {
|
||||
t.Fatalf("expected slash to enter filtering state, got %v", updatedModel.filterState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("oper")
|
||||
model.updateFilteredEntries(false)
|
||||
|
||||
visible := model.visibleEntries()
|
||||
if len(visible) != 1 || visible[0].Name != "Operators" {
|
||||
t.Fatalf("expected Operators to be the only filtered result, got %+v", visible)
|
||||
}
|
||||
if model.currentDocName != "Operators" {
|
||||
t.Fatalf("expected filtering to sync selection to Operators, got %q", model.currentDocName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEnterMovesFocusToViewer(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusViewer {
|
||||
t.Fatalf("expected enter to focus viewer, got %v", updatedModel.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEscClearsAppliedFilter(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("net")
|
||||
model.finishFiltering(false)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.filterState != docsFilterStateUnfiltered {
|
||||
t.Fatalf("expected esc to clear the filter, got %v", updatedModel.filterState)
|
||||
}
|
||||
if len(updatedModel.visibleEntries()) != len(sampleDocs()) {
|
||||
t.Fatalf("expected filter clear to restore all docs, got %d visible", len(updatedModel.visibleEntries()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelBrowserBodyPinsHelpToBottom(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 28)
|
||||
|
||||
body := ansi.Strip(model.renderBrowserBody(model.browserWidth, model.browserHeight))
|
||||
lines := strings.Split(body, "\n")
|
||||
if len(lines) != model.browserHeight {
|
||||
t.Fatalf("expected browser body height %d, got %d lines", model.browserHeight, len(lines))
|
||||
}
|
||||
if !strings.Contains(lines[len(lines)-1], "q quit") {
|
||||
t.Fatalf("expected help controls on the last browser line, got %q", lines[len(lines)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsWindowPollSchedulesResizeAndKeepsPolling(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(100, 28)
|
||||
|
||||
updated, cmd := model.Update(docsWindowPollMsg{width: 120, height: 34})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected window poll to schedule resize handling")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.width != 100 || updatedModel.height != 28 {
|
||||
t.Fatalf("expected poll tick to leave dimensions unchanged until WindowSizeMsg, got %dx%d", updatedModel.width, updatedModel.height)
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
batch, ok := msg.(tea.BatchMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected poll tick to return tea.BatchMsg, got %T", msg)
|
||||
}
|
||||
if len(batch) != 2 {
|
||||
t.Fatalf("expected poll tick batch size 2, got %d", len(batch))
|
||||
}
|
||||
|
||||
var sawWindowSize bool
|
||||
var sawPollTick bool
|
||||
for _, subcmd := range batch {
|
||||
if subcmd == nil {
|
||||
continue
|
||||
}
|
||||
submsg := subcmd()
|
||||
switch msg := submsg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
sawWindowSize = true
|
||||
if msg.Width != 120 || msg.Height != 34 {
|
||||
t.Fatalf("expected resize 120x34, got %dx%d", msg.Width, msg.Height)
|
||||
}
|
||||
case docsWindowPollMsg:
|
||||
sawPollTick = true
|
||||
}
|
||||
}
|
||||
|
||||
if !sawWindowSize {
|
||||
t.Fatal("expected poll tick to emit WindowSizeMsg")
|
||||
}
|
||||
if !sawPollTick {
|
||||
t.Fatal("expected poll tick to schedule another poll")
|
||||
}
|
||||
}
|
||||
@@ -396,10 +396,7 @@ func (m *editorModel) renderLine(index int) string {
|
||||
return ""
|
||||
}
|
||||
prefixWidth := m.lineNumberWidth()
|
||||
contentWidth := totalWidth - prefixWidth
|
||||
if contentWidth < 0 {
|
||||
contentWidth = 0
|
||||
}
|
||||
contentWidth := max(totalWidth-prefixWidth, 0)
|
||||
prefix := m.linePrefix(index, prefixWidth)
|
||||
|
||||
if index >= len(m.lines) {
|
||||
@@ -508,10 +505,7 @@ func (m *editorModel) lineNumberWidth() int {
|
||||
if !m.showLineNumbers {
|
||||
return 0
|
||||
}
|
||||
total := len(m.lines)
|
||||
if total < 1 {
|
||||
total = 1
|
||||
}
|
||||
total := max(len(m.lines), 1)
|
||||
return len(fmt.Sprintf("%d", total)) + 1
|
||||
}
|
||||
|
||||
@@ -559,10 +553,7 @@ func (m *editorModel) ensureCursorVisible() {
|
||||
m.top = 0
|
||||
}
|
||||
|
||||
width := m.textWidth() - m.lineNumberWidth()
|
||||
if width < 1 {
|
||||
width = 1
|
||||
}
|
||||
width := max(m.textWidth()-m.lineNumberWidth(), 1)
|
||||
if m.col < m.left {
|
||||
m.left = m.col
|
||||
} else if m.col >= m.left+width {
|
||||
@@ -1125,20 +1116,13 @@ func lastIndexOfRunes(haystack, needle []rune, end int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
clean := strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(clean, "/"); idx != -1 {
|
||||
return clean[idx+1:]
|
||||
if _, after, ok := strings.CutLast(clean, "/"); ok {
|
||||
return after
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -234,10 +234,12 @@ func parseEnvPairs(envPairs []string) (map[string]string, error) {
|
||||
func determineCommandName(command string) string {
|
||||
commandName := strings.ReplaceAll(command, "\\", "/")
|
||||
|
||||
commandName = commandName[strings.LastIndex(commandName, "/")+1:]
|
||||
if _, after, ok := strings.CutLast(commandName, "/"); ok {
|
||||
commandName = after
|
||||
}
|
||||
|
||||
if strings.Contains(commandName, ".") {
|
||||
commandName = commandName[:strings.LastIndex(commandName, ".")]
|
||||
if before, _, ok := strings.CutLast(commandName, "."); ok {
|
||||
commandName = before
|
||||
}
|
||||
|
||||
return commandName
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// splitExtensionArgs separates Sliver-owned flags (--save, --timeout, --help)
|
||||
// from the extension's own arguments in a raw argument slice.
|
||||
//
|
||||
// Extension commands run with cobra's DisableFlagParsing enabled (see
|
||||
// ExtensionRegisterCommand). Without it, pflag parses the command line first
|
||||
// and, because the extension's own flags (e.g. --type, --fqdn) are unknown to
|
||||
// pflag, ParseErrorsWhitelist.UnknownFlags causes pflag to silently drop each
|
||||
// unknown flag together with its value; the BOF argument parser never saw them
|
||||
// unless the operator injected cobra's "--" separator (#2309). Running with
|
||||
// flag parsing disabled hands us the raw, unstripped slice, so we separate the
|
||||
// Sliver-owned flags ourselves here.
|
||||
//
|
||||
// A literal "--" stops Sliver-flag scanning; every token after it is passed
|
||||
// through verbatim, preserving the long-standing workaround syntax. Unknown
|
||||
// tokens (the extension's named flags, their values and positional arguments)
|
||||
// are never consumed or reordered, so both forms work:
|
||||
//
|
||||
// delegationbof --timeout 30 --type 6 --fqdn child.htb.local
|
||||
// delegationbof -- --type 6 --fqdn child.htb.local
|
||||
func splitExtensionArgs(raw []string) (save bool, timeout int64, help bool, extArgs []string) {
|
||||
timeout = defaultTimeout
|
||||
extArgs = make([]string, 0, len(raw))
|
||||
|
||||
passthrough := false
|
||||
for i := 0; i < len(raw); i++ {
|
||||
tok := raw[i]
|
||||
|
||||
// Once we have seen "--", copy everything through untouched so the
|
||||
// existing `ext -- --foo bar` syntax keeps working identically.
|
||||
if passthrough {
|
||||
extArgs = append(extArgs, tok)
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case tok == "--":
|
||||
passthrough = true
|
||||
|
||||
case tok == "--help" || tok == "-h":
|
||||
help = true
|
||||
|
||||
case tok == "--save" || tok == "-s":
|
||||
save = true
|
||||
|
||||
case strings.HasPrefix(tok, "--save="):
|
||||
save = parseBoolValue(tok[len("--save="):], true)
|
||||
|
||||
case tok == "--timeout" || tok == "-t":
|
||||
// Consume the following token as the value, mirroring pflag. If no
|
||||
// token follows, leave the default in place rather than erroring.
|
||||
if i+1 < len(raw) {
|
||||
if n, err := strconv.ParseInt(raw[i+1], 10, 64); err == nil {
|
||||
timeout = n
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
case strings.HasPrefix(tok, "--timeout="):
|
||||
if n, err := strconv.ParseInt(tok[len("--timeout="):], 10, 64); err == nil {
|
||||
timeout = n
|
||||
}
|
||||
|
||||
default:
|
||||
// Not a Sliver-owned flag: hand it to the extension parser untouched.
|
||||
extArgs = append(extArgs, tok)
|
||||
}
|
||||
}
|
||||
|
||||
return save, timeout, help, extArgs
|
||||
}
|
||||
|
||||
// applyOwnedFlags republishes the manually parsed Sliver-owned flag values back
|
||||
// onto the command's flag set. DisableFlagParsing prevents pflag from populating
|
||||
// them, so helpers that read them via cmd.Flags() (notably ActiveTarget.Request,
|
||||
// which derives the gRPC timeout from --timeout) would otherwise observe only the
|
||||
// defaults.
|
||||
func applyOwnedFlags(cmd *cobra.Command, save bool, timeout int64) {
|
||||
if flag := cmd.Flags().Lookup("timeout"); flag != nil {
|
||||
_ = flag.Value.Set(strconv.FormatInt(timeout, 10))
|
||||
}
|
||||
if flag := cmd.Flags().Lookup("save"); flag != nil {
|
||||
_ = flag.Value.Set(strconv.FormatBool(save))
|
||||
}
|
||||
}
|
||||
|
||||
// parseBoolValue parses a --flag=value bool, defaulting to defVal when the value
|
||||
// is empty (the bare-flag form). It accepts the same spellings pflag does.
|
||||
func parseBoolValue(v string, defVal bool) bool {
|
||||
switch strings.ToLower(v) {
|
||||
case "", "true", "t", "1":
|
||||
return true
|
||||
case "false", "f", "0":
|
||||
return false
|
||||
}
|
||||
return defVal
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package extensions
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSplitExtensionArgs covers the argument-passthrough regression from #2309:
|
||||
// the extension's own named flags must reach the BOF parser intact regardless of
|
||||
// the cobra "--" separator, while Sliver-owned flags are still honoured.
|
||||
func TestSplitExtensionArgs(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in []string
|
||||
save bool
|
||||
timeout int64
|
||||
help bool
|
||||
extArgs []string
|
||||
}{
|
||||
{
|
||||
name: "named extension flags without -- separator (#2309 regression)",
|
||||
in: []string{"--type", "6", "--fqdn", "child.htb.local"},
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
|
||||
},
|
||||
{
|
||||
name: "named extension flags with -- separator (existing workaround)",
|
||||
in: []string{"--", "--type", "6", "--fqdn", "child.htb.local"},
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"--type", "6", "--fqdn", "child.htb.local"},
|
||||
},
|
||||
{
|
||||
name: "sliver-owned flags precede extension arguments",
|
||||
in: []string{"--timeout", "30", "--save", "--type", "6"},
|
||||
save: true,
|
||||
timeout: 30,
|
||||
extArgs: []string{"--type", "6"},
|
||||
},
|
||||
{
|
||||
name: "quoted value containing spaces stays a single argument",
|
||||
in: []string{"--name", "hello world", "--type", "6"},
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"--name", "hello world", "--type", "6"},
|
||||
},
|
||||
{
|
||||
name: "positional extension arguments are preserved",
|
||||
in: []string{"foo", "bar", "baz"},
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
{
|
||||
name: "single-dash and double-dash forms are preserved",
|
||||
in: []string{"-type", "6", "--fqdn", "child.htb.local"},
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"-type", "6", "--fqdn", "child.htb.local"},
|
||||
},
|
||||
{
|
||||
name: "equals form for timeout",
|
||||
in: []string{"--timeout=120", "--type", "6"},
|
||||
timeout: 120,
|
||||
extArgs: []string{"--type", "6"},
|
||||
},
|
||||
{
|
||||
name: "short -s save flag",
|
||||
in: []string{"-s", "--type", "6"},
|
||||
save: true,
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{"--type", "6"},
|
||||
},
|
||||
{
|
||||
name: "help flag is detected",
|
||||
in: []string{"--help"},
|
||||
help: true,
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
in: nil,
|
||||
timeout: defaultTimeout,
|
||||
extArgs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
save, timeout, help, extArgs := splitExtensionArgs(tc.in)
|
||||
|
||||
if save != tc.save {
|
||||
t.Errorf("save: got %v, want %v", save, tc.save)
|
||||
}
|
||||
if timeout != tc.timeout {
|
||||
t.Errorf("timeout: got %d, want %d", timeout, tc.timeout)
|
||||
}
|
||||
if help != tc.help {
|
||||
t.Errorf("help: got %v, want %v", help, tc.help)
|
||||
}
|
||||
if !reflect.DeepEqual(extArgs, tc.extArgs) {
|
||||
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseBoolValue checks the --flag=value helper used for --save=value.
|
||||
func TestParseBoolValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"", true},
|
||||
{"true", true},
|
||||
{"TRUE", true},
|
||||
{"1", true},
|
||||
{"t", true},
|
||||
{"false", false},
|
||||
{"0", false},
|
||||
{"no", false}, // unsupported spellings keep the default
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := parseBoolValue(tc.in, true)
|
||||
// "no" is not a pflag spelling, so it falls through to the default (true).
|
||||
want := tc.want
|
||||
if tc.in == "no" {
|
||||
want = true
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("parseBoolValue(%q): got %v, want %v", tc.in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ package extensions
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -129,9 +130,7 @@ func getInstalledManifests() map[string]*ExtensionManifest {
|
||||
// Name field.
|
||||
func getTemporarilyLoadedManifests() map[string]*ExtensionManifest {
|
||||
tempManifests := map[string]*ExtensionManifest{}
|
||||
for name, manifest := range loadedManifests {
|
||||
tempManifests[name] = manifest
|
||||
}
|
||||
maps.Copy(tempManifests, loadedManifests)
|
||||
return tempManifests
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -113,7 +133,104 @@ const (
|
||||
}`
|
||||
)
|
||||
|
||||
type registerExtensionTestRPC struct {
|
||||
rpcpb.SliverRPCClient
|
||||
register func(context.Context, *sliverpb.RegisterExtensionReq, ...grpc.CallOption) (*sliverpb.RegisterExtension, error)
|
||||
}
|
||||
|
||||
func (f *registerExtensionTestRPC) RegisterExtension(ctx context.Context, req *sliverpb.RegisterExtensionReq, opts ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
return f.register(ctx, req, opts...)
|
||||
}
|
||||
|
||||
func newExtensionTestConsole(t *testing.T, rpc rpcpb.SliverRPCClient, session *clientpb.Session, beacon *clientpb.Beacon) *console.SliverClient {
|
||||
t.Helper()
|
||||
|
||||
con := console.NewConsole(false)
|
||||
con.IsCLI = true
|
||||
con.Rpc = rpc
|
||||
con.ActiveTarget.Set(session, beacon)
|
||||
return con
|
||||
}
|
||||
|
||||
func TestRemoveExtensionByManifestNameOnlyDeletesManagedExtensions(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
|
||||
originalManifests := loadedManifests
|
||||
originalExtensions := loadedExtensions
|
||||
t.Cleanup(func() {
|
||||
loadedManifests = originalManifests
|
||||
loadedExtensions = originalExtensions
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extPath string
|
||||
wantDeleted bool
|
||||
}{
|
||||
{
|
||||
name: "installed extension",
|
||||
extPath: filepath.Join(clientRoot, "extensions", "installed"),
|
||||
wantDeleted: true,
|
||||
},
|
||||
{
|
||||
name: "temporarily loaded extension",
|
||||
extPath: t.TempDir(),
|
||||
wantDeleted: false,
|
||||
},
|
||||
{
|
||||
name: "extensions directory sibling",
|
||||
extPath: filepath.Join(clientRoot, "extensions-backup"),
|
||||
wantDeleted: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if err := os.MkdirAll(test.extPath, 0o700); err != nil {
|
||||
t.Fatalf("create extension directory: %v", err)
|
||||
}
|
||||
markerPath := filepath.Join(test.extPath, "source-code.txt")
|
||||
if err := os.WriteFile(markerPath, []byte("keep me"), 0o600); err != nil {
|
||||
t.Fatalf("create marker file: %v", err)
|
||||
}
|
||||
|
||||
manifestName := "test-extension"
|
||||
commandName := "test-command"
|
||||
command := &ExtCommand{CommandName: commandName}
|
||||
loadedManifests = map[string]*ExtensionManifest{
|
||||
manifestName: {RootPath: test.extPath, ExtCommand: []*ExtCommand{command}},
|
||||
}
|
||||
loadedExtensions = map[string]*ExtCommand{commandName: command}
|
||||
|
||||
found, err := RemoveExtensionByManifestName(manifestName, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("remove extension: %v", err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected extension manifest to be found")
|
||||
}
|
||||
if _, ok := loadedManifests[manifestName]; ok {
|
||||
t.Error("manifest was not unloaded")
|
||||
}
|
||||
if _, ok := loadedExtensions[commandName]; ok {
|
||||
t.Error("command was not unloaded")
|
||||
}
|
||||
|
||||
_, err = os.Stat(markerPath)
|
||||
if test.wantDeleted && !os.IsNotExist(err) {
|
||||
t.Errorf("managed extension directory was not deleted: %v", err)
|
||||
}
|
||||
if !test.wantDeleted && err != nil {
|
||||
t.Errorf("external extension directory was modified: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtensionManifest(t *testing.T) {
|
||||
expectedPath := util.ResolvePath("foo/test1.dll")
|
||||
|
||||
extManifest, err := ParseExtensionManifest([]byte(sample1))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing extension manifest: %s", err)
|
||||
@@ -150,8 +267,8 @@ func TestParseExtensionManifest(t *testing.T) {
|
||||
if extCmd.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extCmd.Files[0].Arch)
|
||||
}
|
||||
if extCmd.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extCmd.Files[0].Path)
|
||||
if extCmd.Files[0].Path != expectedPath {
|
||||
t.Errorf("Expected path '%s', got '%s'", expectedPath, extCmd.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,14 +295,228 @@ func TestParseExtensionManifest(t *testing.T) {
|
||||
if extManifest2.Files[0].Arch != "amd64" {
|
||||
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
|
||||
}
|
||||
if extManifest2.Files[0].Path != "/foo/test1.dll" {
|
||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest2.Files[0].Path)
|
||||
if extManifest2.Files[0].Path != expectedPath {
|
||||
t.Errorf("Expected path '%s', got '%s'", expectedPath, extManifest2.Files[0].Path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConvertOldManifestPreservesInit(t *testing.T) {
|
||||
const initExport = "InitializeExtension"
|
||||
|
||||
manifest, err := ParseExtensionManifest([]byte(`{
|
||||
"name": "legacy-test",
|
||||
"command_name": "legacy-test",
|
||||
"help": "some help",
|
||||
"init": "InitializeExtension",
|
||||
"files": [
|
||||
{
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"path": "foo/test.dll"
|
||||
}
|
||||
]
|
||||
}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing legacy extension manifest: %s", err)
|
||||
}
|
||||
if len(manifest.ExtCommand) != 1 {
|
||||
t.Fatalf("Expected 1 extension command, got %d", len(manifest.ExtCommand))
|
||||
}
|
||||
if manifest.ExtCommand[0].Init != initExport {
|
||||
t.Errorf("Expected init export %q, got %q", initExport, manifest.ExtCommand[0].Init)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterExtensionPropagatesInit(t *testing.T) {
|
||||
const initExport = "InitializeExtension"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *clientpb.Session
|
||||
beacon *clientpb.Beacon
|
||||
wantAsync bool
|
||||
wantID string
|
||||
}{
|
||||
{
|
||||
name: "session",
|
||||
session: &clientpb.Session{ID: "session-id"},
|
||||
wantID: "session-id",
|
||||
},
|
||||
{
|
||||
name: "beacon",
|
||||
beacon: &clientpb.Beacon{ID: "beacon-id"},
|
||||
wantAsync: true,
|
||||
wantID: "beacon-id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var gotReq *sliverpb.RegisterExtensionReq
|
||||
rpc := ®isterExtensionTestRPC{
|
||||
register: func(_ context.Context, req *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
gotReq = req
|
||||
return &sliverpb.RegisterExtension{Response: &commonpb.Response{}}, nil
|
||||
},
|
||||
}
|
||||
con := newExtensionTestConsole(t, rpc, test.session, test.beacon)
|
||||
|
||||
err := registerExtension("linux", &ExtCommand{CommandName: "test", Init: initExport}, []byte("extension"), nil, con)
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterExtension returned an error: %s", err)
|
||||
}
|
||||
if gotReq == nil {
|
||||
t.Fatal("RegisterExtension RPC was not called")
|
||||
}
|
||||
if gotReq.Init != initExport {
|
||||
t.Errorf("Expected init export %q, got %q", initExport, gotReq.Init)
|
||||
}
|
||||
if gotReq.Request.GetAsync() != test.wantAsync {
|
||||
t.Errorf("Expected async=%t, got %t", test.wantAsync, gotReq.Request.GetAsync())
|
||||
}
|
||||
if test.wantAsync && gotReq.Request.GetBeaconID() != test.wantID {
|
||||
t.Errorf("Expected beacon ID %q, got %q", test.wantID, gotReq.Request.GetBeaconID())
|
||||
}
|
||||
if !test.wantAsync && gotReq.Request.GetSessionID() != test.wantID {
|
||||
t.Errorf("Expected session ID %q, got %q", test.wantID, gotReq.Request.GetSessionID())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterExtensionWaitsForBeaconRegistration(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
rpc := ®isterExtensionTestRPC{
|
||||
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
close(started)
|
||||
<-release
|
||||
return &sliverpb.RegisterExtension{Response: &commonpb.Response{Async: true}}, nil
|
||||
},
|
||||
}
|
||||
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Timed out waiting for RegisterExtension RPC")
|
||||
}
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("RegisterExtension returned before the beacon registration was enqueued: %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
close(release)
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("RegisterExtension returned an error: %s", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Timed out waiting for RegisterExtension to return")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterExtensionReturnsBeaconRPCError(t *testing.T) {
|
||||
wantErr := errors.New("register failed")
|
||||
rpc := ®isterExtensionTestRPC{
|
||||
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
return nil, wantErr
|
||||
},
|
||||
}
|
||||
con := newExtensionTestConsole(t, rpc, nil, &clientpb.Beacon{ID: "beacon-id"})
|
||||
|
||||
err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("Expected error %q, got %v", wantErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterExtensionRejectsNilResponse(t *testing.T) {
|
||||
rpc := ®isterExtensionTestRPC{
|
||||
register: func(_ context.Context, _ *sliverpb.RegisterExtensionReq, _ ...grpc.CallOption) (*sliverpb.RegisterExtension, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
con := newExtensionTestConsole(t, rpc, &clientpb.Session{ID: "session-id"}, nil)
|
||||
|
||||
if err := registerExtension("linux", &ExtCommand{CommandName: "test"}, []byte("extension"), nil, con); err == nil {
|
||||
t.Fatal("RegisterExtension accepted a nil response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionCommandVisibilityUsesExactTargetPairs(t *testing.T) {
|
||||
extension := &ExtCommand{Files: []*extensionFile{
|
||||
{OS: "windows", Arch: "amd64"},
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "darwin", Arch: "arm64"},
|
||||
}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *clientpb.Session
|
||||
beacon *clientpb.Beacon
|
||||
wantVisible bool
|
||||
}{
|
||||
{
|
||||
name: "cross-platform Linux session",
|
||||
session: &clientpb.Session{OS: "linux", Arch: "amd64"},
|
||||
wantVisible: true,
|
||||
},
|
||||
{
|
||||
name: "cross-platform macOS beacon",
|
||||
beacon: &clientpb.Beacon{OS: "darwin", Arch: "arm64"},
|
||||
wantVisible: true,
|
||||
},
|
||||
{
|
||||
name: "cross-platform Windows session",
|
||||
session: &clientpb.Session{OS: "windows", Arch: "amd64"},
|
||||
wantVisible: true,
|
||||
},
|
||||
{
|
||||
name: "Linux architecture mismatch",
|
||||
session: &clientpb.Session{OS: "linux", Arch: "arm64"},
|
||||
wantVisible: false,
|
||||
},
|
||||
{
|
||||
name: "macOS architecture mismatch",
|
||||
beacon: &clientpb.Beacon{OS: "darwin", Arch: "amd64"},
|
||||
wantVisible: false,
|
||||
},
|
||||
{
|
||||
name: "Windows architecture mismatch",
|
||||
session: &clientpb.Session{OS: "windows", Arch: "386"},
|
||||
wantVisible: false,
|
||||
},
|
||||
{
|
||||
name: "unsupported native extension target",
|
||||
beacon: &clientpb.Beacon{OS: "linux", Arch: "riscv64"},
|
||||
wantVisible: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
con := newExtensionTestConsole(t, nil, test.session, test.beacon)
|
||||
command := &cobra.Command{Annotations: makeCommandPlatformFilters(extension)}
|
||||
activeFilters := con.App.Menu(consts.ImplantMenu).ActiveFiltersFor(command)
|
||||
visible := len(activeFilters) == 0
|
||||
if visible != test.wantVisible {
|
||||
t.Fatalf("command visibility = %t, want %t (active filters: %v)", visible, test.wantVisible, activeFilters)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultipleCmdManifest(t *testing.T) {
|
||||
expectedPath := util.ResolvePath("ex.dll")
|
||||
|
||||
mextManifest, err := ParseExtensionManifest([]byte(multicmd))
|
||||
if err != nil {
|
||||
t.Errorf("error parsing manifest: %s", err)
|
||||
@@ -206,11 +537,11 @@ func TestParseMultipleCmdManifest(t *testing.T) {
|
||||
if mextManifest.ExtCommand[1].Entrypoint != "Test2" {
|
||||
t.Errorf("expected entrypoint Test2, got %s", mextManifest.ExtCommand[1].Entrypoint)
|
||||
}
|
||||
if mextManifest.ExtCommand[0].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
|
||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
||||
if mextManifest.ExtCommand[0].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[0].Files[0].Path)
|
||||
}
|
||||
if mextManifest.ExtCommand[1].Files[0].Path != "/ex.dll" { //path cleaning adds a root path here? I am not sure if this should be a bug or not... works fine in prod
|
||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
||||
if mextManifest.ExtCommand[1].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[1].Files[0].Path)
|
||||
}
|
||||
//maybe some more? args?
|
||||
}
|
||||
@@ -317,3 +648,285 @@ func TestParseExtensionManifestErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtensionManifestBOFExecutor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "multi-command manifest",
|
||||
data: `{
|
||||
"name":"modern-bof",
|
||||
"commands":[{
|
||||
"command_name":"modern-bof",
|
||||
"help":"test",
|
||||
"bof_executor":"reflektor",
|
||||
"files":[{"os":"windows","arch":"amd64","path":"modern.o"}]
|
||||
}]
|
||||
}`,
|
||||
want: BOFExecutorReflektor,
|
||||
},
|
||||
{
|
||||
name: "legacy manifest",
|
||||
data: `{
|
||||
"name":"legacy-bof",
|
||||
"command_name":"legacy-bof",
|
||||
"help":"test",
|
||||
"bof_executor":"coff-loader",
|
||||
"depends_on":"custom-loader",
|
||||
"files":[{"os":"windows","arch":"amd64","path":"legacy.o"}]
|
||||
}`,
|
||||
want: BOFExecutorCOFFLoader,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
manifest, err := ParseExtensionManifest([]byte(test.data))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseExtensionManifest returned an error: %s", err)
|
||||
}
|
||||
if got := manifest.ExtCommand[0].BOFExecutor; got != test.want {
|
||||
t.Fatalf("BOFExecutor = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtensionManifestRejectsInvalidBOFExecutor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
executor string
|
||||
dependsOn string
|
||||
wantErrPart string
|
||||
}{
|
||||
{
|
||||
name: "executor must be exact lowercase",
|
||||
executor: "Reflektor",
|
||||
wantErrPart: "invalid `bof_executor`",
|
||||
},
|
||||
{
|
||||
name: "unknown executor",
|
||||
executor: "other-loader",
|
||||
wantErrPart: "invalid `bof_executor`",
|
||||
},
|
||||
{
|
||||
name: "coff loader requires dependency",
|
||||
executor: BOFExecutorCOFFLoader,
|
||||
wantErrPart: "no `depends_on` fallback",
|
||||
},
|
||||
{
|
||||
name: "coff loader rejects whitespace dependency",
|
||||
executor: BOFExecutorCOFFLoader,
|
||||
dependsOn: " \t",
|
||||
wantErrPart: "no `depends_on` fallback",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
manifest := &ExtensionManifest{
|
||||
Name: "test",
|
||||
ExtCommand: []*ExtCommand{{
|
||||
CommandName: "test",
|
||||
Help: "test",
|
||||
BOFExecutor: test.executor,
|
||||
DependsOn: test.dependsOn,
|
||||
Files: []*extensionFile{{
|
||||
OS: "windows", Arch: "amd64", Path: "test.o",
|
||||
}},
|
||||
}},
|
||||
}
|
||||
err := validManifest(manifest)
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantErrPart) {
|
||||
t.Fatalf("validManifest error = %v, want error containing %q", err, test.wantErrPart)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanExtensionExecution(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ext *ExtCommand
|
||||
path string
|
||||
capabilities uint64
|
||||
want extensionExecutionMode
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "native artifact remains native",
|
||||
ext: &ExtCommand{CommandName: "native"},
|
||||
path: "native.dll",
|
||||
want: extensionExecutionNative,
|
||||
},
|
||||
{
|
||||
name: "legacy manifest keeps dependency route",
|
||||
ext: &ExtCommand{CommandName: "legacy", DependsOn: "coff-loader"},
|
||||
path: "legacy.o",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
want: extensionExecutionCOFFLoader,
|
||||
},
|
||||
{
|
||||
name: "explicit coff loader is authoritative",
|
||||
ext: &ExtCommand{CommandName: "coff", BOFExecutor: BOFExecutorCOFFLoader, DependsOn: "custom-loader"},
|
||||
path: "coff.bin",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
want: extensionExecutionCOFFLoader,
|
||||
},
|
||||
{
|
||||
name: "explicit Reflektor uses capable target",
|
||||
ext: &ExtCommand{CommandName: "builtin", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
|
||||
path: "builtin.o",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
want: extensionExecutionReflektor,
|
||||
},
|
||||
{
|
||||
name: "explicit Reflektor falls back to dependency",
|
||||
ext: &ExtCommand{CommandName: "fallback", BOFExecutor: BOFExecutorReflektor, DependsOn: "coff-loader"},
|
||||
path: "fallback.o",
|
||||
want: extensionExecutionCOFFLoader,
|
||||
},
|
||||
{
|
||||
name: "dependency-free BOF defaults to Reflektor",
|
||||
ext: &ExtCommand{CommandName: "default"},
|
||||
path: "default.o",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
want: extensionExecutionReflektor,
|
||||
},
|
||||
{
|
||||
name: "uppercase object extension is a BOF",
|
||||
ext: &ExtCommand{CommandName: "uppercase"},
|
||||
path: "uppercase.O",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
want: extensionExecutionReflektor,
|
||||
},
|
||||
{
|
||||
name: "unsupported dependency-free BOF fails closed",
|
||||
ext: &ExtCommand{CommandName: "unsupported"},
|
||||
path: "unsupported.o",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "coff loader without dependency fails closed",
|
||||
ext: &ExtCommand{CommandName: "missing", BOFExecutor: BOFExecutorCOFFLoader},
|
||||
path: "missing.o",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid executor fails closed",
|
||||
ext: &ExtCommand{CommandName: "invalid", BOFExecutor: "other"},
|
||||
path: "invalid.o",
|
||||
capabilities: sliverpb.CapabilityBOFV1,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := planExtensionExecution(test.ext, test.path, test.capabilities)
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("planExtensionExecution returned mode %d, want error", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("planExtensionExecution returned an error: %s", err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("planExtensionExecution mode = %d, want %d", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCallExtensionRequestPreservesLegacyWireFields(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
objectData := []byte("object-data")
|
||||
loaderData := []byte("loader-data")
|
||||
objectPath := filepath.Join(root, "object.o")
|
||||
loaderPath := filepath.Join(root, "loader.dll")
|
||||
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
|
||||
t.Fatalf("write object fixture: %s", err)
|
||||
}
|
||||
if err := os.WriteFile(loaderPath, loaderData, 0o600); err != nil {
|
||||
t.Fatalf("write loader fixture: %s", err)
|
||||
}
|
||||
|
||||
const loaderName = "custom-loader"
|
||||
previousLoader, hadPreviousLoader := loadedExtensions[loaderName]
|
||||
defer func() {
|
||||
if hadPreviousLoader {
|
||||
loadedExtensions[loaderName] = previousLoader
|
||||
} else {
|
||||
delete(loadedExtensions, loaderName)
|
||||
}
|
||||
}()
|
||||
loaderManifest := &ExtensionManifest{Name: loaderName, RootPath: root}
|
||||
loadedExtensions[loaderName] = &ExtCommand{
|
||||
CommandName: loaderName,
|
||||
Entrypoint: "RunCOFF",
|
||||
Files: []*extensionFile{{
|
||||
OS: "windows", Arch: "amd64", Path: filepath.Base(loaderPath),
|
||||
}},
|
||||
Manifest: loaderManifest,
|
||||
}
|
||||
|
||||
legacyArgs := []byte{0x01, 0x02, 0x03, 0x04}
|
||||
ext := &ExtCommand{CommandName: "test-bof", DependsOn: loaderName}
|
||||
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionCOFFLoader, ext, objectPath, "RunCOFF", legacyArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
|
||||
}
|
||||
loaderHash := sha256.Sum256(loaderData)
|
||||
want := &sliverpb.CallExtensionReq{
|
||||
Name: hex.EncodeToString(loaderHash[:]),
|
||||
Export: "RunCOFF",
|
||||
Args: legacyArgs,
|
||||
}
|
||||
gotWire, err := proto.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal actual request: %s", err)
|
||||
}
|
||||
wantWire, err := proto.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal expected request: %s", err)
|
||||
}
|
||||
if !bytes.Equal(gotWire, wantWire) {
|
||||
t.Fatalf("legacy request wire bytes changed:\n got: %x\nwant: %x", gotWire, wantWire)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCallExtensionRequestForReflektor(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
objectData := []byte("object-data")
|
||||
objectPath := filepath.Join(root, "object.o")
|
||||
if err := os.WriteFile(objectPath, objectData, 0o600); err != nil {
|
||||
t.Fatalf("write object fixture: %s", err)
|
||||
}
|
||||
|
||||
typedArgs := []byte{0x05, 0x06}
|
||||
got, err := buildCallExtensionRequest("windows", "amd64", extensionExecutionReflektor, &ExtCommand{CommandName: "test-bof"}, objectPath, "go", typedArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCallExtensionRequest returned an error: %s", err)
|
||||
}
|
||||
objectHash := sha256.Sum256(objectData)
|
||||
if got.Name != hex.EncodeToString(objectHash[:]) {
|
||||
t.Errorf("Name = %q, want object hash %q", got.Name, hex.EncodeToString(objectHash[:]))
|
||||
}
|
||||
if got.Export != "go" {
|
||||
t.Errorf("Export = %q, want %q", got.Export, "go")
|
||||
}
|
||||
if !bytes.Equal(got.Args, typedArgs) {
|
||||
t.Errorf("Args = %x, want %x", got.Args, typedArgs)
|
||||
}
|
||||
if !bytes.Equal(got.BOFData, objectData) {
|
||||
t.Errorf("BOFData = %q, want %q", got.BOFData, objectData)
|
||||
}
|
||||
if !got.IsBOF {
|
||||
t.Error("IsBOF = false, want true")
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,21 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintErrorf("Extension path '%s' does not exist", extLocalPath)
|
||||
return
|
||||
}
|
||||
InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
|
||||
_ = InstallFromDir(extLocalPath, true, con, strings.HasSuffix(extLocalPath, ".tar.gz"))
|
||||
}
|
||||
|
||||
func readExtensionManifest(extLocalPath string, isGz bool) ([]byte, error) {
|
||||
if isGz {
|
||||
return util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
|
||||
}
|
||||
return os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
|
||||
}
|
||||
|
||||
func extensionPackageID(manifest *ExtensionManifest) string {
|
||||
if manifest.PackageName != "" {
|
||||
return manifest.PackageName
|
||||
}
|
||||
return manifest.Name
|
||||
}
|
||||
|
||||
// InstallFromDir installs a Sliver extension from either a local directory or gzipped archive.
|
||||
@@ -54,37 +68,28 @@ func ExtensionsInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
// - con: Sliver console client for displaying status and error messages
|
||||
// - isGz: Whether the source is a gzipped archive (true) or directory (false)
|
||||
//
|
||||
// The function will return early with error messages printed to console if:
|
||||
// The function returns errors after printing them to the console if:
|
||||
// - The manifest cannot be read or parsed
|
||||
// - Required directories cannot be created
|
||||
// - File copy operations fail
|
||||
// - User declines overwrite when prompted
|
||||
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) {
|
||||
var manifestData []byte
|
||||
var err error
|
||||
|
||||
if isGz {
|
||||
manifestData, err = util.ReadFileFromTarGz(extLocalPath, fmt.Sprintf("./%s", ManifestFileName))
|
||||
} else {
|
||||
manifestData, err = os.ReadFile(filepath.Join(extLocalPath, ManifestFileName))
|
||||
}
|
||||
//
|
||||
// Declining an overwrite prompt cancels the installation without an error.
|
||||
func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.SliverClient, isGz bool) error {
|
||||
manifestData, err := readExtensionManifest(extLocalPath, isGz)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error reading %s: %s", ManifestFileName, err)
|
||||
return
|
||||
return fmt.Errorf("read %s: %w", ManifestFileName, err)
|
||||
}
|
||||
|
||||
manifestF, err := ParseExtensionManifest(manifestData)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error parsing %s: %s", ManifestFileName, err)
|
||||
return
|
||||
return fmt.Errorf("parse %s: %w", ManifestFileName, err)
|
||||
}
|
||||
|
||||
// Use package name if available, otherwise use extension name
|
||||
// (Note, for v1 manifests this will actually be command_name)
|
||||
packageID := manifestF.Name
|
||||
if manifestF.PackageName != "" {
|
||||
packageID = manifestF.PackageName
|
||||
}
|
||||
packageID := extensionPackageID(manifestF)
|
||||
|
||||
//create repo path
|
||||
minstallPath := filepath.Join(assets.GetExtensionsDir(), filepath.Base(packageID))
|
||||
@@ -94,7 +99,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
|
||||
confirm := false
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
forceRemoveAll(minstallPath)
|
||||
@@ -103,13 +108,13 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
|
||||
err = os.MkdirAll(minstallPath, 0o700)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error creating extension directory: %s\n", err)
|
||||
return
|
||||
return fmt.Errorf("create extension directory: %w", err)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(minstallPath, ManifestFileName), manifestData, 0o600)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to write %s: %s\n", ManifestFileName, err)
|
||||
forceRemoveAll(minstallPath)
|
||||
return
|
||||
return fmt.Errorf("write %s: %w", ManifestFileName, err)
|
||||
}
|
||||
|
||||
for _, manifest := range manifestF.ExtCommand {
|
||||
@@ -125,7 +130,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
|
||||
if err != nil {
|
||||
con.PrintErrorf("\nError creating extension directory: %s\n", err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
return fmt.Errorf("create extension artifact directory: %w", err)
|
||||
}
|
||||
err = util.CopyFile(src, dst)
|
||||
if err != nil {
|
||||
@@ -135,11 +140,12 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error installing command: %s\n", err)
|
||||
forceRemoveAll(installPath)
|
||||
return
|
||||
return fmt.Errorf("install extension artifact: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installArtifact(extGzFilePath string, installPath string, artifactPath string) error {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type installArchiveMember struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
const installTestManifest = `{
|
||||
"name": "Test Extension",
|
||||
"package_name": "test-extension",
|
||||
"version": "v1.0.0",
|
||||
"commands": [{
|
||||
"command_name": "test-command",
|
||||
"help": "test command",
|
||||
"files": [{"os": "windows", "arch": "amd64", "path": "foo/test.dll"}]
|
||||
}]
|
||||
}`
|
||||
|
||||
func writeInstallArchive(t *testing.T, prefix string, members ...installArchiveMember) string {
|
||||
t.Helper()
|
||||
archivePath := filepath.Join(t.TempDir(), "extension.tar.gz")
|
||||
archive, err := os.Create(archivePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gzipWriter := gzip.NewWriter(archive)
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
for _, member := range members {
|
||||
header := &tar.Header{
|
||||
Name: prefix + member.name,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(member.data)),
|
||||
Typeflag: tar.TypeReg,
|
||||
}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tarWriter.Write(member.data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := archive.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return archivePath
|
||||
}
|
||||
|
||||
func testInstallConsole(t *testing.T) *console.SliverClient {
|
||||
t.Helper()
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(
|
||||
con, nil, nil, nil, emptyCommands, emptyCommands, false, "",
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return con
|
||||
}
|
||||
|
||||
func TestInstallFromDirAcceptsPrefixedAndUnprefixedArchives(t *testing.T) {
|
||||
for _, prefix := range []string{"./", ""} {
|
||||
name := "unprefixed"
|
||||
if prefix != "" {
|
||||
name = "prefixed"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
archivePath := writeInstallArchive(t, prefix,
|
||||
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
|
||||
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
|
||||
)
|
||||
|
||||
if err := InstallFromDir(archivePath, false, testInstallConsole(t), true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
|
||||
if _, err := os.Stat(filepath.Join(installRoot, ManifestFileName)); err != nil {
|
||||
t.Fatalf("installed manifest: %v", err)
|
||||
}
|
||||
artifact, err := os.ReadFile(filepath.Join(installRoot, "foo", "test.dll"))
|
||||
if err != nil {
|
||||
t.Fatalf("installed artifact: %v", err)
|
||||
}
|
||||
if string(artifact) != "extension data" {
|
||||
t.Fatalf("artifact data = %q", artifact)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallFromDirReturnsMissingManifest(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
archivePath := writeInstallArchive(t, "./",
|
||||
installArchiveMember{name: "foo/test.dll", data: []byte("extension data")},
|
||||
)
|
||||
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Fatalf("error = %v, want fs.ErrNotExist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallFromDirReturnsMissingArtifactAndCleansUp(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
archivePath := writeInstallArchive(t, "./",
|
||||
installArchiveMember{name: ManifestFileName, data: []byte(installTestManifest)},
|
||||
)
|
||||
err := InstallFromDir(archivePath, false, testInstallConsole(t), true)
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Fatalf("error = %v, want fs.ErrNotExist", err)
|
||||
}
|
||||
installRoot := filepath.Join(assets.GetExtensionsDir(), "test-extension")
|
||||
if _, err := os.Stat(installRoot); !errors.Is(err, fs.ErrNotExist) {
|
||||
t.Fatalf("install directory was not cleaned up: %v", err)
|
||||
}
|
||||
}
|
||||
+181
-102
@@ -54,6 +54,20 @@ const (
|
||||
|
||||
// ManifestFileName - Extension manifest file name.
|
||||
ManifestFileName = "extension.json"
|
||||
|
||||
// BOFExecutorReflektor selects the built-in Reflektor BOF executor.
|
||||
BOFExecutorReflektor = "reflektor"
|
||||
|
||||
// BOFExecutorCOFFLoader selects the manifest's depends_on COFF loader.
|
||||
BOFExecutorCOFFLoader = "coff-loader"
|
||||
)
|
||||
|
||||
type extensionExecutionMode uint8
|
||||
|
||||
const (
|
||||
extensionExecutionNative extensionExecutionMode = iota
|
||||
extensionExecutionCOFFLoader
|
||||
extensionExecutionReflektor
|
||||
)
|
||||
|
||||
var loadedExtensions = map[string]*ExtCommand{}
|
||||
@@ -72,6 +86,7 @@ type ExtensionManifest_ struct {
|
||||
Arguments []*extensionArgument `json:"arguments"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
DependsOn string `json:"depends_on"`
|
||||
BOFExecutor string `json:"bof_executor"`
|
||||
Init string `json:"init"`
|
||||
|
||||
RootPath string `json:"-"`
|
||||
@@ -100,6 +115,7 @@ type ExtCommand struct {
|
||||
Arguments []*extensionArgument `json:"arguments"`
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
DependsOn string `json:"depends_on"`
|
||||
BOFExecutor string `json:"bof_executor"`
|
||||
Init string `json:"init"`
|
||||
Schema *packages.OutputSchema `json:"schema"`
|
||||
|
||||
@@ -218,9 +234,11 @@ func convertOldManifest(old *ExtensionManifest_) *ExtensionManifest {
|
||||
{
|
||||
CommandName: old.CommandName,
|
||||
DependsOn: old.DependsOn,
|
||||
BOFExecutor: old.BOFExecutor,
|
||||
Help: old.Help,
|
||||
LongHelp: old.LongHelp,
|
||||
Entrypoint: old.Entrypoint,
|
||||
Init: old.Init,
|
||||
Files: old.Files,
|
||||
Arguments: old.Arguments,
|
||||
Schema: nil,
|
||||
@@ -270,6 +288,15 @@ func validManifest(manifest *ExtensionManifest) error {
|
||||
if extManifest.CommandName == "" {
|
||||
return errors.New("missing `command_name` field in extension manifest")
|
||||
}
|
||||
switch extManifest.BOFExecutor {
|
||||
case "", BOFExecutorReflektor:
|
||||
case BOFExecutorCOFFLoader:
|
||||
if strings.TrimSpace(extManifest.DependsOn) == "" {
|
||||
return fmt.Errorf("command %q uses bof_executor %q but has no `depends_on` fallback", extManifest.CommandName, BOFExecutorCOFFLoader)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("command %q has invalid `bof_executor` %q (must be %q or %q)", extManifest.CommandName, extManifest.BOFExecutor, BOFExecutorReflektor, BOFExecutorCOFFLoader)
|
||||
}
|
||||
if len(extManifest.Files) == 0 {
|
||||
return errors.New("missing `files` field in extension manifest")
|
||||
}
|
||||
@@ -370,16 +397,19 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runExtensionCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.ExtensionHelpGroup,
|
||||
Annotations: makeCommandPlatformFilters(extCmd),
|
||||
GroupID: consts.ExtensionHelpGroup,
|
||||
// DisableFlagParsing keeps the extension's own flags (which are unknown to
|
||||
// pflag) from being silently stripped together with their values. We split
|
||||
// Sliver-owned flags out of the raw slice ourselves in runExtensionCmd (#2309).
|
||||
DisableFlagParsing: true,
|
||||
Annotations: makeCommandPlatformFilters(extCmd),
|
||||
}
|
||||
|
||||
// Flags
|
||||
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
|
||||
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")
|
||||
extensionCmd.Flags().AddFlagSet(f)
|
||||
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
|
||||
// Completions
|
||||
comps := carapace.Gen(extensionCmd)
|
||||
@@ -468,44 +498,30 @@ func registerExtension(goos string, ext *ExtCommand, binData []byte, cmd *cobra.
|
||||
//set extension name to a hash of the data to avoid loading more than one instance
|
||||
bd := sha256.Sum256(binData)
|
||||
name := hex.EncodeToString(bd[:])
|
||||
sess, beac := con.ActiveTarget.GetInteractive()
|
||||
sess, _ := con.ActiveTarget.GetInteractive()
|
||||
ctrl := make(chan bool)
|
||||
//first time run of an extension will require some waiting depending on the size
|
||||
if sess != nil {
|
||||
msg := fmt.Sprintf("Sending %s to implant ...", ext.CommandName)
|
||||
con.SpinUntil(msg, ctrl)
|
||||
}
|
||||
//don't block if we are in beacon mode
|
||||
if beac != nil && sess == nil {
|
||||
go func() {
|
||||
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: name,
|
||||
Data: binData,
|
||||
OS: goos,
|
||||
Init: ext.Init,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
if err != nil {
|
||||
con.PrintErrorf("Error registering extension: %s\n", err)
|
||||
}
|
||||
if registerResp.Response != nil && registerResp.Response.Err != "" {
|
||||
con.PrintErrorf("Error registering extension: %s\n", errors.New(registerResp.Response.Err))
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
//session mode (hopefully)
|
||||
registerResp, err := con.Rpc.RegisterExtension(context.Background(), &sliverpb.RegisterExtensionReq{
|
||||
Name: name,
|
||||
Data: binData,
|
||||
OS: goos,
|
||||
Init: ext.Init,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if sess != nil {
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if registerResp == nil {
|
||||
return errors.New("received empty register extension response")
|
||||
}
|
||||
if registerResp.Response != nil && registerResp.Response.Err != "" {
|
||||
return errors.New(registerResp.Response.Err)
|
||||
}
|
||||
@@ -528,7 +544,102 @@ func loadDep(goos string, goarch string, depName string, cmd *cobra.Command, con
|
||||
return fmt.Errorf("missing dependency %s", depName)
|
||||
}
|
||||
|
||||
func planExtensionExecution(ext *ExtCommand, binPath string, capabilities uint64) (extensionExecutionMode, error) {
|
||||
isBOF := ext.BOFExecutor != "" || strings.EqualFold(filepath.Ext(binPath), ".o")
|
||||
if !isBOF {
|
||||
return extensionExecutionNative, nil
|
||||
}
|
||||
dependsOn := strings.TrimSpace(ext.DependsOn)
|
||||
|
||||
switch ext.BOFExecutor {
|
||||
case BOFExecutorCOFFLoader:
|
||||
if dependsOn == "" {
|
||||
return extensionExecutionNative, fmt.Errorf("BOF command %q selects %q but has no depends_on loader", ext.CommandName, BOFExecutorCOFFLoader)
|
||||
}
|
||||
return extensionExecutionCOFFLoader, nil
|
||||
case "":
|
||||
// Existing manifests infer the loader from depends_on. Keep that route
|
||||
// even when the target supports the built-in executor.
|
||||
if dependsOn != "" {
|
||||
return extensionExecutionCOFFLoader, nil
|
||||
}
|
||||
case BOFExecutorReflektor:
|
||||
default:
|
||||
return extensionExecutionNative, fmt.Errorf("BOF command %q has invalid bof_executor %q", ext.CommandName, ext.BOFExecutor)
|
||||
}
|
||||
|
||||
if capabilities&sliverpb.CapabilityBOFV1 != 0 {
|
||||
return extensionExecutionReflektor, nil
|
||||
}
|
||||
if dependsOn != "" {
|
||||
return extensionExecutionCOFFLoader, nil
|
||||
}
|
||||
return extensionExecutionNative, fmt.Errorf("target does not support built-in Reflektor BOF execution and command %q has no depends_on fallback", ext.CommandName)
|
||||
}
|
||||
|
||||
func activeTargetCapabilities(session *clientpb.Session, beacon *clientpb.Beacon) uint64 {
|
||||
if session != nil {
|
||||
return session.Capabilities
|
||||
}
|
||||
if beacon != nil {
|
||||
return beacon.Capabilities
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func buildCallExtensionRequest(goos string, goarch string, mode extensionExecutionMode, ext *ExtCommand, binPath string, entryPoint string, extensionArgs []byte) (*sliverpb.CallExtensionReq, error) {
|
||||
extData, err := os.ReadFile(binPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bd := sha256.Sum256(extData)
|
||||
name := hex.EncodeToString(bd[:])
|
||||
|
||||
if mode == extensionExecutionCOFFLoader {
|
||||
loader, found := loadedExtensions[ext.DependsOn]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("attempted to load non-existing extension: %s", ext.DependsOn)
|
||||
}
|
||||
loaderPath, err := loader.getFileForTarget(goos, goarch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get file for extension %s: %w", ext.DependsOn, err)
|
||||
}
|
||||
loaderData, err := os.ReadFile(loaderPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dep read file error: %w", err)
|
||||
}
|
||||
if len(loaderData) == 0 {
|
||||
return nil, fmt.Errorf("read empty file: %s", loaderPath)
|
||||
}
|
||||
bd = sha256.Sum256(loaderData)
|
||||
name = hex.EncodeToString(bd[:])
|
||||
}
|
||||
|
||||
req := &sliverpb.CallExtensionReq{
|
||||
Name: name,
|
||||
Export: entryPoint,
|
||||
Args: extensionArgs,
|
||||
}
|
||||
if mode == extensionExecutionReflektor {
|
||||
req.BOFData = extData
|
||||
req.IsBOF = true
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// The command runs with DisableFlagParsing, so args is the raw, unstripped
|
||||
// slice. Separate Sliver-owned flags from the extension's own arguments and
|
||||
// re-publish them on the flag set (so Request()'s --timeout still works),
|
||||
// then feed only the extension arguments to the BOF/DLL parsers (#2309).
|
||||
save, timeout, helpRequested, extArgs := splitExtensionArgs(args)
|
||||
if helpRequested {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
applyOwnedFlags(cmd, save, timeout)
|
||||
args = extArgs
|
||||
|
||||
var (
|
||||
err error
|
||||
extensionArgs []byte
|
||||
@@ -555,27 +666,32 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
|
||||
checkCache := session != nil
|
||||
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
|
||||
con.PrintErrorf("Could not load extension: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
binPath, err := ext.getFileForTarget(goos, goarch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to read extension file: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
isBOF := filepath.Ext(binPath) == ".o"
|
||||
executionMode, err := planExtensionExecution(ext, binPath, activeTargetCapabilities(session, beacon))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not execute extension: %s\n", err)
|
||||
return
|
||||
}
|
||||
if executionMode != extensionExecutionReflektor {
|
||||
checkCache := session != nil
|
||||
if err = loadExtension(goos, goarch, checkCache, ext, cmd, con); err != nil {
|
||||
con.PrintErrorf("Could not load extension: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// BOFs (Beacon Object Files) are a specific kind of extensions
|
||||
// that require another extension (a COFF loader) to be present.
|
||||
// that either use the built-in Reflektor executor or another extension.
|
||||
// BOFs also have strongly typed arguments that need to be parsed in the proper way.
|
||||
// This block will pack both the BOF data and its arguments into a single buffer that
|
||||
// the loader will extract and load.
|
||||
if isBOF {
|
||||
// Beacon Object File -- requires a COFF loader
|
||||
switch executionMode {
|
||||
case extensionExecutionCOFFLoader:
|
||||
// Legacy COFF loaders expect the entrypoint, object, and typed argument
|
||||
// buffer packed into the existing outer argument envelope.
|
||||
extensionArgs, err = getBOFArgs(cmd, args, binPath, ext)
|
||||
if err != nil {
|
||||
con.PrintErrorf("BOF args error: %s\n", err)
|
||||
@@ -583,7 +699,15 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
extName = ext.DependsOn
|
||||
entryPoint = loadedExtensions[extName].Entrypoint // should exist at this point
|
||||
} else {
|
||||
case extensionExecutionReflektor:
|
||||
extensionArgs, err = ParseFlagArgumentsToBuffer(cmd, args, binPath, ext)
|
||||
if err != nil {
|
||||
con.PrintErrorf("BOF args error: %s\n", err)
|
||||
return
|
||||
}
|
||||
extName = ext.CommandName
|
||||
entryPoint = ext.Entrypoint
|
||||
default:
|
||||
// Regular DLL - Just join the arguments with spaces
|
||||
if len(args) > 0 {
|
||||
extensionArgs = []byte(strings.Join(args, " "))
|
||||
@@ -597,45 +721,15 @@ func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
ctrl := make(chan bool)
|
||||
msg := fmt.Sprintf("Executing %s ...", cmd.Name())
|
||||
con.SpinUntil(msg, ctrl)
|
||||
extdata, err := os.ReadFile(binPath)
|
||||
callExtReq, err := buildCallExtensionRequest(goos, goarch, executionMode, ext, binPath, entryPoint, extensionArgs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("ext read file error: %s\n", err)
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
con.PrintErrorf("Could not prepare extension call: %s\n", err)
|
||||
return
|
||||
}
|
||||
bd := sha256.Sum256(extdata)
|
||||
name := hex.EncodeToString(bd[:])
|
||||
if isBOF {
|
||||
//if we are using a bof, we are actually calling the coffloader extension - so get the file from dep ref and use that shasum
|
||||
|
||||
if extension, found := loadedExtensions[ext.DependsOn]; found {
|
||||
dep, err := extension.getFileForTarget(goos, goarch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not get file for extension %s", ext.DependsOn)
|
||||
return
|
||||
}
|
||||
depdata, err := os.ReadFile(dep)
|
||||
if err != nil {
|
||||
con.PrintErrorf("dep read file error: %s\n", err)
|
||||
return
|
||||
}
|
||||
if len(depdata) == 0 {
|
||||
con.PrintErrorf("read empty file: %s\n", dep)
|
||||
return
|
||||
}
|
||||
bd = sha256.Sum256(depdata)
|
||||
name = hex.EncodeToString(bd[:])
|
||||
} else {
|
||||
// handle error
|
||||
con.PrintErrorf("attempted to load non-existing extension: %s", ext.DependsOn)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
callExtResp, err := con.Rpc.CallExtension(context.Background(), &sliverpb.CallExtensionReq{
|
||||
Name: name,
|
||||
Export: entryPoint,
|
||||
Args: extensionArgs,
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
})
|
||||
callExtReq.Request = con.ActiveTarget.Request(cmd)
|
||||
callExtResp, err := con.Rpc.CallExtension(context.Background(), callExtReq)
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
@@ -850,37 +944,22 @@ func makeExtensionFlagNameCompletion(extCmd *ExtCommand) carapace.Action {
|
||||
}
|
||||
|
||||
func makeCommandPlatformFilters(extCmd *ExtCommand) map[string]string {
|
||||
filtersOS := make(map[string]bool)
|
||||
filtersArch := make(map[string]bool)
|
||||
|
||||
var all []string
|
||||
|
||||
// Only add filters for architectures when there OS matters.
|
||||
supportedTargets := make(map[string]struct{}, len(extCmd.Files))
|
||||
for _, file := range extCmd.Files {
|
||||
filtersOS[file.OS] = true
|
||||
|
||||
if filtersOS[file.OS] {
|
||||
filtersArch[file.Arch] = true
|
||||
}
|
||||
supportedTargets[file.OS+"/"+file.Arch] = struct{}{}
|
||||
}
|
||||
|
||||
for os, enabled := range filtersOS {
|
||||
if enabled {
|
||||
all = append(all, os)
|
||||
// Console command filters are hide-on-match, so annotate the command with
|
||||
// every exact target pair it does not support. A separate fallback hides all
|
||||
// native extensions on target pairs outside the supported loader matrix.
|
||||
filters := []string{consts.NativeExtensionUnsupportedTargetFilter}
|
||||
for _, target := range consts.NativeExtensionTargets() {
|
||||
if _, supported := supportedTargets[target.GOOS+"/"+target.GOARCH]; !supported {
|
||||
filters = append(filters, target.Filter)
|
||||
}
|
||||
}
|
||||
|
||||
for arch, enabled := range filtersArch {
|
||||
if enabled {
|
||||
all = append(all, arch)
|
||||
}
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
appConsole.CommandFilterKey: strings.Join(all, ","),
|
||||
appConsole.CommandFilterKey: strings.Join(filters, ","),
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -100,21 +99,11 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If path is outside extensions directory, prompt for confirmation
|
||||
if !strings.HasPrefix(extPath, assets.GetExtensionsDir()) {
|
||||
confirm := false
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' extension directory from filesystem?", manifestName), &confirm)
|
||||
if !confirm {
|
||||
// Skip the forceRemoveAll but continue with the rest
|
||||
delete(loadedManifests, manifestName)
|
||||
for _, cmd := range man.ExtCommand {
|
||||
delete(loadedExtensions, cmd.CommandName)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
// Only installed extensions are owned by Sliver and may be deleted.
|
||||
// Temporarily loaded extensions can live in arbitrary source directories.
|
||||
if isWithinExtensionsDir(extPath) {
|
||||
forceRemoveAll(extPath)
|
||||
}
|
||||
|
||||
forceRemoveAll(extPath)
|
||||
delete(loadedManifests, manifestName)
|
||||
for _, cmd := range man.ExtCommand {
|
||||
delete(loadedExtensions, cmd.CommandName)
|
||||
@@ -124,6 +113,14 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isWithinExtensionsDir(extPath string) bool {
|
||||
relPath, err := filepath.Rel(assets.GetExtensionsDir(), extPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return relPath != "." && filepath.IsLocal(relPath)
|
||||
}
|
||||
|
||||
func forceRemoveAll(rootPath string) {
|
||||
util.ChmodR(rootPath, 0o600, 0o700)
|
||||
os.RemoveAll(rootPath)
|
||||
|
||||
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
GroupID: consts.PayloadsHelpGroup,
|
||||
}
|
||||
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
// Session flags and completions.
|
||||
@@ -159,7 +159,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("save", "s", "", "directory/file to the binary to")
|
||||
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
|
||||
f.StringP("name", "n", "", "Implant name")
|
||||
|
||||
f.BoolP("external-builder", "E", false, "use an external builder")
|
||||
})
|
||||
flags.BindFlagCompletions(profilesGenerateCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||
@@ -340,6 +340,13 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
||||
|
||||
f.Uint32P("key-exchange", "X", DefaultWGKeyExPort, "wg key-exchange port")
|
||||
f.Uint32P("tcp-comms", "T", DefaultWGNPort, "wg c2 comms port")
|
||||
f.Bool("include-mtls", false, "force include mtls transport")
|
||||
f.Bool("include-http", false, "force include http transport")
|
||||
f.Bool("include-wg", false, "force include wireguard transport")
|
||||
f.Bool("include-dns", false, "force include dns transport")
|
||||
f.Bool("include-named-pipe", false, "force include named-pipe transport")
|
||||
f.Bool("include-tcp-pivot", false, "force include tcp-pivot transport")
|
||||
f.Bool("all-protocols", false, "force include all transport protocols")
|
||||
|
||||
f.BoolP("run-at-load", "R", false, "run the implant entrypoint from DllMain/Constructor (shared library only)")
|
||||
f.BoolP("netgo", "q", false, "force the use of netgo")
|
||||
@@ -360,7 +367,7 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
||||
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
|
||||
bindSpoofMetadataFlag(f)
|
||||
|
||||
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
|
||||
f.StringP("format", "f", "exe", "Specifies the output formats, valid values are: 'exe', 'shared' (for dynamic libraries), 'archive' (for Go c-archives), 'service' (see: `psexec` for more info) and 'shellcode' (windows, darwin/arm64, linux/amd64, linux/arm64)")
|
||||
|
||||
// Shellcode generation options:
|
||||
// - Windows: Donut
|
||||
|
||||
@@ -24,6 +24,30 @@ func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
|
||||
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
|
||||
}
|
||||
|
||||
func TestProfilesGenerateExternalBuilderFlagRegistered(t *testing.T) {
|
||||
cmds := Commands(&console.SliverClient{})
|
||||
|
||||
profilesCmd := commandByUse(cmds, consts.ProfilesStr)
|
||||
if profilesCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.ProfilesStr)
|
||||
}
|
||||
profilesGenerateCmd := commandByUse(profilesCmd.Commands(), consts.GenerateStr)
|
||||
if profilesGenerateCmd == nil {
|
||||
t.Fatalf("missing %q subcommand under %q", consts.GenerateStr, consts.ProfilesStr)
|
||||
}
|
||||
|
||||
flag := profilesGenerateCmd.Flags().Lookup("external-builder")
|
||||
if flag == nil {
|
||||
t.Fatalf("%q missing %q flag", profilesGenerateCmd.Use, "external-builder")
|
||||
}
|
||||
if flag.Shorthand != "E" {
|
||||
t.Fatalf("%q %q flag shorthand mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.Shorthand, "E")
|
||||
}
|
||||
if flag.DefValue != "false" {
|
||||
t.Fatalf("%q %q flag default mismatch: got=%q want=%q", profilesGenerateCmd.Use, "external-builder", flag.DefValue, "false")
|
||||
}
|
||||
}
|
||||
|
||||
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ var (
|
||||
"linux/arm64": true,
|
||||
"windows/386": true,
|
||||
"windows/amd64": true,
|
||||
"windows/arm64": true,
|
||||
}
|
||||
|
||||
ErrNoExternalBuilder = errors.New("no external builders are available")
|
||||
@@ -254,6 +255,8 @@ func nameOfOutputFormat(value clientpb.OutputFormat) string {
|
||||
return "Service"
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
return "Shared Library"
|
||||
case clientpb.OutputFormat_GO_ARCHIVE:
|
||||
return "Go Archive"
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
return "Shellcode"
|
||||
default:
|
||||
@@ -389,6 +392,9 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
configFormat = clientpb.OutputFormat_SHARED_LIB
|
||||
isSharedLib = true
|
||||
runAtLoad, _ = cmd.Flags().GetBool("run-at-load")
|
||||
case "archive":
|
||||
configFormat = clientpb.OutputFormat_GO_ARCHIVE
|
||||
isSharedLib = true
|
||||
case "shellcode":
|
||||
configFormat = clientpb.OutputFormat_SHELLCODE
|
||||
isShellcode = true
|
||||
@@ -483,6 +489,23 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
c2Profile = consts.DefaultC2Profile
|
||||
}
|
||||
|
||||
allProtocols, _ := cmd.Flags().GetBool("all-protocols")
|
||||
includeMTLS, _ := cmd.Flags().GetBool("include-mtls")
|
||||
includeHTTP, _ := cmd.Flags().GetBool("include-http")
|
||||
includeWG, _ := cmd.Flags().GetBool("include-wg")
|
||||
includeDNS, _ := cmd.Flags().GetBool("include-dns")
|
||||
includeNamedPipe, _ := cmd.Flags().GetBool("include-named-pipe")
|
||||
includeTCP, _ := cmd.Flags().GetBool("include-tcp-pivot")
|
||||
|
||||
if allProtocols {
|
||||
includeMTLS = true
|
||||
includeHTTP = true
|
||||
includeWG = true
|
||||
includeDNS = true
|
||||
includeNamedPipe = true
|
||||
includeTCP = true
|
||||
}
|
||||
|
||||
// exports if its a shared library
|
||||
|
||||
config := &clientpb.ImplantConfig{
|
||||
@@ -527,6 +550,13 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
||||
DebugFile: debugFile,
|
||||
HTTPC2ConfigName: c2Profile,
|
||||
ShellcodeConfig: shellcodeConfig,
|
||||
|
||||
IncludeMTLS: includeMTLS,
|
||||
IncludeHTTP: includeHTTP,
|
||||
IncludeWG: includeWG,
|
||||
IncludeDNS: includeDNS,
|
||||
IncludeNamePipe: includeNamedPipe,
|
||||
IncludeTCP: includeTCP,
|
||||
}
|
||||
|
||||
return name, config
|
||||
|
||||
@@ -197,7 +197,7 @@ func OSCompleter(con *console.SliverClient) carapace.Action {
|
||||
func FormatCompleter() carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
return carapace.ActionValues([]string{
|
||||
"exe", "shared", "service", "shellcode",
|
||||
"exe", "shared", "archive", "service", "shellcode",
|
||||
}...).Tag("implant format")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestNameOfOutputFormatGoArchive(t *testing.T) {
|
||||
if got := nameOfOutputFormat(clientpb.OutputFormat_GO_ARCHIVE); got != "Go Archive" {
|
||||
t.Fatalf("nameOfOutputFormat(GO_ARCHIVE) = %q, want %q", got, "Go Archive")
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package generate
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -81,9 +82,27 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
||||
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
|
||||
}
|
||||
}
|
||||
_, err := compile(implantName, profile.Config, nil, save, con)
|
||||
if err != nil {
|
||||
return
|
||||
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
||||
_, err := compile(implantName, profile.Config, nil, save, con)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := externalBuild(implantName, profile.Config, nil, save, con)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case ErrNoExternalBuilder:
|
||||
con.PrintErrorf("There are no external builders currently connected to the server\n")
|
||||
con.PrintErrorf("See 'builders' command for more information\n")
|
||||
case ErrNoValidBuilders:
|
||||
con.PrintErrorf("There are external builders connected to the server, but none can build the target you specified\n")
|
||||
con.PrintErrorf("Invalid target %s\n", fmt.Sprintf("%s:%s/%s", profile.Config.Format, profile.Config.GOOS, profile.Config.GOARCH))
|
||||
con.PrintErrorf("See 'builders' command for more information\n")
|
||||
default:
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
con.PrintErrorf("No profile with name '%s'", name)
|
||||
|
||||
@@ -263,6 +263,8 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
|
||||
properties["format"] = "Executable"
|
||||
case clientpb.OutputFormat_SHARED_LIB:
|
||||
properties["format"] = "Shared Library"
|
||||
case clientpb.OutputFormat_GO_ARCHIVE:
|
||||
properties["format"] = "Go Archive"
|
||||
case clientpb.OutputFormat_SERVICE:
|
||||
properties["format"] = "Service"
|
||||
case clientpb.OutputFormat_SHELLCODE:
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -33,12 +34,12 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/proto"
|
||||
uuid "uuid"
|
||||
)
|
||||
|
||||
// TrafficEncodersCmd - Generate traffic encoders command implementation.
|
||||
@@ -67,7 +68,7 @@ func DisplayTrafficEncoders(encoderMap *clientpb.TrafficEncoderMap, con *console
|
||||
})
|
||||
allIDs := []uint64{}
|
||||
for _, encoder := range encoderMap.Encoders {
|
||||
isDuplicate := util.Contains(allIDs, encoder.ID)
|
||||
isDuplicate := slices.Contains(allIDs, encoder.ID)
|
||||
allIDs = append(allIDs, encoder.ID)
|
||||
name := encoder.Wasm.Name
|
||||
if isDuplicate {
|
||||
@@ -94,7 +95,7 @@ func TrafficEncodersAddCmd(cmd *cobra.Command, con *console.SliverClient, args [
|
||||
}
|
||||
|
||||
skipTests, _ := cmd.Flags().GetBool("skip-tests")
|
||||
testID := uuid.Must(uuid.NewV4()).String()
|
||||
testID := uuid.NewV4().String()
|
||||
trafficEncoder := &clientpb.TrafficEncoder{
|
||||
Wasm: &commonpb.File{
|
||||
Name: filepath.Base(args[0]),
|
||||
|
||||
@@ -56,6 +56,10 @@ var (
|
||||
consts.LsStr: lsHelp,
|
||||
consts.CdStr: cdHelp,
|
||||
consts.PwdStr: pwdHelp,
|
||||
consts.AIStr: aiHelp,
|
||||
consts.AIConfigStr: aiConfigHelp,
|
||||
consts.DocsStr: docsHelp,
|
||||
consts.OpforStr: opforHelp,
|
||||
consts.CatStr: catHelp,
|
||||
consts.EditStr: editHelp,
|
||||
consts.HexEditStr: hexEditHelp,
|
||||
@@ -167,6 +171,55 @@ _=/usr/bin/env
|
||||
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
|
||||
[[.Bold]]About:[[.Normal]] Get information about a Sliver by name, or for the active Sliver.`
|
||||
|
||||
aiHelp = `[[.Bold]]Command:[[.Normal]] ai
|
||||
[[.Bold]]About:[[.Normal]] Launch the Sliver AI Bubble Tea TUI. The AI workspace now loads server-side conversation threads over gRPC, keeps them synced through the shared event stream, and renders conversation transcripts as markdown in the main pane.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
use my-session
|
||||
ai
|
||||
`
|
||||
|
||||
opforHelp = `[[.Bold]]Command:[[.Normal]] opfor [script.cna|subcommand]
|
||||
|
||||
[[.Bold]]About:[[.Normal]] Check, run, or persistently load OPFOR-compatible CNA scripts in the Sliver client. Loaded Beacon aliases are exposed beneath the opfor namespace and use the active session or beacon. This integration intentionally focuses on BOF-oriented scripts and a small set of terminal UI and session APIs; it is not a complete Aggressor Script client implementation.
|
||||
|
||||
[[.Bold]]Modes:[[.Normal]]
|
||||
check compiles a CNA without executing any top-level code.
|
||||
run executes top-level code once with trailing arguments in @ARGV, then discards aliases, hooks, and callbacks.
|
||||
load executes top-level initialization and retains aliases, hooks, and callbacks for the lifetime of this client process. A bare CNA path is shorthand for load.
|
||||
help displays beacon_command_register metadata for a loaded alias without invoking it.
|
||||
|
||||
Most BOF CNAs, including FirefoxDump, define an alias at top level. Running such a file only creates and immediately retires that alias; load it, select a target, and invoke the alias to execute its BOF.
|
||||
|
||||
The default operation timeout is 600 seconds. Put --timeout before the alias name so the host consumes it rather than passing it to the CNA command.
|
||||
|
||||
[[.Bold]]Examples:[[.Normal]]
|
||||
opfor check ./script.cna
|
||||
opfor run ./script.cna argument
|
||||
opfor load ./firefoxdump.cna
|
||||
opfor help firefoxdump
|
||||
use <session-or-beacon>
|
||||
opfor firefoxdump /all
|
||||
opfor --timeout 900 firefoxdump /all
|
||||
opfor list
|
||||
opfor unload firefoxdump.cna
|
||||
`
|
||||
|
||||
aiConfigHelp = `[[.Bold]]Command:[[.Normal]] ai-config
|
||||
[[.Bold]]About:[[.Normal]] Open a server-side form to update the AI block in server.yaml. The form stores the default provider, model, thinking level, and provider-specific credentials such as the API key and optional base URL.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
ai-config
|
||||
`
|
||||
|
||||
docsHelp = `[[.Bold]]Command:[[.Normal]] docs
|
||||
[[.Bold]]About:[[.Normal]] Browse the embedded Sliver documentation in a Bubble Tea TUI. The left pane is a searchable docs browser, and the right pane renders markdown with Glow styling.
|
||||
[[.Bold]]Keys:[[.Normal]] Use / to filter docs, enter to focus the selected document, tab to switch panes, and q to quit.
|
||||
|
||||
[[.Bold]]Example:[[.Normal]]
|
||||
docs
|
||||
`
|
||||
|
||||
useHelp = `[[.Bold]]Command:[[.Normal]] use [sliver name/session]
|
||||
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
|
||||
|
||||
|
||||
@@ -540,10 +540,7 @@ func (m *editorModel) ensureCursorVisible() {
|
||||
if m.top < 0 {
|
||||
m.top = 0
|
||||
}
|
||||
maxTop := m.totalLines() - height
|
||||
if maxTop < 0 {
|
||||
maxTop = 0
|
||||
}
|
||||
maxTop := max(m.totalLines()-height, 0)
|
||||
if m.top > maxTop {
|
||||
m.top = maxTop
|
||||
}
|
||||
@@ -725,8 +722,8 @@ func baseName(path string) string {
|
||||
return ""
|
||||
}
|
||||
clean := strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(clean, "/"); idx != -1 {
|
||||
return clean[idx+1:]
|
||||
if _, after, ok := strings.CutLast(clean, "/"); ok {
|
||||
return after
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -56,10 +56,7 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchSize := maxSize + 1
|
||||
if fetchSize < maxSize {
|
||||
fetchSize = maxSize
|
||||
}
|
||||
fetchSize := max(maxSize+1, maxSize)
|
||||
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
Loaded 100 of 3684 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user