mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
1158
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13935b2e68 | ||
|
|
1db7b99685 | ||
|
|
10ec67d274 | ||
|
|
52f5517c2a | ||
|
|
c4ed103182 | ||
|
|
c04b0884a5 | ||
|
|
893fe4ccda | ||
|
|
b43ef0fabe | ||
|
|
25d2bffb5a | ||
|
|
534763f087 | ||
|
|
ec733974de | ||
|
|
d9811baa7e | ||
|
|
d8b360ecea | ||
|
|
12c55be611 | ||
|
|
3a147f1e01 | ||
|
|
5b03320a32 | ||
|
|
1cc6d35dc6 | ||
|
|
1c5d8ab5a9 | ||
|
|
04a71e6d7d | ||
|
|
bb8b90be56 | ||
|
|
7a2b58ade8 | ||
|
|
854143b593 | ||
|
|
d3068394bf | ||
|
|
d80db8a932 | ||
|
|
179ff728dd | ||
|
|
422044af0d | ||
|
|
7dbdd1931c | ||
|
|
8b7e1b63ab | ||
|
|
ed5c89253c | ||
|
|
06b5a56e74 | ||
|
|
704912d25a | ||
|
|
ea1fe95408 | ||
|
|
8fe54f243e | ||
|
|
4fa6fe86fe | ||
|
|
23c5a0b164 | ||
|
|
7b8dafcf55 | ||
|
|
4e6531fa98 | ||
|
|
6e9b2a9820 | ||
|
|
b11f85d4d7 | ||
|
|
23f1341684 | ||
|
|
e68e163456 | ||
|
|
f8430cecf7 | ||
|
|
276a30fbaa | ||
|
|
3aa8a9f93c | ||
|
|
fc3fb7386a | ||
|
|
6edea618e9 | ||
|
|
e920f2f8ea | ||
|
|
5b4a03c9c3 | ||
|
|
e93fabecd6 | ||
|
|
b2cae511e6 | ||
|
|
2e8e53789f | ||
|
|
6a08fb2f7c | ||
|
|
76d38d4677 | ||
|
|
3f87964397 | ||
|
|
bb19ba0665 | ||
|
|
262851373d | ||
|
|
4afcd5bc9d | ||
|
|
c746a5262c | ||
|
|
e91cdf117f | ||
|
|
1fb671f71b | ||
|
|
40df2701d1 | ||
|
|
c43347886b | ||
|
|
4ef33f389c | ||
|
|
acd52520e4 | ||
|
|
9a373e20f0 | ||
|
|
aad186a632 | ||
|
|
ec9d9b2cc1 | ||
|
|
05d3b2604b | ||
|
|
c3e8a97f66 | ||
|
|
547a56696d | ||
|
|
84bb2397c7 | ||
|
|
503d040885 | ||
|
|
801452eeff | ||
|
|
f6c7d73014 | ||
|
|
228757f656 | ||
|
|
738010e990 | ||
|
|
9dcf9b2d25 | ||
|
|
32ab020dbd | ||
|
|
6fcaf970b1 | ||
|
|
47c83bbaa0 | ||
|
|
7d76febbc9 | ||
|
|
f4c182427f | ||
|
|
836e33ade7 | ||
|
|
0aa7e5bf96 | ||
|
|
b46df4b05e | ||
|
|
cbc9200649 | ||
|
|
a0bf009c06 | ||
|
|
46f516fa3a | ||
|
|
1f82c38e11 | ||
|
|
6ac84ffc11 | ||
|
|
2638b5f384 | ||
|
|
cf19741b9d | ||
|
|
b2c0979b63 | ||
|
|
ea5e1412b2 | ||
|
|
bd948b8ab1 | ||
|
|
937a74d597 | ||
|
|
e747187dda | ||
|
|
ec992dea76 | ||
|
|
544c872881 | ||
|
|
4701b895b7 | ||
|
|
18c56ec3c5 | ||
|
|
4b12c80534 | ||
|
|
a8c6d5e0a3 | ||
|
|
67c6d0d356 | ||
|
|
5e3b432b9b | ||
|
|
fc4d9a1c36 | ||
|
|
55b97df9f7 | ||
|
|
227328fa87 | ||
|
|
071106d0bf | ||
|
|
8c326e6af9 | ||
|
|
5e9ba47bf4 | ||
|
|
0537e285fc | ||
|
|
ca685f5eed | ||
|
|
902999493b | ||
|
|
14363c5d2a | ||
|
|
926b7ae1a7 | ||
|
|
bf815b36ed | ||
|
|
637853e8c2 | ||
|
|
0c022ec722 | ||
|
|
7c83b458fa | ||
|
|
f447a170c6 | ||
|
|
d565592773 | ||
|
|
c21da4dceb | ||
|
|
b08e2ff5c0 | ||
|
|
09b0540e75 | ||
|
|
b4005efda8 | ||
|
|
6d086fd7a7 | ||
|
|
ccebd505b0 | ||
|
|
94c5022dd0 | ||
|
|
b19e383111 | ||
|
|
02ebd99c68 | ||
|
|
83d82e7161 | ||
|
|
c481a4f5db | ||
|
|
eb5ac1b309 | ||
|
|
e73618e0a5 | ||
|
|
0017fd2fe8 | ||
|
|
e44111b6b4 | ||
|
|
aa2dce510a | ||
|
|
5e6acde3c7 | ||
|
|
99a8d3f859 | ||
|
|
fc15a47b1e | ||
|
|
8b32b6525f | ||
|
|
9e0fafb785 | ||
|
|
12ebffb16c | ||
|
|
b539762de7 | ||
|
|
e71af809d4 | ||
|
|
f3ffa3323b | ||
|
|
778331d474 | ||
|
|
8d259577af | ||
|
|
aa108a66a8 | ||
|
|
44e5464950 | ||
|
|
1381200b33 | ||
|
|
f81b250a04 | ||
|
|
8b4ee5f5c5 | ||
|
|
68cb1363e8 | ||
|
|
2344a3ae50 | ||
|
|
8cec5164b7 | ||
|
|
be50c25cb2 | ||
|
|
ee2cc83aa1 | ||
|
|
8e62e2dcd9 | ||
|
|
1347059639 | ||
|
|
29efea561c | ||
|
|
82deca2b0f | ||
|
|
808b3b6408 | ||
|
|
eba10ebbfc | ||
|
|
13903e46a8 | ||
|
|
8aaaa583bd | ||
|
|
4b428065e2 | ||
|
|
578f9a28d0 | ||
|
|
ce2f494f96 | ||
|
|
c1c36491a6 | ||
|
|
a5c3482c6d | ||
|
|
b428274515 | ||
|
|
5dc6adae47 | ||
|
|
264b00f3c7 | ||
|
|
ada79c8ecd | ||
|
|
edd4c3d097 | ||
|
|
38f84a4f6f | ||
|
|
aee0958e99 | ||
|
|
23c067982a | ||
|
|
7e917da3ac | ||
|
|
674b49f41f | ||
|
|
4ba04df621 | ||
|
|
3274382bc7 | ||
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
db45c4aaf9 | ||
|
|
198913d6f8 | ||
|
|
cd85824185 | ||
|
|
63797ac79c | ||
|
|
4633409623 | ||
|
|
13c2d33449 | ||
|
|
8cf478d7b0 | ||
|
|
f55215ff93 | ||
|
|
b5d3d0bad6 | ||
|
|
a32d81252c | ||
|
|
a1537535c4 | ||
|
|
61dd34e989 | ||
|
|
2b67cb917b | ||
|
|
ceead92ec5 | ||
|
|
df9ed94635 | ||
|
|
e891613f6f | ||
|
|
528b04427a | ||
|
|
f60a0e91d3 | ||
|
|
2308314529 | ||
|
|
04bc82bca3 | ||
|
|
acdfa33231 | ||
|
|
a03592de81 | ||
|
|
5cf9d088e8 | ||
|
|
db3294b926 | ||
|
|
1847b8c24b | ||
|
|
79e4259081 | ||
|
|
9ff9b55352 | ||
|
|
9767e24edd | ||
|
|
bb572f3846 | ||
|
|
01c5b5cee4 | ||
|
|
1049dc4a98 | ||
|
|
d1b0ba13c4 | ||
|
|
2d98f14149 | ||
|
|
19e82064b1 | ||
|
|
3b72374685 | ||
|
|
20fb8c4975 | ||
|
|
68829c0a40 | ||
|
|
2c18062f4d | ||
|
|
f17ce0afbe | ||
|
|
116b12885c | ||
|
|
3d9860db6c | ||
|
|
1a6579e954 | ||
|
|
2659d848d7 | ||
|
|
df6571f63f | ||
|
|
aab1470e08 | ||
|
|
19d43fdc37 | ||
|
|
3b09e5ea80 | ||
|
|
b904d29b25 | ||
|
|
8e2b069a7d | ||
|
|
096c418d27 | ||
|
|
d9e241ccb5 | ||
|
|
086da95525 | ||
|
|
27ed557e55 | ||
|
|
af7c823508 | ||
|
|
f2a3d8ee6d | ||
|
|
de7215d2c0 | ||
|
|
2d555b16d9 | ||
|
|
597a2571fc | ||
|
|
409b4c8fdb | ||
|
|
2e72bc96b9 | ||
|
|
fa9557348e | ||
|
|
4768e36074 | ||
|
|
e53f66de72 | ||
|
|
1d33b89ec5 | ||
|
|
7b0f420290 | ||
|
|
00824d1b83 | ||
|
|
99ad05b7b7 | ||
|
|
80273462bd | ||
|
|
2cdefc7369 | ||
|
|
80c752a422 | ||
|
|
8a8a9dc498 | ||
|
|
5f8540b3c4 | ||
|
|
4eef868047 | ||
|
|
5ebfecd822 | ||
|
|
9762560228 | ||
|
|
6310c7d6a0 | ||
|
|
fafe360672 | ||
|
|
d9b6f41940 | ||
|
|
f1ccf0eac3 | ||
|
|
d2aa924bc4 | ||
|
|
46861cd428 | ||
|
|
1c1825f8f0 | ||
|
|
a943995040 | ||
|
|
f0d2a2cf24 | ||
|
|
27ac612c82 | ||
|
|
baedb563f3 | ||
|
|
45c7914602 | ||
|
|
92d85cb8ff | ||
|
|
f5e831844a | ||
|
|
ab2fc7dc4a | ||
|
|
5e377019e4 | ||
|
|
88a4bebca0 | ||
|
|
2daccb5bd3 | ||
|
|
e2a009d4ac | ||
|
|
eda51f826a | ||
|
|
f83dc42a64 | ||
|
|
17e8c996a6 | ||
|
|
2b667f0e7e | ||
|
|
73bcf40bfd | ||
|
|
b635adcb3c | ||
|
|
2b92397e07 | ||
|
|
c23067a0e0 | ||
|
|
3aefc39c08 | ||
|
|
abd8c83df8 | ||
|
|
6cce9426f2 | ||
|
|
2e742863a1 | ||
|
|
dc13a5bb93 | ||
|
|
1f2946eb87 | ||
|
|
5eeda4c02b | ||
|
|
55a3386313 | ||
|
|
b34589a002 | ||
|
|
4486b17b49 | ||
|
|
377ee5ca7c | ||
|
|
4765f40854 | ||
|
|
e239d85821 | ||
|
|
dec419b284 | ||
|
|
6d51a23cc9 | ||
|
|
3d6eec47be | ||
|
|
464fb7008c | ||
|
|
ffa5f89f5f | ||
|
|
f19cbdc957 | ||
|
|
83bfc8d8e6 | ||
|
|
365bb3596e | ||
|
|
9ee96c2ce6 | ||
|
|
e776e266d0 | ||
|
|
d080c8a646 | ||
|
|
cb6402245e | ||
|
|
e4357277ec | ||
|
|
5dc2ab1490 | ||
|
|
9f034a9956 | ||
|
|
f700535997 | ||
|
|
31bca034d8 | ||
|
|
fac4ad5fb7 | ||
|
|
c128d33514 | ||
|
|
4a37262bcf | ||
|
|
a2295112bd | ||
|
|
834dff0f6e | ||
|
|
f4a0431fdd | ||
|
|
d072b328c0 | ||
|
|
435bbcbd7e | ||
|
|
84dc28e584 | ||
|
|
639592cb02 | ||
|
|
8c7a1a6b40 | ||
|
|
06274c4061 | ||
|
|
593925b4a5 | ||
|
|
255477e572 | ||
|
|
0acc6911d8 | ||
|
|
4ca026906b | ||
|
|
97a249e28e | ||
|
|
9f331687d3 | ||
|
|
14d68d6fcd | ||
|
|
05c5334fea | ||
|
|
d277a14c57 | ||
|
|
a7b5dbe6ee | ||
|
|
c188dc1e69 | ||
|
|
9fc9d1762e | ||
|
|
4ef86448d0 | ||
|
|
02117a6072 | ||
|
|
c272c8abac | ||
|
|
cdf2412b39 | ||
|
|
40f52791d3 | ||
|
|
a5d8bd16b9 | ||
|
|
bf113f2151 | ||
|
|
4cd010afb9 | ||
|
|
04b0bdd199 | ||
|
|
9682db93b3 | ||
|
|
1d7ffb4ac2 | ||
|
|
c5e7cd6354 | ||
|
|
b20ba7284b | ||
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 | ||
|
|
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 | ||
|
|
87b1efaa5f | ||
|
|
009e505a54 | ||
|
|
c0dd2d5b51 | ||
|
|
6dab9dc6c5 | ||
|
|
854249fa25 | ||
|
|
7db573a03e | ||
|
|
0bef5491dc | ||
|
|
1c50e5a87b | ||
|
|
8dae2d0d52 | ||
|
|
023776f7e3 | ||
|
|
b7566a3b09 | ||
|
|
b889e2eb6f | ||
|
|
4466ace279 | ||
|
|
2b65089b27 | ||
|
|
87d5fd53a8 | ||
|
|
cf79a2d284 | ||
|
|
6d0276ff67 | ||
|
|
9b001ab88e | ||
|
|
503218f3f4 | ||
|
|
495dce0987 | ||
|
|
a7ff35c229 | ||
|
|
162fc4fb8a | ||
|
|
636ea0a9b2 | ||
|
|
84ed9bc4e7 | ||
|
|
b985d8ff9f | ||
|
|
5a43705e1d | ||
|
|
76af248850 | ||
|
|
8ea7eb4236 | ||
|
|
8f3c3ec1e0 | ||
|
|
8d4bf9c759 | ||
|
|
628757250e | ||
|
|
85094fc695 | ||
|
|
0a88ad77f7 | ||
|
|
4d04f08421 | ||
|
|
4a01971f7d | ||
|
|
977c63dc16 | ||
|
|
2d65d7d6a5 | ||
|
|
4f5ca51cfe | ||
|
|
83208eeab4 | ||
|
|
21674a9f0a | ||
|
|
a18bda14e2 | ||
|
|
37f9670783 | ||
|
|
1745886622 | ||
|
|
bd7361825c | ||
|
|
32af81c132 | ||
|
|
818127349c | ||
|
|
a712bead88 | ||
|
|
aa86a1d12c | ||
|
|
afb58f7155 | ||
|
|
97d5adccd7 | ||
|
|
83cc755388 | ||
|
|
4dc1fe145c | ||
|
|
5e27b23bde | ||
|
|
6011d23a9f | ||
|
|
976d720043 | ||
|
|
86f41e9abc | ||
|
|
8b9fc399fa | ||
|
|
27c75c14e6 | ||
|
|
11d6af8aa0 | ||
|
|
54dc6dcec3 | ||
|
|
c7412d4d5b | ||
|
|
c3817dc175 | ||
|
|
09cedf588e | ||
|
|
66aed89ebd | ||
|
|
16397e7a0f | ||
|
|
5062b483b7 | ||
|
|
b268295c8d | ||
|
|
2b3f23cec9 | ||
|
|
25d242230b | ||
|
|
110658ec39 | ||
|
|
b4345226c1 | ||
|
|
e99999ebb6 | ||
|
|
24795e7cba | ||
|
|
32e3386a3e | ||
|
|
7feea8bef3 | ||
|
|
d182434d54 | ||
|
|
0237af0c6e | ||
|
|
1fa36e46c4 | ||
|
|
4d9bae0778 | ||
|
|
9a9b40e400 | ||
|
|
d784b305ef | ||
|
|
7a224b4757 | ||
|
|
76ae704ce4 | ||
|
|
71cd8fa610 | ||
|
|
e9fdcc1285 | ||
|
|
870c995ae2 | ||
|
|
15d7e7c4d1 | ||
|
|
750cdfce0a | ||
|
|
0f128a22b3 | ||
|
|
3296a8f483 | ||
|
|
b77ade2bb9 | ||
|
|
41dc54d736 | ||
|
|
8a0694cb38 | ||
|
|
52c0618bc3 | ||
|
|
e2e1912628 | ||
|
|
36794f1b07 | ||
|
|
ccd8f75b03 | ||
|
|
dc89afe404 | ||
|
|
9db1f3a967 | ||
|
|
a2ab3aede6 | ||
|
|
c9f4970e4b | ||
|
|
e72fa3dbd0 | ||
|
|
2e93ce5684 | ||
|
|
545fc27d03 | ||
|
|
5bf65aa2b0 | ||
|
|
ebaf07ff1f | ||
|
|
fc95d3e354 | ||
|
|
88dca9337b | ||
|
|
44d9f8d5aa | ||
|
|
f76e9e367d | ||
|
|
69fa4143f6 | ||
|
|
dcbda0c64e | ||
|
|
3db48408af | ||
|
|
828838cddf | ||
|
|
16c1dc21fd | ||
|
|
3c8edb7294 | ||
|
|
2e2206e0ae | ||
|
|
a43648a24a | ||
|
|
f65f98bf65 | ||
|
|
26d14e15ad | ||
|
|
cee2cc76c4 | ||
|
|
daa33b6301 | ||
|
|
ef52973536 | ||
|
|
6a7bd0803a | ||
|
|
6c70c1c1e6 | ||
|
|
ddc222b8a2 | ||
|
|
5506a582ac | ||
|
|
63d872c98b | ||
|
|
ef9f5dde0d | ||
|
|
e35b7fdb97 | ||
|
|
efbbf382eb | ||
|
|
0b8d00900d | ||
|
|
9077596f25 | ||
|
|
0a8104a32e | ||
|
|
15a0ba4c1e | ||
|
|
d452c8a676 | ||
|
|
fb74b491fa | ||
|
|
2af6e11adc | ||
|
|
33c656d082 | ||
|
|
99a44e340f | ||
|
|
de5fa3458b | ||
|
|
a3a98858fd | ||
|
|
d0652f2fd3 | ||
|
|
936f067363 | ||
|
|
041c3c7048 | ||
|
|
b48f74af58 | ||
|
|
6ec8849b20 | ||
|
|
db7cf1ed62 | ||
|
|
16e1e77068 | ||
|
|
1cfa694aa2 | ||
|
|
5898877136 | ||
|
|
a7d8cac565 | ||
|
|
0e73afca47 | ||
|
|
432941d694 | ||
|
|
ac521bf53c | ||
|
|
9720c9f14f | ||
|
|
4a224fdb9c | ||
|
|
671eb4d777 | ||
|
|
16cf816610 | ||
|
|
879f576c6e | ||
|
|
2df78cdbf5 | ||
|
|
a05ffc0ae1 | ||
|
|
6b4b218f09 | ||
|
|
4d0d46afd6 | ||
|
|
f24c057503 | ||
|
|
7e3c1f1549 | ||
|
|
1d4c0ec8a5 | ||
|
|
92dda6aa4f | ||
|
|
9a605dfda6 | ||
|
|
e918885556 | ||
|
|
263702594f | ||
|
|
72b8dfb454 | ||
|
|
aa2383ac24 | ||
|
|
b6f0ea013d | ||
|
|
eb82e18a52 | ||
|
|
40ea51b685 | ||
|
|
1285b44c50 | ||
|
|
cb63a3378f | ||
|
|
4f90bfdcda | ||
|
|
5bb1dcc137 | ||
|
|
57c417c48f | ||
|
|
55fc59f6f5 | ||
|
|
332852d095 | ||
|
|
a903d2f596 | ||
|
|
4a4bb683e3 | ||
|
|
016299c756 | ||
|
|
075caaccda | ||
|
|
729be253d3 | ||
|
|
13bd48182f | ||
|
|
67188dabea | ||
|
|
6e4c38c10e | ||
|
|
00ecb1f4fd | ||
|
|
bac047fedd | ||
|
|
10c16be351 | ||
|
|
d51ea9dbab | ||
|
|
0eb41094de | ||
|
|
a219a97cb4 | ||
|
|
b617bb4cf4 | ||
|
|
613b53430b | ||
|
|
472de54757 | ||
|
|
bdbc3bd7b5 | ||
|
|
87db61bcfb | ||
|
|
095eb2722b | ||
|
|
abbb0da434 | ||
|
|
52f7484a71 | ||
|
|
a79b5fd7b9 | ||
|
|
3e98d2d7a1 | ||
|
|
13bf9d0a01 | ||
|
|
7aae725b88 | ||
|
|
afd01073e4 | ||
|
|
ae0c33f41b | ||
|
|
18da00b449 | ||
|
|
dc736f6095 | ||
|
|
e02d53ce67 | ||
|
|
064cd6a2cc | ||
|
|
eab9d0b653 | ||
|
|
29c4ec0afa | ||
|
|
b7c336cb76 | ||
|
|
d8ddd09c86 | ||
|
|
04bb356207 | ||
|
|
a003a5e3cf | ||
|
|
75e3cad76a | ||
|
|
8aa0cbee7e | ||
|
|
30a50717e1 | ||
|
|
f4c9e7109b | ||
|
|
08f3997756 | ||
|
|
3aebebecbb | ||
|
|
1d7eca8e39 | ||
|
|
773ee5f867 | ||
|
|
7608a4b0a0 | ||
|
|
9ecd36b7b6 | ||
|
|
d8fc5a6df4 | ||
|
|
649a6582e0 | ||
|
|
00e8a0cfca | ||
|
|
e9890bc8f9 | ||
|
|
8b798f8307 | ||
|
|
d4728af1ea | ||
|
|
d1b7d91b1a | ||
|
|
cac6dced34 | ||
|
|
a35bf0c1fc | ||
|
|
67616e50c1 | ||
|
|
54de8a2a97 | ||
|
|
20916c655c | ||
|
|
6c18201ed6 | ||
|
|
29165aff0d | ||
|
|
704285f28d | ||
|
|
a007872fbc | ||
|
|
c90e674f25 | ||
|
|
679b8d0c51 | ||
|
|
0a42a03052 | ||
|
|
73852e5bf2 | ||
|
|
5bcf59e1b3 | ||
|
|
935c737c35 | ||
|
|
5c04e61235 | ||
|
|
cff7a8ce53 | ||
|
|
e6f452ea2e | ||
|
|
c5f533bc80 | ||
|
|
bc08760320 | ||
|
|
9b37bee391 | ||
|
|
709085238d | ||
|
|
6e4240f0d9 | ||
|
|
c95e5c6cd7 | ||
|
|
ed3ab31741 | ||
|
|
9372b768be | ||
|
|
2d7406f415 | ||
|
|
85ba0cb587 | ||
|
|
251871cca1 | ||
|
|
604318ea54 | ||
|
|
bc72b20f9b | ||
|
|
61022e1eb1 | ||
|
|
a8d59daded | ||
|
|
9195376993 | ||
|
|
a6de8e0304 | ||
|
|
d3ebd34b0d | ||
|
|
310619526d | ||
|
|
1990777c4b | ||
|
|
9570556a87 | ||
|
|
b563919c19 | ||
|
|
f1858ee8f3 | ||
|
|
46a8313a33 | ||
|
|
22454f3385 | ||
|
|
868b2c294c | ||
|
|
9827b4df4b | ||
|
|
a48309a813 | ||
|
|
5df5dec5d4 | ||
|
|
a0fb2a229f | ||
|
|
3ff3ef2ed4 | ||
|
|
73cc8c38ff | ||
|
|
896c763166 | ||
|
|
ec7e2facbc | ||
|
|
4969c2dc51 | ||
|
|
94cb60b1f7 | ||
|
|
33fcfd54ef | ||
|
|
10bb248e50 | ||
|
|
e023fd507e | ||
|
|
1a9f0f38dd | ||
|
|
02831a80bf | ||
|
|
9ccff1798e | ||
|
|
cd5ef8b5c6 | ||
|
|
dda1fdbbc2 | ||
|
|
3b49df5662 | ||
|
|
32be71ff8e | ||
|
|
3f77da1524 | ||
|
|
bae3e9e45d | ||
|
|
205f5b4196 | ||
|
|
12f12218e0 | ||
|
|
fae7e211a7 | ||
|
|
346da14431 | ||
|
|
15174f6c31 | ||
|
|
f29df040bb | ||
|
|
0bbecc16ec | ||
|
|
0c216161c2 | ||
|
|
8ad9c62eb5 | ||
|
|
4c99fbfd82 | ||
|
|
ed713e199c | ||
|
|
aacdb71c4a | ||
|
|
6968c3aae5 | ||
|
|
410fd93025 | ||
|
|
f2a0435dd2 | ||
|
|
c09c57da3f | ||
|
|
d66ec956a4 | ||
|
|
8229722735 | ||
|
|
dcd81a4db7 | ||
|
|
2617387417 | ||
|
|
56aee7d6e4 | ||
|
|
b4f07e8629 | ||
|
|
280cb34049 | ||
|
|
a21b0da928 | ||
|
|
8a0ba038f6 | ||
|
|
8e5c5f1450 | ||
|
|
b8f4f0c1ea | ||
|
|
17cb7fbd50 | ||
|
|
9963bc6a3f | ||
|
|
274943f383 | ||
|
|
be81755056 | ||
|
|
a5861c43f2 | ||
|
|
e3025a2da2 | ||
|
|
dd072bbdb6 | ||
|
|
1e3c736acf | ||
|
|
3c46cf2210 | ||
|
|
40cc1f4eeb | ||
|
|
3d880914d9 | ||
|
|
1162de9ee3 | ||
|
|
b117a1325d | ||
|
|
a809989536 | ||
|
|
a9ee3ca9ed | ||
|
|
56d972dd32 | ||
|
|
b4091264d7 | ||
|
|
23bf325a66 | ||
|
|
d9344a4c79 | ||
|
|
67d9a772e3 | ||
|
|
dae284e62e | ||
|
|
60f48bdc51 | ||
|
|
0d7885de29 | ||
|
|
d41d29fc1e | ||
|
|
3f400bd3f0 | ||
|
|
d43f664821 | ||
|
|
85c57c07da | ||
|
|
60d15447e0 | ||
|
|
6351b2d671 | ||
|
|
c7535ec509 | ||
|
|
67e17d073b | ||
|
|
c5cb4dcb6c | ||
|
|
0b9fe603a8 | ||
|
|
c7141892d1 | ||
|
|
c5556f9e2b | ||
|
|
1bc45c2ab2 | ||
|
|
739ad98b81 | ||
|
|
95c1697d1b | ||
|
|
f7b9571cfc | ||
|
|
0d08f8c62f | ||
|
|
aa9c8d89e9 | ||
|
|
152e60ba29 | ||
|
|
d13a71b808 | ||
|
|
b17e2f056f | ||
|
|
42fb1e835e | ||
|
|
521356ff6e | ||
|
|
74b5d720b2 | ||
|
|
cd75545323 | ||
|
|
396dc9724f | ||
|
|
33398146d3 | ||
|
|
21039554e5 | ||
|
|
cca73be1c8 | ||
|
|
596bc455d8 | ||
|
|
400c36c5df | ||
|
|
c94dde3efb | ||
|
|
827a9ca156 | ||
|
|
d066dbc49a | ||
|
|
f1fbc345a2 | ||
|
|
2889c1ee29 | ||
|
|
28ee216b36 | ||
|
|
2dd3e84f60 | ||
|
|
067cdf712d | ||
|
|
22ae941e17 | ||
|
|
7c0c2c2dda | ||
|
|
39de742223 | ||
|
|
b07e750cf5 | ||
|
|
8786979001 | ||
|
|
aa223e4488 | ||
|
|
8785c55cf3 | ||
|
|
38fa64d266 | ||
|
|
b04fff21fd | ||
|
|
3f656da786 | ||
|
|
55311868a7 | ||
|
|
422a1d39c3 | ||
|
|
6938759e9d | ||
|
|
5febe5e539 | ||
|
|
38479e34aa | ||
|
|
abe8f8e77b | ||
|
|
c0f3161525 | ||
|
|
c6bbba16e1 | ||
|
|
af4f99de51 | ||
|
|
8e6e8e18ad | ||
|
|
544e577eb2 | ||
|
|
9939d2c320 | ||
|
|
f47af1e94e | ||
|
|
d3302583f5 | ||
|
|
d9403c7f05 | ||
|
|
65eb72506b | ||
|
|
d86cdcad8e | ||
|
|
7d7f858c15 | ||
|
|
ee1ad9edd1 | ||
|
|
d985564198 | ||
|
|
c1b5a3f7b4 | ||
|
|
927ff9c4ab | ||
|
|
3411b438b6 | ||
|
|
40bc0d6464 | ||
|
|
b0639275f4 | ||
|
|
6216cd1dc6 | ||
|
|
78e6b3a9b3 | ||
|
|
509d1b46ff | ||
|
|
f06b2c2192 | ||
|
|
4138ddd84a | ||
|
|
e9a300bab2 | ||
|
|
6edd10a010 | ||
|
|
b438df4db9 | ||
|
|
49eab476bb | ||
|
|
8976654010 | ||
|
|
a78b8c2097 | ||
|
|
5709bb9a24 | ||
|
|
25668a6d82 | ||
|
|
87dfe96ee9 | ||
|
|
5bb4338d76 | ||
|
|
bb35fa56d6 | ||
|
|
fbab18b2fe | ||
|
|
3ae8353030 | ||
|
|
a3d053bde5 | ||
|
|
7e27a18245 | ||
|
|
6d09b27c85 | ||
|
|
c389bac46e | ||
|
|
fae8e77665 | ||
|
|
38cf0aeb8b | ||
|
|
a755750a89 | ||
|
|
555ee5c63d | ||
|
|
0fc51677c1 | ||
|
|
cc1c7a5364 | ||
|
|
d1fa99d6eb | ||
|
|
6697801f59 | ||
|
|
32fa69356c | ||
|
|
c42d754e72 | ||
|
|
e4babaf471 | ||
|
|
da196a789d | ||
|
|
6a656d63ab | ||
|
|
b0e226199a | ||
|
|
7cce6b17b2 | ||
|
|
d07aae4d0b | ||
|
|
f4a2ba536c | ||
|
|
2bbd69719d | ||
|
|
cf4ff14342 | ||
|
|
ae3a2fd51e | ||
|
|
d14ba3d35f | ||
|
|
10ef8b3e88 | ||
|
|
4bc65e2193 | ||
|
|
a45a5795d3 | ||
|
|
e1c5b77db4 | ||
|
|
280d08d074 | ||
|
|
1d55b34b62 | ||
|
|
d32302a1ab | ||
|
|
2eabfc255b | ||
|
|
d0375da6e4 | ||
|
|
4523c83b89 | ||
|
|
1decc7f462 | ||
|
|
f97a1e3fdf | ||
|
|
1e12ea3242 | ||
|
|
8919bc3b07 | ||
|
|
1a2b392d20 | ||
|
|
11a6584973 | ||
|
|
1e06fe6612 | ||
|
|
65ee18026e | ||
|
|
9c1617c213 | ||
|
|
d74067ddff | ||
|
|
60772c7659 | ||
|
|
25a70e6e4f | ||
|
|
a89675d961 | ||
|
|
0f340a25cf | ||
|
|
9cdc326b57 | ||
|
|
3faabd36c1 | ||
|
|
113ef5d7d0 | ||
|
|
9fa1e15425 | ||
|
|
2d9a46543d | ||
|
|
e5e589e1b0 | ||
|
|
2e0fab3a71 | ||
|
|
2f486be19c | ||
|
|
85868e5b8f | ||
|
|
31c70938bd | ||
|
|
99571dc73e | ||
|
|
5c2848c84c | ||
|
|
ffcfa8d1b5 | ||
|
|
debc03203c | ||
|
|
63668f57b5 | ||
|
|
aefe6b31e9 | ||
|
|
f854af1134 | ||
|
|
a9ef42a416 | ||
|
|
67c6140e6a | ||
|
|
b84037af11 | ||
|
|
61ed176683 | ||
|
|
12e44644ed | ||
|
|
59352177eb | ||
|
|
0b185cc885 | ||
|
|
ae0c106595 | ||
|
|
6d2b0f8319 | ||
|
|
60443f4fc7 | ||
|
|
418fe757d1 | ||
|
|
c9a624831a | ||
|
|
ed64f2e0d2 | ||
|
|
5f18609ed7 | ||
|
|
53c61cb024 | ||
|
|
3d70247f8b | ||
|
|
52b1c7b93b | ||
|
|
daeb463f77 | ||
|
|
0c75f70a1d | ||
|
|
674df112a4 | ||
|
|
26efc776c7 | ||
|
|
f221dc95cf | ||
|
|
187fc6acd2 | ||
|
|
9ecb68f6a3 | ||
|
|
f0113655ce | ||
|
|
31265c626b | ||
|
|
e2603ac3ae | ||
|
|
edcdfb5937 | ||
|
|
7acbed9637 | ||
|
|
20786fa6fb | ||
|
|
03c750a8b5 | ||
|
|
4da8c6053f | ||
|
|
a43061c61b | ||
|
|
f81efdb38c | ||
|
|
11dc6bac4d | ||
|
|
7d5a886397 | ||
|
|
77aef8179d | ||
|
|
db34ded0a1 | ||
|
|
8a7a92a9b9 | ||
|
|
3eda32f9d7 | ||
|
|
69e7c8ce71 | ||
|
|
c18c193ea7 | ||
|
|
82e294e3ef | ||
|
|
ccf3701011 | ||
|
|
79a8758448 | ||
|
|
e4b64e1050 | ||
|
|
0b8f702aa0 | ||
|
|
8caf90536b | ||
|
|
50e7d89576 | ||
|
|
fe5e9382d7 | ||
|
|
934c01f08d | ||
|
|
4ced15e528 | ||
|
|
3c151ed172 | ||
|
|
dd09895d7e | ||
|
|
3dd2b2ddce | ||
|
|
e4027d6373 | ||
|
|
7e880d33c7 | ||
|
|
356b17160e | ||
|
|
9a9b0c74ba | ||
|
|
dbde661d90 | ||
|
|
8904267cad | ||
|
|
c58cb2d4e7 | ||
|
|
cf971ea599 | ||
|
|
71c922c914 | ||
|
|
3607b526f0 | ||
|
|
1c87e8134c | ||
|
|
2a849312cc | ||
|
|
127caf54ae | ||
|
|
5902f4b02d | ||
|
|
40dfa95291 | ||
|
|
b8cef5fbd6 | ||
|
|
eed6890700 | ||
|
|
36e0c71b10 | ||
|
|
1bf020c594 | ||
|
|
072111a692 | ||
|
|
2cd7a96324 | ||
|
|
d8ff64222d | ||
|
|
0deaee625d | ||
|
|
bdfd89167d | ||
|
|
6b35032c0e | ||
|
|
8bf4f8ef02 | ||
|
|
7676fc6c37 | ||
|
|
d2ff8dca46 | ||
|
|
9c3d7abff5 | ||
|
|
4df4e21255 | ||
|
|
3fdde93255 | ||
|
|
65a7faebd5 | ||
|
|
c3b0909be0 | ||
|
|
641361d7c3 | ||
|
|
4a149f073a | ||
|
|
ba43b3fd4b | ||
|
|
5c2314702b | ||
|
|
3051de7a61 | ||
|
|
1abf8e54c3 | ||
|
|
93624cdb2d | ||
|
|
a7c6ea1f43 | ||
|
|
f23093c3a9 | ||
|
|
6f4a150d82 | ||
|
|
6790cd1647 | ||
|
|
05408ef4ea | ||
|
|
2bc3fe8a57 | ||
|
|
015942a1e4 | ||
|
|
321da76fd9 | ||
|
|
f67b08768d | ||
|
|
fb1e59ef5a | ||
|
|
a1318f1061 | ||
|
|
9543951747 | ||
|
|
0926339482 | ||
|
|
7f60e133dc | ||
|
|
ba3893980d | ||
|
|
363f59d276 |
No files matched your search
@@ -1,4 +1,9 @@
|
||||
# go-assets.sh downloads
|
||||
/server/assets/fs/**.zip
|
||||
/server/assets/fs/**/garble*
|
||||
/server/assets/fs/**/zig.tar.xz
|
||||
/server/assets/fs/**/zig.zip
|
||||
/server/assets/fs/src.zip
|
||||
|
||||
/*.sdk.tar.xz
|
||||
|
||||
@@ -19,3 +24,5 @@
|
||||
/SECURITY.md
|
||||
/README.md
|
||||
/CONTRIBUTING.md
|
||||
|
||||
.downloaded_assets
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @BishopFox/red-team
|
||||
* @BishopFox/bf-red-team
|
||||
@@ -2,136 +2,109 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
servers-build:
|
||||
name: Release Build Server
|
||||
if: startsWith( github.ref, 'refs/tags/v')
|
||||
tag-check:
|
||||
name: Check Release Tag
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- name: Go 1.21
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.21"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
sudo apt-get update --fix-missing && sudo apt-get -y install \
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make
|
||||
run: make servers
|
||||
|
||||
- name: Server Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
name: servers
|
||||
path: ./sliver-server_*
|
||||
fetch-depth: 0
|
||||
|
||||
clients-build:
|
||||
name: Release Build Client
|
||||
if: startsWith( github.ref, 'refs/tags/v')
|
||||
- name: Validate Release Tag
|
||||
id: tag
|
||||
run: |
|
||||
git fetch --prune --tags -f origin
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
|
||||
if ! printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "Tag $tag is not a release semver tag."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/master"; then
|
||||
echo "Tag $tag does not point to a commit on origin/master."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
name: End-to-end test suites
|
||||
needs: tag-check
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
uses: ./.github/workflows/comprehensive-e2e.yml
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs:
|
||||
- tag-check
|
||||
- e2e
|
||||
if: needs.tag-check.outputs.tag != '' && needs.e2e.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make
|
||||
run: make clients
|
||||
|
||||
- name: Client Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
name: clients
|
||||
path: ./sliver-client_*
|
||||
fetch-depth: 0
|
||||
|
||||
tagged-release:
|
||||
needs: [servers-build, clients-build]
|
||||
|
||||
name: "Tagged Release"
|
||||
if: startsWith( github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
|
||||
steps:
|
||||
- run: mkdir -p ./builds
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
path: ./builds
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Extract Artifacts
|
||||
run: |
|
||||
mkdir -p ./artifacts
|
||||
cp ./builds/clients/* ./artifacts
|
||||
cp ./builds/servers/* ./artifacts
|
||||
- name: Install Minisign
|
||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||
|
||||
- name: GPG Key(s)
|
||||
- name: Build Clients
|
||||
run: |
|
||||
cat <(echo -e "${{ secrets.SLIVER_GPG }}") | gpg --batch --import
|
||||
gpg --list-secret-keys --keyid-format LONG
|
||||
make clients
|
||||
mkdir -p artifacts
|
||||
mv sliver-client_* artifacts/
|
||||
|
||||
# Linux
|
||||
- name: Linux 386 (Client Only)
|
||||
- name: Build Servers
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-386.sig --detach-sig ./artifacts/sliver-client_linux-386
|
||||
- name: Linux AMD64
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-server_linux-amd64.sig --detach-sig ./artifacts/sliver-server_linux-amd64
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-amd64.sig --detach-sig ./artifacts/sliver-client_linux-amd64
|
||||
- name: Linux ARM64
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-server_linux-arm64.sig --detach-sig ./artifacts/sliver-server_linux-arm64
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_linux-arm64.sig --detach-sig ./artifacts/sliver-client_linux-arm64
|
||||
make servers
|
||||
mv sliver-server_* artifacts/
|
||||
|
||||
# Windows
|
||||
- name: Windows 386 (Client Only)
|
||||
- name: Sign Artifacts
|
||||
env:
|
||||
MINISIGN_KEY: ${{ secrets.MINISIGN_SLIVER_RELEASE_KEY }}
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-386.sig --detach-sig ./artifacts/sliver-client_windows-386.exe
|
||||
- name: Windows AMD64
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-server_windows-amd64.sig --detach-sig ./artifacts/sliver-server_windows-amd64.exe
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-amd64.sig --detach-sig ./artifacts/sliver-client_windows-amd64.exe
|
||||
- name: Windows ARM64 (Client Only)
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_windows-arm64.sig --detach-sig ./artifacts/sliver-client_windows-arm64.exe
|
||||
echo "$MINISIGN_KEY" | base64 -d > minisign.key
|
||||
echo "minisign.key bytes: $(wc -c < minisign.key | tr -d ' ')"
|
||||
echo "minisign.key sha256: $(sha256sum minisign.key | awk '{print $1}')"
|
||||
chmod 600 minisign.key
|
||||
for file in artifacts/sliver-*; do
|
||||
[ -f "$file" ] || continue
|
||||
minisign -S -s minisign.key -m "$file" -x "$file.minisig" -W
|
||||
done
|
||||
rm -f minisign.key
|
||||
|
||||
# MacOS
|
||||
- name: MacOS AMD64
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-server_macos-amd64.sig --detach-sig ./artifacts/sliver-server_macos-amd64
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_macos-amd64.sig --detach-sig ./artifacts/sliver-client_macos-amd64
|
||||
- name: Package MacOS ARM64
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-server_macos-arm64.sig --detach-sig ./artifacts/sliver-server_macos-arm64
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_macos-arm64.sig --detach-sig ./artifacts/sliver-client_macos-arm64
|
||||
tag="${{ needs.tag-check.outputs.tag }}"
|
||||
if gh release view "$tag" >/dev/null 2>&1; then
|
||||
echo "Release $tag already exists."
|
||||
exit 0
|
||||
fi
|
||||
gh release create "$tag" --generate-notes
|
||||
|
||||
# FreeBSD
|
||||
- name: FreeBSD AMD64 (Client Only)
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_freebsd-amd64.sig --detach-sig ./artifacts/sliver-client_freebsd-amd64.exe
|
||||
- name: FreeBSD ARM64 (Client Only)
|
||||
run: |
|
||||
gpg --default-key 4449039C --output ./artifacts/sliver-client_freebsd-arm64.sig --detach-sig ./artifacts/sliver-client_freebsd-arm64.exe
|
||||
|
||||
- name: "Publish Release"
|
||||
uses: "bishopfox/action-gh-release@v1"
|
||||
with:
|
||||
files: |
|
||||
./artifacts/*
|
||||
- name: Upload Artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release upload "${{ needs.tag-check.outputs.tag }}" artifacts/* --clobber
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head of the pull request.
|
||||
@@ -36,10 +36,10 @@ jobs:
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: go
|
||||
@@ -48,6 +48,6 @@ jobs:
|
||||
run: make
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
env:
|
||||
VERSION: ${{github.sha}}
|
||||
@@ -0,0 +1,551 @@
|
||||
name: Comprehensive e2e Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: true
|
||||
default: comprehensive
|
||||
type: choice
|
||||
options:
|
||||
- comprehensive
|
||||
- rportfwd
|
||||
workflow_call:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: false
|
||||
default: comprehensive
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
|
||||
concurrency:
|
||||
group: comprehensive-e2e-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
name: Authorize invocation
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check actor repository permission
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
REQUESTING_ACTOR: ${{ github.triggering_actor }}
|
||||
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
with:
|
||||
script: |
|
||||
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
|
||||
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
|
||||
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
|
||||
return;
|
||||
}
|
||||
core.info(`Validated E2E test scope ${testScope}`);
|
||||
|
||||
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
|
||||
if (isReleaseTag) {
|
||||
core.info(`Authorized release test invocation for ${context.ref}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = process.env.REQUESTING_ACTOR || context.actor;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: actor,
|
||||
});
|
||||
core.info(`${actor} has repository permission ${data.permission} (${data.role_name})`);
|
||||
if (data.permission !== "admin") {
|
||||
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
|
||||
}
|
||||
|
||||
rportfwd-security-regression:
|
||||
name: Reverse-port-forward malicious protocol regression
|
||||
needs: authorize
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Test malicious destination poisoning and cross-session isolation
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: >-
|
||||
go test -buildvcs=false -mod=vendor
|
||||
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
|
||||
./server/c2
|
||||
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
|
||||
-count=1 -v
|
||||
|
||||
darwin:
|
||||
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os darwin \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
linux-native:
|
||||
name: comprehensive-e2e-linux-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os linux \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
windows:
|
||||
name: comprehensive-e2e-windows-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server.exe ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e.exe ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e.exe \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server.exe \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os windows \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate-rportfwd:
|
||||
name: Aggregate reverse-port-forward E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: rportfwd-coverage-input
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
|
||||
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce all 162 required cells
|
||||
id: aggregate
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input rportfwd-coverage-input
|
||||
-output rportfwd-coverage-summary
|
||||
|
||||
- name: Publish reverse-port-forward coverage table
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="rportfwd-coverage-summary/rportfwd-coverage.md"
|
||||
if [[ -s "${report}" ]]; then
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
- name: Upload aggregate reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-rportfwd-coverage-summary
|
||||
path: rportfwd-coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
aggregate-coverage:
|
||||
name: Aggregate comprehensive E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
|
||||
outputs:
|
||||
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: coverage-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p coverage-input coverage-summary
|
||||
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce required cells
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/report
|
||||
-input coverage-input
|
||||
-output coverage-summary
|
||||
|
||||
- name: Publish command coverage table
|
||||
id: publish_command_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
detailed_report="coverage-summary/coverage-summary.md"
|
||||
report="coverage-summary/command-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [[ -f "${detailed_report}" ]]; then
|
||||
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-coverage-summary
|
||||
path: coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites or failed or not-run coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
reflektor:
|
||||
name: Existing Reflektor test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/reflektor-tests.yml
|
||||
|
||||
shellcode:
|
||||
name: Shellcode E2E test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/shellcode-tests.yml
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Detect package manager
|
||||
id: detect-package-manager
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
.next/cache
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
run: ${{ steps.detect-package-manager.outputs.runner }} next build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/docs/sliver-docs/out
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
name: golangci-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- ".golangci.yml"
|
||||
- ".github/workflows/golangci-lint.yml"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- "go.work"
|
||||
- "go.work.sum"
|
||||
- "Makefile"
|
||||
- "vendor/**"
|
||||
- "**/*.go"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: golangci-lint (${{ matrix.goos }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: "0"
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.13.1
|
||||
# Gate only findings introduced since the event base revision.
|
||||
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
|
||||
@@ -0,0 +1,164 @@
|
||||
name: Reflektor Integration Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
darwin:
|
||||
name: reflektor-darwin-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-native:
|
||||
name: reflektor-linux-${{ matrix.goarch }}-native
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
windows:
|
||||
name: reflektor-windows-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: amd64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e.exe
|
||||
-repo .
|
||||
-server ./sliver-server.exe
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
@@ -0,0 +1,201 @@
|
||||
name: Shellcode E2E Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
shellcode_coverage_markdown:
|
||||
description: Shellcode E2E coverage table in Markdown
|
||||
value: ${{ jobs.aggregate.outputs.shellcode_coverage_markdown }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
target:
|
||||
name: shellcode-e2e-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-26
|
||||
target_os: darwin
|
||||
target_arch: arm64
|
||||
server_os: darwin
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04
|
||||
target_os: linux
|
||||
target_arch: amd64
|
||||
server_os: linux
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04-arm
|
||||
target_os: linux
|
||||
target_arch: arm64
|
||||
server_os: linux
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: windows-2022
|
||||
target_os: windows
|
||||
target_arch: amd64
|
||||
server_os: windows
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server.exe
|
||||
test_executable: ./sliver-shellcode-e2e.exe
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare target artifact
|
||||
run: |
|
||||
mkdir -p shellcode-results
|
||||
printf '%s/%s\n' \
|
||||
'${{ matrix.target_os }}' \
|
||||
'${{ matrix.target_arch }}' \
|
||||
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: ${{ matrix.server_os }}
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o ${{ matrix.server_executable }} ./server
|
||||
|
||||
- name: Compile shellcode E2E client
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.target_arch }}
|
||||
GOOS: ${{ matrix.target_os }}
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o ${{ matrix.test_executable }} ./test/e2e
|
||||
|
||||
- name: Run shellcode generation and execution tests
|
||||
run: >-
|
||||
${{ matrix.test_executable }}
|
||||
-test.v
|
||||
-test.run=^TestShellcodeE2E$
|
||||
-test.timeout=0
|
||||
-repo .
|
||||
-server ${{ matrix.server_executable }}
|
||||
-server-arch ${{ matrix.server_arch }}
|
||||
-target-os ${{ matrix.target_os }}
|
||||
-target-arch ${{ matrix.target_arch }}
|
||||
-results ./shellcode-results
|
||||
-transports mtls,wg,http
|
||||
-implant-modes session,beacon
|
||||
-beacon-interval 10s
|
||||
-shellcode-sgn-samples 4
|
||||
|
||||
- name: Upload target shellcode results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: shellcode-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate:
|
||||
name: Aggregate shellcode E2E coverage
|
||||
needs: target
|
||||
if: ${{ always() }}
|
||||
outputs:
|
||||
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p shellcode-input shellcode-summary
|
||||
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
|
||||
|
||||
- name: Download target shellcode results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: shellcode-e2e-target-*
|
||||
path: shellcode-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Aggregate shellcode coverage
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
|
||||
-input ./shellcode-input
|
||||
-output ./shellcode-summary
|
||||
|
||||
- name: Publish shellcode coverage table
|
||||
id: publish_shellcode_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="shellcode-summary/shellcode-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate shellcode coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-coverage-summary
|
||||
path: shellcode-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed or not-run shellcode coverage
|
||||
if: ${{ always() && steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
@@ -6,11 +6,15 @@ on:
|
||||
paths:
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- ".gitattributes"
|
||||
- "client/command/opfor/testdata/corpus/**"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- "Makefile"
|
||||
- "**/*.go"
|
||||
- "**/*.c"
|
||||
- "**/*.h"
|
||||
- "**/*.sh"
|
||||
- "**/*.ps1"
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -18,15 +22,15 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
linux-windows-build-test:
|
||||
name: Linux & Windows Test
|
||||
linux-build-test:
|
||||
name: Linux Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -34,38 +38,92 @@ jobs:
|
||||
git build-essential zlib1g zlib1g-dev wget zip unzip
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make Linux (amd64)
|
||||
run: make linux-amd64
|
||||
|
||||
- name: Test Linux
|
||||
run: ./sliver-server unpack --force && ./go-tests.sh
|
||||
|
||||
- name: Make Windows (amd64)
|
||||
run: make windows-amd64
|
||||
|
||||
- name: Make Linux (arm64)
|
||||
run: make linux-arm64
|
||||
|
||||
- name: Make Linux (amd64)
|
||||
run: make linux-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
windows-build-test:
|
||||
name: Windows Build & Test
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Install make
|
||||
run: choco install make -y --no-progress
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
- name: Make Windows (amd64)
|
||||
run: make windows-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: pwsh
|
||||
run: |
|
||||
$baseSha = "${{ github.event.pull_request.base.sha }}"
|
||||
$headSha = "${{ github.sha }}"
|
||||
$changedFiles = git diff --name-only $baseSha $headSha
|
||||
|
||||
if ($changedFiles | Select-String -Pattern '^(implant/|server/gogo/|server/generate/)') {
|
||||
"args=" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
} else {
|
||||
"args=--skip-generate" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
}
|
||||
|
||||
- name: Test Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
make clean
|
||||
make
|
||||
./go-tests.ps1 ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
name: MacOS Test
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
|
||||
- name: Mingw
|
||||
run: brew install mingw-w64
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
@@ -73,24 +131,37 @@ jobs:
|
||||
- name: Make MacOS (amd64)
|
||||
run: make macos-amd64
|
||||
|
||||
- name: Test MacOS (amd64)
|
||||
run: ./sliver-server unpack --force && ./go-tests.sh
|
||||
|
||||
- name: Make MacOS (arm64)
|
||||
run: make macos-arm64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test MacOS
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
clients-build:
|
||||
name: Clients Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
+12
@@ -1,7 +1,17 @@
|
||||
/crush
|
||||
|
||||
/examples
|
||||
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
/artifacts
|
||||
/artifacts/**
|
||||
|
||||
*.rc
|
||||
|
||||
/Dockerfile.*
|
||||
/*.sdk.tar.xz
|
||||
|
||||
@@ -45,6 +55,8 @@ sliver-server-windows.zip
|
||||
.DS_Store
|
||||
|
||||
dist/
|
||||
.gocache/
|
||||
.gomodcache/
|
||||
|
||||
# Flag to show that go-assets.sh has been run
|
||||
.downloaded_assets
|
||||
@@ -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
|
||||
@@ -0,0 +1,18 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
||||
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
|
||||
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
|
||||
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
|
||||
- Do not import `server` packages from `client`.
|
||||
|
||||
## Testing Guidelines
|
||||
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
|
||||
- Add or update tests when behavior changes; some server packages run longer tests.
|
||||
|
||||
+11
-7
@@ -5,11 +5,12 @@
|
||||
#
|
||||
# For unit testing:
|
||||
# docker build --target test .
|
||||
# docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .
|
||||
#
|
||||
|
||||
# STAGE: base
|
||||
## Compiles Sliver for use
|
||||
FROM golang:1.22.2 as base
|
||||
FROM golang:latest AS base
|
||||
|
||||
### Base packages
|
||||
RUN apt-get update --fix-missing && apt-get -y install \
|
||||
@@ -20,16 +21,19 @@ RUN groupadd -g 999 sliver && useradd -r -u 999 -g sliver sliver
|
||||
RUN mkdir -p /home/sliver/ && chown -R sliver:sliver /home/sliver
|
||||
|
||||
### Build sliver:
|
||||
RUN mkdir -p /go/src/github.com/bishopfox/sliver
|
||||
WORKDIR /go/src/github.com/bishopfox/sliver
|
||||
ADD . /go/src/github.com/bishopfox/sliver/
|
||||
RUN make clean-all
|
||||
RUN make
|
||||
RUN make
|
||||
RUN cp -vv sliver-server /opt/sliver-server
|
||||
|
||||
# STAGE: test
|
||||
## Run unit tests against the compiled instance
|
||||
## Use `--target test` in the docker build command to run this stage
|
||||
FROM base as test
|
||||
FROM base AS test
|
||||
|
||||
ARG GO_TESTS_FLAGS=""
|
||||
ENV GO_TESTS_FLAGS="${GO_TESTS_FLAGS}"
|
||||
|
||||
RUN apt-get update --fix-missing \
|
||||
&& apt-get -y upgrade \
|
||||
@@ -39,11 +43,11 @@ RUN apt-get update --fix-missing \
|
||||
RUN /opt/sliver-server unpack --force
|
||||
|
||||
### Run unit tests
|
||||
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh
|
||||
RUN /go/src/github.com/bishopfox/sliver/go-tests.sh ${GO_TESTS_FLAGS}
|
||||
|
||||
# STAGE: production
|
||||
## Final dockerized form of Sliver
|
||||
FROM debian:bookworm-slim as production
|
||||
FROM debian:bookworm-slim AS production
|
||||
|
||||
### Install production packages
|
||||
RUN apt-get update --fix-missing \
|
||||
@@ -91,7 +95,7 @@ ENTRYPOINT [ "/opt/sliver-server" ]
|
||||
|
||||
|
||||
# STAGE: production-slim (about 1Gb smaller)
|
||||
FROM debian:bookworm-slim as production-slim
|
||||
FROM debian:bookworm-slim AS production-slim
|
||||
|
||||
### Install production packages
|
||||
RUN apt-get update --fix-missing \
|
||||
|
||||
@@ -2,202 +2,330 @@
|
||||
# Makefile for Sliver
|
||||
#
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
|
||||
GO ?= go
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ARTIFACT_SUFFIX ?=
|
||||
ENV =
|
||||
TAGS ?= -tags go_sqlite
|
||||
CGO_ENABLED = 0
|
||||
GOLANGCI_LINT ?= golangci-lint
|
||||
# The tree has legacy lint findings, so gate issues introduced by this change.
|
||||
GOLANGCI_LINT_BASE ?= master
|
||||
GOLANGCI_LINT_ARGS ?= --new-from-merge-base=$(GOLANGCI_LINT_BASE)
|
||||
GOLANGCI_LINT_PACKAGES = \
|
||||
./client/... \
|
||||
./server/... \
|
||||
./util/... \
|
||||
./protobuf/... \
|
||||
./docs \
|
||||
./implant/...
|
||||
|
||||
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
|
||||
CGO_ENABLED = 1
|
||||
endif
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git zip date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Version Information
|
||||
#
|
||||
GO_VERSION = $(shell $(GO) version)
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 21
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 27
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
VERSION ?= $(shell git describe --abbrev=0)
|
||||
COMPILED_AT = $(shell date +%s)
|
||||
RELEASES_URL ?= https://api.github.com/repos/BishopFox/sliver/releases
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
VERSION_PKG = github.com/bishopfox/sliver/client/version
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
|
||||
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
|
||||
GIT_COMMIT = $(shell git rev-parse HEAD)
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
# Targets
|
||||
# >>> WINDOWS <<<
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
else
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
SHELL := cmd.exe
|
||||
.SHELLFLAGS := /C
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
|
||||
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers:
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
./go-assets.sh
|
||||
touch ./.downloaded_assets
|
||||
COMMA := ,
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
define windows_exec
|
||||
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||
endef
|
||||
|
||||
define windows_go_build
|
||||
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: pb
|
||||
pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: validate-pb-compilers
|
||||
validate-pb-compilers:
|
||||
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
|
||||
|
||||
validate-go-version:
|
||||
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
-rmdir /S /Q server\assets\fs\darwin\amd64
|
||||
-rmdir /S /Q server\assets\fs\darwin\arm64
|
||||
-rmdir /S /Q server\assets\fs\windows\amd64
|
||||
-rmdir /S /Q server\assets\fs\linux\amd64
|
||||
-del /Q /F server\assets\fs\*.zip 2>NUL
|
||||
-del /Q /F .downloaded_assets 2>NUL
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets:
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
|
||||
endif
|
||||
@@ -1,33 +1,16 @@
|
||||
# Sliver
|
||||
|
||||
Sliver is an open source cross-platform adversary emulation/red team framework, it can be used by organizations of all sizes to perform security testing. Sliver's implants support C2 over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS and are dynamically compiled with per-binary asymmetric encryption keys.
|
||||
Sliver is an open-source, cross-platform adversary emulation and red team framework designed for organizations of all sizes to perform authorized security testing.
|
||||
|
||||
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
|
||||
Sliver implants support command-and-control (C2) over Mutual TLS (mTLS), WireGuard, HTTP(S), and DNS. Implants are dynamically compiled with unique per-binary asymmetric encryption keys and support a wide range of post-exploitation capabilities across macOS, Windows, and Linux.
|
||||
|
||||
[](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)
|
||||
Sliver includes support for native and shellcode payloads, userspace reflective loading, built-in shellcode encoding, and BOF/COFF execution on both amd64 and arm64 architectures for macOS, Windows, and Linux.
|
||||
|
||||
# v1.6.0 / `master`
|
||||
The Sliver server and client run on macOS, Windows, and Linux. Implants officially support those same platforms and may also run on additional targets supported by the Go compiler, although those configurations are not regularly tested.
|
||||
|
||||
**NOTE:** You are looking the latest master branch of Sliver v1.6.0; new PRs should target this branch. However, this branch is NOT RECOMMENDED for production use yet. Please use release tagged versions for the best experience.
|
||||
[](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)
|
||||
|
||||
For PRs containing bug fixes specific to Sliver v1.5, please target the [`v1.5.x/master` branch](https://github.com/BishopFox/sliver/tree/v1.5.x/master).
|
||||
|
||||
### Features
|
||||
|
||||
- Dynamic code generation
|
||||
- Compile-time obfuscation
|
||||
- Multiplayer-mode
|
||||
- Staged and Stageless payloads
|
||||
- [Procedurally generated C2](https://sliver.sh/docs?name=HTTPS+C2) over HTTP(S)
|
||||
- [DNS canary](https://sliver.sh/docs?name=DNS+C2) blue team detection
|
||||
- [Secure C2](https://sliver.sh/docs?name=Transport+Encryption) over mTLS, WireGuard, HTTP(S), and DNS
|
||||
- Fully scriptable using [JavaScript/TypeScript](https://github.com/moloch--/sliver-script) or [Python](https://github.com/moloch--/sliver-py)
|
||||
- Windows process migration, process injection, user token manipulation, etc.
|
||||
- Let's Encrypt integration
|
||||
- In-memory .NET assembly execution
|
||||
- COFF/BOF in-memory loader
|
||||
- TCP and named pipe pivots
|
||||
- Much more!
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Getting Started
|
||||
|
||||
@@ -39,16 +22,12 @@ Download the latest [release](https://github.com/BishopFox/sliver/releases) and
|
||||
|
||||
### Help!
|
||||
|
||||
Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions). We also tend to hang out in the #golang Slack channel on the [Bloodhound Gang](https://bloodhoundgang.herokuapp.com/) server.
|
||||
Please checkout the [wiki](https://sliver.sh/), or start a [GitHub discussion](https://github.com/BishopFox/sliver/discussions).
|
||||
|
||||
### Compile From Source
|
||||
|
||||
See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
|
||||
### Feedback
|
||||
|
||||
Please take a moment and fill out [our survey](https://forms.gle/SwVsHFNh24ChG58C6)
|
||||
|
||||
### License - GPLv3
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
INFO = "\033[1m\033[36m[*]\033[0m "
|
||||
WARN = "\033[1m\033[31m[!]\033[0m "
|
||||
BOLD = "\033[1m"
|
||||
NORM = "\033[0m"
|
||||
|
||||
|
||||
def exec_cmd(cmd, ignore_status=False):
|
||||
"""Executes a command with some sugar"""
|
||||
print("\n" + INFO + " %s\n" % cmd)
|
||||
status = os.system(cmd)
|
||||
if status != 0 and not ignore_status:
|
||||
print("\n" + WARN + "Command did not exit cleanly (%s) " % status)
|
||||
if input("continue? [Y/n]: ").upper() != "Y":
|
||||
sys.exit(status)
|
||||
return status
|
||||
|
||||
|
||||
def docker_rm_volumes():
|
||||
exec_cmd("docker volume rm $(docker volume ls -q)", ignore_status=True)
|
||||
|
||||
|
||||
def docker_rm_containers():
|
||||
exec_cmd("docker rm $(docker ps -a -q)", ignore_status=True)
|
||||
|
||||
|
||||
def docker_rm_images():
|
||||
exec_cmd("docker rmi -f $(docker images -q)", ignore_status=True)
|
||||
|
||||
|
||||
def docker_prune():
|
||||
exec_cmd("docker image prune -f", ignore_status=True)
|
||||
|
||||
|
||||
def docker_rm_all():
|
||||
docker_rm_containers()
|
||||
docker_rm_images()
|
||||
docker_rm_volumes()
|
||||
|
||||
|
||||
def build():
|
||||
status = exec_cmd("docker build --target test .")
|
||||
if status == 0:
|
||||
print("\n\n" + INFO + "Build successful")
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Execute the respective task(s) based on cli args"""
|
||||
if args.rm_images:
|
||||
docker_rm_images()
|
||||
if args.rm_containers:
|
||||
docker_rm_containers()
|
||||
if args.rm_volumes:
|
||||
docker_rm_volumes()
|
||||
if args.rm_all:
|
||||
docker_rm_all()
|
||||
|
||||
if not args.no_prune:
|
||||
docker_prune()
|
||||
if not args.no_build:
|
||||
build()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="automates interactions with docker")
|
||||
|
||||
parser.add_argument(
|
||||
"--no-build",
|
||||
"-b",
|
||||
help="do not run build",
|
||||
dest="no_build",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-prune",
|
||||
"-np",
|
||||
help="do not prune images",
|
||||
dest="no_prune",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--rm-all",
|
||||
"-rma",
|
||||
help="rm all containers, images, and volumes",
|
||||
dest="rm_all",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rm-containers",
|
||||
"-rmc",
|
||||
help="rm all containers",
|
||||
dest="rm_containers",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rm-images",
|
||||
"-rmi",
|
||||
help="rm all images",
|
||||
dest="rm_images",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rm-volumes",
|
||||
"-rmv",
|
||||
help="rm dangling volumes only",
|
||||
dest="rm_volumes",
|
||||
action="store_true",
|
||||
)
|
||||
try:
|
||||
main(parser.parse_args())
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n" + WARN + "User stopped process")
|
||||
+23
-15
@@ -1,18 +1,26 @@
|
||||
Client
|
||||
=======
|
||||
# client
|
||||
|
||||
This directory contains the client implementation, pretty much all of this code is also used by the server console.
|
||||
## Overview
|
||||
|
||||
__Important:__ No code in this directory can import any code from `server/`.
|
||||
Main command-line client for interacting with and administering Sliver servers. Provides the Cobra entrypoint and aggregates console, command, and transport subsystems.
|
||||
|
||||
* `assets/` - Static asset files and management code (e.g. client configs)
|
||||
* `cli/` - Command line interface implementation
|
||||
* `command/` - Console command implementations
|
||||
* `constants/` - Various shared constant values
|
||||
* `core/` - Client state management
|
||||
* `licenses/` - Open source licenses
|
||||
* `spin/` - Console spinner library
|
||||
* `tcpproxy/` - Local TCP proxy listener code
|
||||
* `transport/` - Wires the client to the server
|
||||
* `version/` - Version information
|
||||
* `main.go` - Entrypoint
|
||||
## Go Files
|
||||
|
||||
- `main.go` – Entry point that initializes the CLI, processes flags, and launches the interactive console.
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `assets/` – Manages embedded client asset manifests such as operator profiles, settings, and armory catalogs. Handles serialization, bundling, and lookup of static client metadata.
|
||||
- `cli/` – Defines the Cobra root command and CLI initialization for the Sliver client binary. Sets up persistent flags, environment bootstrapping, and command wiring.
|
||||
- `command/` – Shared registration glue, scaffolding, and helpers for Sliver client console command groups. Hosts common utilities for binding Cobra commands to RPC-backed actions.
|
||||
- `console/` – Renders the interactive console UI, logging, and terminal helpers for the client. Coordinates readline behavior, prompt styling, and message formatting.
|
||||
- `constants/` – Centralizes constants shared across the client runtime and command packages. Captures strings, flag names, and feature toggles used throughout the CLI.
|
||||
- `core/` – Maintains client application state, background workers, and RPC coordination. Orchestrates session tracking, event streams, and health checks.
|
||||
- `credentials/` – Credential management utilities including sniffers and import helpers for operators. Provides parsers, storage helpers, and output formatting for recovered secrets.
|
||||
- `licenses/` – Holds embedded license metadata bundled with the client distribution. Supplies license retrieval utilities used for attribution commands.
|
||||
- `overlord/` – Supervises long-running client jobs, scheduling, and task orchestration. Tracks job lifecycle events and exposes status updates to the console.
|
||||
- `packages/` – Deals with packaged extensions and module discovery for the client. Scans, validates, and caches extension manifests and assets.
|
||||
- `spin/` – Lightweight terminal spinner utilities used for progress feedback. Wraps spinner styles and timing helpers shared across commands.
|
||||
- `tcpproxy/` – Implements the local TCP proxy used for port forwarding from the client side. Manages listener lifecycles, connection pumps, and cleanup hooks.
|
||||
- `transport/` – Client-side transports and RPC wiring to communicate with Sliver servers. Builds authenticated gRPC clients, TLS dialers, and reconnect logic.
|
||||
- `version/` – Build-time version and semantic metadata for the client binary. Exposes git information, release channels, and helper formatters.
|
||||
+14
-2
@@ -1,3 +1,15 @@
|
||||
Assets
|
||||
======
|
||||
# client/assets
|
||||
|
||||
## Overview
|
||||
|
||||
Manages embedded client asset manifests such as operator profiles, settings, and armory catalogs. Handles serialization, bundling, and lookup of static client metadata. Core logic addresses aliases, armories, C2 profiles, and extensions within the assets package.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `aliases.go` – Provides helpers for discovering, installing, and reading alias manifests shipped with the client.
|
||||
- `armories.go` – Loads configured armory definitions and persists operator customizations.
|
||||
- `assets.go` – Central entry point that exposes asset directories, lazy initialization, and filesystem utilities.
|
||||
- `c2profiles.go` – Embeds default HTTP C2 profile templates and exposes lookup utilities.
|
||||
- `config.go` – Tracks client configuration paths and manages persistence of runtime options.
|
||||
- `extensions.go` – Handles embedded extension manifests and supports lookup by command name.
|
||||
- `settings.go` – Stores default console settings and manages serialization to disk.
|
||||
+24
-6
@@ -34,12 +34,19 @@ const (
|
||||
SliverClientDirName = ".sliver-client"
|
||||
|
||||
versionFileName = "version"
|
||||
envVarName = "SLIVER_CLIENT_ROOT_DIR"
|
||||
)
|
||||
|
||||
// GetRootAppDir - Get the Sliver app dir ~/.sliver-client/
|
||||
func GetRootAppDir() string {
|
||||
user, _ := user.Current()
|
||||
dir := filepath.Join(user.HomeDir, SliverClientDirName)
|
||||
value := os.Getenv(envVarName)
|
||||
var dir string
|
||||
if len(value) == 0 {
|
||||
user, _ := user.Current()
|
||||
dir = filepath.Join(user.HomeDir, SliverClientDirName)
|
||||
} else {
|
||||
dir = value
|
||||
}
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
@@ -73,6 +80,18 @@ func GetConsoleLogsDir() string {
|
||||
return consoleLogsDir
|
||||
}
|
||||
|
||||
// GetMCPLogsDir - Get the Sliver client MCP logs dir ~/.sliver-client/logs/mcp/
|
||||
func GetMCPLogsDir() string {
|
||||
mcpLogsDir := filepath.Join(GetClientLogsDir(), "mcp")
|
||||
if _, err := os.Stat(mcpLogsDir); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(mcpLogsDir, 0700)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return mcpLogsDir
|
||||
}
|
||||
|
||||
func assetVersion() string {
|
||||
appDir := GetRootAppDir()
|
||||
data, err := os.ReadFile(filepath.Join(appDir, versionFileName))
|
||||
@@ -96,7 +115,7 @@ func Setup(force bool, echo bool) {
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2022 Bishop Fox
|
||||
Sliver Copyright (C) 2026 Bishop Fox
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type 'licenses'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type 'licenses' for details.`)
|
||||
@@ -104,7 +123,6 @@ under certain conditions; type 'licenses' for details.`)
|
||||
}
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
|
||||
SaveSettings(nil)
|
||||
}
|
||||
_, _ = LoadSettings()
|
||||
_, _ = LoadSpoofMetadataConfig()
|
||||
}
|
||||
+12
-25
@@ -35,7 +35,7 @@ type NameValueProbability struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Probability int `json:"probability"`
|
||||
Methods []string
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
// HTTPC2ImplantConfig - Implant configuration options
|
||||
@@ -57,30 +57,17 @@ type HTTPC2ImplantConfig struct {
|
||||
NonceQueryArgChars string `json:"nonce_query_args"`
|
||||
URLParameters []NameValueProbability `json:"url_parameters"`
|
||||
Headers []NameValueProbability `json:"headers"`
|
||||
NonceQueryLength int `json:"nonce_query_length"`
|
||||
NonceMode string `json:"nonce_mode"`
|
||||
|
||||
MaxFiles int `json:"max_files"`
|
||||
MinFiles int `json:"min_files"`
|
||||
MaxPaths int `json:"max_paths"`
|
||||
MinPaths int `json:"min_paths"`
|
||||
MaxFileGen int `json:"max_files"`
|
||||
MinFileGen int `json:"min_files"`
|
||||
MaxPathGen int `json:"max_paths"`
|
||||
MinPathGen int `json:"min_paths"`
|
||||
MaxPathLength int `json:"max_path_length"`
|
||||
MinPathLength int `json:"min_path_length"`
|
||||
|
||||
// Stager files and paths
|
||||
StagerFileExt string `json:"stager_file_ext"`
|
||||
StagerFiles []string `json:"stager_files"`
|
||||
StagerPaths []string `json:"stager_paths"`
|
||||
|
||||
// Poll files and paths
|
||||
PollFileExt string `json:"poll_file_ext"`
|
||||
PollFiles []string `json:"poll_files"`
|
||||
PollPaths []string `json:"poll_paths"`
|
||||
|
||||
// Session files and paths
|
||||
StartSessionFileExt string `json:"start_session_file_ext"`
|
||||
SessionFileExt string `json:"session_file_ext"`
|
||||
SessionFiles []string `json:"session_files"`
|
||||
SessionPaths []string `json:"session_paths"`
|
||||
|
||||
// Close session files and paths
|
||||
CloseFileExt string `json:"close_file_ext"`
|
||||
CloseFiles []string `json:"close_files"`
|
||||
ClosePaths []string `json:"close_paths"`
|
||||
Extensions []string `json:"extensions"`
|
||||
Files []string `json:"files"`
|
||||
Paths []string `json:"paths"`
|
||||
}
|
||||
+18
-7
@@ -34,15 +34,26 @@ const (
|
||||
ConfigDirName = "configs"
|
||||
)
|
||||
|
||||
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
|
||||
type ClientWGConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
ServerPubKey string `json:"server_pub_key"`
|
||||
ClientPrivateKey string `json:"client_private_key"`
|
||||
ClientPubKey string `json:"client_pub_key"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
}
|
||||
|
||||
// ClientConfig - Client JSON config
|
||||
type ClientConfig struct {
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
|
||||
LHost string `json:"lhost"`
|
||||
LPort int `json:"lport"`
|
||||
Token string `json:"token"`
|
||||
CACertificate string `json:"ca_certificate"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Certificate string `json:"certificate"`
|
||||
WG *ClientWGConfig `json:"wg,omitempty"`
|
||||
}
|
||||
|
||||
// GetConfigDir - Returns the path to the config dir
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClientWGConfigEnabledIsPositiveOptIn(t *testing.T) {
|
||||
legacyJSON := []byte(`{"wg":{"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
|
||||
legacy := &ClientConfig{}
|
||||
if err := json.Unmarshal(legacyJSON, legacy); err != nil {
|
||||
t.Fatalf("unmarshal legacy config: %v", err)
|
||||
}
|
||||
if legacy.WG == nil {
|
||||
t.Fatal("expected legacy WireGuard config")
|
||||
}
|
||||
if legacy.WG.Enabled {
|
||||
t.Fatal("expected an absent enabled marker to remain disabled")
|
||||
}
|
||||
|
||||
enabledJSON := []byte(`{"wg":{"enabled":true,"server_pub_key":"server-pub","client_private_key":"client-priv","client_ip":"100.64.0.2"}}`)
|
||||
enabled := &ClientConfig{}
|
||||
if err := json.Unmarshal(enabledJSON, enabled); err != nil {
|
||||
t.Fatalf("unmarshal enabled config: %v", err)
|
||||
}
|
||||
if enabled.WG == nil || !enabled.WG.Enabled {
|
||||
t.Fatal("expected enabled marker to opt in to WireGuard")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func legacyBackupPath(legacyPath string) string {
|
||||
dir := filepath.Dir(legacyPath)
|
||||
base := filepath.Base(legacyPath)
|
||||
name := strings.TrimSuffix(base, filepath.Ext(base))
|
||||
return filepath.Join(dir, "."+name+".json-old")
|
||||
}
|
||||
|
||||
func renameLegacyConfig(legacyPath string) error {
|
||||
if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
backupPath := legacyBackupPath(legacyPath)
|
||||
if _, err := os.Stat(backupPath); err == nil {
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Rename(legacyPath, backupPath)
|
||||
}
|
||||
+92
-16
@@ -22,39 +22,111 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
settingsFileName = "tui-settings.json"
|
||||
settingsFileName = "tui-settings.yaml"
|
||||
settingsLegacyFileName = "tui-settings.json"
|
||||
)
|
||||
|
||||
const (
|
||||
// Accepted values for tui-settings.yaml "prompt:":
|
||||
// - host: show "[host]" prefix on sliver-client, "[server]" on server console
|
||||
// - operator-host: show "[operator@host]" prefix on sliver-client, "[server]" on server console
|
||||
// - basic: show "sliver >" (no prefix)
|
||||
// - custom: render full prompt from prompt_template
|
||||
PromptStyleHost = "host"
|
||||
PromptStyleOperatorHost = "operator-host"
|
||||
PromptStyleBasic = "basic"
|
||||
PromptStyleCustom = "custom"
|
||||
)
|
||||
|
||||
// ClientSettings - Client JSON config
|
||||
type ClientSettings struct {
|
||||
TableStyle string `json:"tables"`
|
||||
AutoAdult bool `json:"autoadult"`
|
||||
BeaconAutoResults bool `json:"beacon_autoresults"`
|
||||
SmallTermWidth int `json:"small_term_width"`
|
||||
AlwaysOverflow bool `json:"always_overflow"`
|
||||
VimMode bool `json:"vim_mode"`
|
||||
UserConnect bool `json:"user_connect"`
|
||||
ConsoleLogs bool `json:"console_logs"`
|
||||
TableStyle string `json:"tables" yaml:"tables"`
|
||||
AutoAdult bool `json:"autoadult" yaml:"autoadult"`
|
||||
BeaconAutoResults bool `json:"beacon_autoresults" yaml:"beacon_autoresults"`
|
||||
SmallTermWidth int `json:"small_term_width" yaml:"small_term_width"`
|
||||
AlwaysOverflow bool `json:"always_overflow" yaml:"always_overflow"`
|
||||
VimMode bool `json:"vim_mode" yaml:"vim_mode"`
|
||||
UserConnect bool `json:"user_connect" yaml:"user_connect"`
|
||||
ConsoleLogs bool `json:"console_logs" yaml:"console_logs"`
|
||||
PromptStyle string `json:"prompt" yaml:"prompt"`
|
||||
PromptTemplate string `json:"prompt_template" yaml:"prompt_template"`
|
||||
}
|
||||
|
||||
// LoadSettings - Load the client settings from disk
|
||||
func LoadSettings() (*ClientSettings, error) {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
data, err := os.ReadFile(filepath.Join(rootDir, settingsFileName))
|
||||
if err != nil {
|
||||
settingsPath := filepath.Join(rootDir, settingsFileName)
|
||||
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
|
||||
settings := defaultSettings()
|
||||
migratedLegacy := false
|
||||
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err == nil {
|
||||
if err = yaml.Unmarshal(data, settings); err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return defaultSettings(), err
|
||||
} else if data, err = os.ReadFile(legacyPath); err == nil {
|
||||
if err = json.Unmarshal(data, settings); err != nil {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
migratedLegacy = true
|
||||
} else if !os.IsNotExist(err) {
|
||||
return defaultSettings(), err
|
||||
}
|
||||
settings := defaultSettings()
|
||||
err = json.Unmarshal(data, settings)
|
||||
if err != nil {
|
||||
return defaultSettings(), err
|
||||
|
||||
// Ensure any missing/unknown values are coerced to a supported prompt style.
|
||||
settings.PromptStyle = NormalizePromptStyle(settings.PromptStyle)
|
||||
if err := SaveSettings(settings); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
if migratedLegacy {
|
||||
if err := renameLegacyConfig(legacyPath); err != nil {
|
||||
return settings, err
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// NormalizePromptStyle canonicalizes prompt style strings and returns a safe
|
||||
// default if the value is empty/unknown.
|
||||
func NormalizePromptStyle(v string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case PromptStyleOperatorHost:
|
||||
return PromptStyleOperatorHost
|
||||
case PromptStyleHost:
|
||||
return PromptStyleHost
|
||||
case PromptStyleBasic:
|
||||
return PromptStyleBasic
|
||||
case PromptStyleCustom:
|
||||
return PromptStyleCustom
|
||||
|
||||
// Backward compatible aliases.
|
||||
case "show host":
|
||||
return PromptStyleHost
|
||||
case "show user and host":
|
||||
return PromptStyleOperatorHost
|
||||
case "show operator and host":
|
||||
return PromptStyleOperatorHost
|
||||
case "operator@host":
|
||||
return PromptStyleOperatorHost
|
||||
case "user@host":
|
||||
return PromptStyleOperatorHost
|
||||
|
||||
case "":
|
||||
return PromptStyleHost
|
||||
default:
|
||||
return PromptStyleHost
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSettings() *ClientSettings {
|
||||
return &ClientSettings{
|
||||
TableStyle: "SliverDefault",
|
||||
@@ -64,16 +136,20 @@ func defaultSettings() *ClientSettings {
|
||||
AlwaysOverflow: false,
|
||||
VimMode: false,
|
||||
ConsoleLogs: true,
|
||||
PromptStyle: PromptStyleHost,
|
||||
PromptTemplate: DefaultPromptTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultPromptTemplate = `{{- if .IsServer -}}{{ .Styles.Bold.Render "[server]" }} {{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- else -}}{{- if .Host -}}{{ .Styles.BoldPrimary.Render (printf "[%s]" .Host) }} {{- end -}}{{ .Styles.Underline.Render "sliver" }}{{ .Target.Suffix }} > {{- end -}}`
|
||||
|
||||
// SaveSettings - Save the current settings to disk
|
||||
func SaveSettings(settings *ClientSettings) error {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
if settings == nil {
|
||||
settings = defaultSettings()
|
||||
}
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
data, err := yaml.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadSettingsParsesYAML(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
settingsPath := filepath.Join(rootDir, settingsFileName)
|
||||
|
||||
data := []byte(`tables: "Compact"
|
||||
autoadult: true
|
||||
beacon_autoresults: false
|
||||
small_term_width: 120
|
||||
always_overflow: true
|
||||
vim_mode: true
|
||||
user_connect: true
|
||||
console_logs: false
|
||||
prompt: "basic"
|
||||
prompt_template: "{{.Host}}"
|
||||
`)
|
||||
if err := os.WriteFile(settingsPath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write yaml settings: %v", err)
|
||||
}
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "Compact" {
|
||||
t.Fatalf("expected TableStyle %q, got %q", "Compact", settings.TableStyle)
|
||||
}
|
||||
if !settings.AutoAdult {
|
||||
t.Fatalf("expected AutoAdult true")
|
||||
}
|
||||
if settings.BeaconAutoResults {
|
||||
t.Fatalf("expected BeaconAutoResults false")
|
||||
}
|
||||
if settings.SmallTermWidth != 120 {
|
||||
t.Fatalf("expected SmallTermWidth %d, got %d", 120, settings.SmallTermWidth)
|
||||
}
|
||||
if !settings.AlwaysOverflow {
|
||||
t.Fatalf("expected AlwaysOverflow true")
|
||||
}
|
||||
if !settings.VimMode {
|
||||
t.Fatalf("expected VimMode true")
|
||||
}
|
||||
if !settings.UserConnect {
|
||||
t.Fatalf("expected UserConnect true")
|
||||
}
|
||||
if settings.ConsoleLogs {
|
||||
t.Fatalf("expected ConsoleLogs false")
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleBasic {
|
||||
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleBasic, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != "{{.Host}}" {
|
||||
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Host}}", settings.PromptTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsMigratesLegacyJSON(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
legacyPath := filepath.Join(rootDir, settingsLegacyFileName)
|
||||
|
||||
legacy := &ClientSettings{
|
||||
TableStyle: "Legacy",
|
||||
AutoAdult: true,
|
||||
BeaconAutoResults: false,
|
||||
SmallTermWidth: 200,
|
||||
AlwaysOverflow: true,
|
||||
VimMode: true,
|
||||
UserConnect: true,
|
||||
ConsoleLogs: false,
|
||||
PromptStyle: PromptStyleOperatorHost,
|
||||
PromptTemplate: "{{.Operator}}@{{.Host}} sliver > ",
|
||||
}
|
||||
data, err := json.MarshalIndent(legacy, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal legacy settings: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(legacyPath, data, 0o600); err != nil {
|
||||
t.Fatalf("failed to write legacy settings: %v", err)
|
||||
}
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "Legacy" {
|
||||
t.Fatalf("expected TableStyle %q, got %q", "Legacy", settings.TableStyle)
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleOperatorHost {
|
||||
t.Fatalf("expected PromptStyle %q, got %q", PromptStyleOperatorHost, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != "{{.Operator}}@{{.Host}} sliver > " {
|
||||
t.Fatalf("expected PromptTemplate %q, got %q", "{{.Operator}}@{{.Host}} sliver > ", settings.PromptTemplate)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected yaml settings to exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected legacy settings to be renamed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(legacyBackupPath(legacyPath)); err != nil {
|
||||
t.Fatalf("expected legacy backup to exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSettingsWritesDefault(t *testing.T) {
|
||||
t.Setenv(envVarName, t.TempDir())
|
||||
|
||||
settings, err := LoadSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if settings.TableStyle != "SliverDefault" {
|
||||
t.Fatalf("expected default TableStyle %q, got %q", "SliverDefault", settings.TableStyle)
|
||||
}
|
||||
if settings.PromptStyle != PromptStyleHost {
|
||||
t.Fatalf("expected default PromptStyle %q, got %q", PromptStyleHost, settings.PromptStyle)
|
||||
}
|
||||
if settings.PromptTemplate != DefaultPromptTemplate {
|
||||
t.Fatalf("expected default PromptTemplate %q, got %q", DefaultPromptTemplate, settings.PromptTemplate)
|
||||
}
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
if _, err := os.Stat(filepath.Join(rootDir, settingsFileName)); err != nil {
|
||||
t.Fatalf("expected default settings to be written: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package assets
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const spoofMetadataFileName = "spoof-metadata.yaml"
|
||||
|
||||
const defaultSpoofMetadataConfigTemplate = `# Sliver executable metadata spoofing configuration.
|
||||
# Used by generate commands when --spoof-metadata is set.
|
||||
#
|
||||
# Fields that reference file content support either:
|
||||
# - path: read bytes from a local file path
|
||||
# - base64: inline base64 file content
|
||||
# Do not set both path and base64 for the same object.
|
||||
#
|
||||
# Example 1: Minimal path-based metadata cloning (most common)
|
||||
pe:
|
||||
source:
|
||||
name: metadata-source.exe
|
||||
path: ""
|
||||
|
||||
# Optional icon donor. Reserved for future standalone icon mutation support.
|
||||
icon:
|
||||
name: icon.ico
|
||||
path: ""
|
||||
|
||||
# Example 2: Inline/base64 source (uncomment and remove "path" above)
|
||||
# source:
|
||||
# name: metadata-source.exe
|
||||
# base64: "TVqQAAMAAAAEAAAA..."
|
||||
|
||||
# Example 3: Optional PE structure overrides (advanced)
|
||||
# Applied after metadata/resources are cloned from source.
|
||||
# Omitted fields keep cloned values.
|
||||
# resource_directory:
|
||||
# characteristics: 0
|
||||
# time_date_stamp: 0
|
||||
# major_version: 1
|
||||
# minor_version: 0
|
||||
# number_of_named_entries: 0
|
||||
# number_of_id_entries: 0
|
||||
#
|
||||
# resource_directory_entries:
|
||||
# - name: 1
|
||||
# offset_to_data: 0
|
||||
#
|
||||
# resource_data_entries:
|
||||
# - offset_to_data: 0
|
||||
# size: 0
|
||||
# code_page: 0
|
||||
# reserved: 0
|
||||
#
|
||||
# export_directory:
|
||||
# characteristics: 0
|
||||
# time_date_stamp: 0
|
||||
# major_version: 1
|
||||
# minor_version: 0
|
||||
# name: 0
|
||||
# base: 0
|
||||
# number_of_functions: 0
|
||||
# number_of_names: 0
|
||||
# address_of_functions: 0
|
||||
# address_of_names: 0
|
||||
# address_of_name_ordinals: 0
|
||||
`
|
||||
|
||||
// SpoofMetadataConfig stores default metadata spoofing inputs for generate commands.
|
||||
type SpoofMetadataConfig struct {
|
||||
PE *PESpoofMetadataConfig `json:"pe,omitempty" yaml:"pe,omitempty"`
|
||||
}
|
||||
|
||||
// PESpoofMetadataConfig stores PE-specific spoofing inputs.
|
||||
type PESpoofMetadataConfig struct {
|
||||
Source *SpoofMetadataDataSource `json:"source,omitempty" yaml:"source,omitempty"`
|
||||
Icon *SpoofMetadataDataSource `json:"icon,omitempty" yaml:"icon,omitempty"`
|
||||
|
||||
ResourceDirectory *ImageResourceDirectory `json:"resource_directory,omitempty" yaml:"resource_directory,omitempty"`
|
||||
ResourceDirectoryEntries []*ImageResourceDirectoryEntry `json:"resource_directory_entries,omitempty" yaml:"resource_directory_entries,omitempty"`
|
||||
ResourceDataEntries []*ImageResourceDataEntry `json:"resource_data_entries,omitempty" yaml:"resource_data_entries,omitempty"`
|
||||
ExportDirectory *ImageExportDirectory `json:"export_directory,omitempty" yaml:"export_directory,omitempty"`
|
||||
}
|
||||
|
||||
// SpoofMetadataDataSource points to bytes that should be sent to the server.
|
||||
// Either Path or Base64 may be set.
|
||||
type SpoofMetadataDataSource struct {
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Path string `json:"path,omitempty" yaml:"path,omitempty"`
|
||||
Base64 string `json:"base64,omitempty" yaml:"base64,omitempty"`
|
||||
}
|
||||
|
||||
type ImageResourceDirectory struct {
|
||||
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
|
||||
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
|
||||
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
|
||||
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
|
||||
NumberOfNamedEntries uint32 `json:"number_of_named_entries,omitempty" yaml:"number_of_named_entries,omitempty"`
|
||||
NumberOfIDEntries uint32 `json:"number_of_id_entries,omitempty" yaml:"number_of_id_entries,omitempty"`
|
||||
}
|
||||
|
||||
type ImageResourceDirectoryEntry struct {
|
||||
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
|
||||
}
|
||||
|
||||
type ImageResourceDataEntry struct {
|
||||
OffsetToData uint32 `json:"offset_to_data,omitempty" yaml:"offset_to_data,omitempty"`
|
||||
Size uint32 `json:"size,omitempty" yaml:"size,omitempty"`
|
||||
CodePage uint32 `json:"code_page,omitempty" yaml:"code_page,omitempty"`
|
||||
Reserved uint32 `json:"reserved,omitempty" yaml:"reserved,omitempty"`
|
||||
}
|
||||
|
||||
type ImageExportDirectory struct {
|
||||
Characteristics uint32 `json:"characteristics,omitempty" yaml:"characteristics,omitempty"`
|
||||
TimeDateStamp uint32 `json:"time_date_stamp,omitempty" yaml:"time_date_stamp,omitempty"`
|
||||
MajorVersion uint32 `json:"major_version,omitempty" yaml:"major_version,omitempty"`
|
||||
MinorVersion uint32 `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
|
||||
Name uint32 `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Base uint32 `json:"base,omitempty" yaml:"base,omitempty"`
|
||||
NumberOfFunctions uint32 `json:"number_of_functions,omitempty" yaml:"number_of_functions,omitempty"`
|
||||
NumberOfNames uint32 `json:"number_of_names,omitempty" yaml:"number_of_names,omitempty"`
|
||||
AddressOfFunctions uint32 `json:"address_of_functions,omitempty" yaml:"address_of_functions,omitempty"`
|
||||
AddressOfNames uint32 `json:"address_of_names,omitempty" yaml:"address_of_names,omitempty"`
|
||||
AddressOfNameOrdinals uint32 `json:"address_of_name_ordinals,omitempty" yaml:"address_of_name_ordinals,omitempty"`
|
||||
}
|
||||
|
||||
// SpoofMetadataConfigPath returns the spoof metadata config file path.
|
||||
func SpoofMetadataConfigPath() string {
|
||||
rootDir, _ := filepath.Abs(GetRootAppDir())
|
||||
return filepath.Join(rootDir, spoofMetadataFileName)
|
||||
}
|
||||
|
||||
// LoadSpoofMetadataConfig loads spoof metadata config from disk, writing defaults
|
||||
// if the file is missing.
|
||||
func LoadSpoofMetadataConfig() (*SpoofMetadataConfig, error) {
|
||||
config := defaultSpoofMetadataConfig()
|
||||
configPath := SpoofMetadataConfigPath()
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return defaultSpoofMetadataConfig(), err
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte(defaultSpoofMetadataConfigTemplate), 0o600); err != nil {
|
||||
return defaultSpoofMetadataConfig(), err
|
||||
}
|
||||
data = []byte(defaultSpoofMetadataConfigTemplate)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, config); err != nil {
|
||||
return defaultSpoofMetadataConfig(), err
|
||||
}
|
||||
|
||||
normalizeSpoofMetadataConfig(config)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SaveSpoofMetadataConfig writes spoof metadata config to disk.
|
||||
func SaveSpoofMetadataConfig(config *SpoofMetadataConfig) error {
|
||||
if config == nil {
|
||||
config = defaultSpoofMetadataConfig()
|
||||
}
|
||||
normalizeSpoofMetadataConfig(config)
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(SpoofMetadataConfigPath(), data, 0o600)
|
||||
}
|
||||
|
||||
func normalizeSpoofMetadataConfig(config *SpoofMetadataConfig) {
|
||||
if config.PE == nil {
|
||||
config.PE = &PESpoofMetadataConfig{}
|
||||
}
|
||||
if config.PE.Source == nil {
|
||||
config.PE.Source = &SpoofMetadataDataSource{Name: "metadata-source.exe"}
|
||||
}
|
||||
if config.PE.Icon == nil {
|
||||
config.PE.Icon = &SpoofMetadataDataSource{Name: "icon.ico"}
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSpoofMetadataConfig() *SpoofMetadataConfig {
|
||||
return &SpoofMetadataConfig{
|
||||
PE: &PESpoofMetadataConfig{
|
||||
Source: &SpoofMetadataDataSource{
|
||||
Name: "metadata-source.exe",
|
||||
Path: "",
|
||||
},
|
||||
Icon: &SpoofMetadataDataSource{
|
||||
Name: "icon.ico",
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -1,4 +1,14 @@
|
||||
CLI
|
||||
===
|
||||
# client/cli
|
||||
|
||||
This package implements the Sliver client cli.
|
||||
## Overview
|
||||
|
||||
Defines the Cobra root command and CLI initialization for the Sliver client binary. Sets up persistent flags, environment bootstrapping, and command wiring. Core logic addresses console, implant, import, and version within the cli package.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `cli.go` – Builds the root Cobra command, wires global flags, and starts the console UI.
|
||||
- `config.go` – Parses CLI configuration files and environment variables used during startup.
|
||||
- `console.go` – Launches the interactive console mode and handles profile selection.
|
||||
- `implant.go` – Implements CLI entry points for implant-specific operations without launching the console.
|
||||
- `import.go` – Provides import routines for bringing external state into the client (e.g., implants or loot).
|
||||
- `version.go` – Prints version/build information for the CLI binary.
|
||||
+37
-3
@@ -19,6 +19,7 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -34,6 +35,8 @@ const (
|
||||
logFileName = "sliver-client.log"
|
||||
)
|
||||
|
||||
var clientLogFile *os.File
|
||||
|
||||
// Initialize logging.
|
||||
func initLogging(appDir string) *os.File {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
@@ -47,10 +50,14 @@ func initLogging(appDir string) *os.File {
|
||||
|
||||
func init() {
|
||||
appDir := assets.GetRootAppDir()
|
||||
logFile := initLogging(appDir)
|
||||
defer logFile.Close()
|
||||
clientLogFile = initLogging(appDir)
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
rootCmd.PersistentFlags().String(ConsoleConfigFlagName, "", "path to client config file")
|
||||
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
|
||||
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
|
||||
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
@@ -69,6 +76,9 @@ func init() {
|
||||
// other command trees below, such as the implant one.
|
||||
rootCmd.AddCommand(consoleCmd(con))
|
||||
|
||||
// MCP stdio server.
|
||||
rootCmd.AddCommand(mcpCmd(con))
|
||||
|
||||
// Implant.
|
||||
// The implant command allows users to run commands on slivers from their
|
||||
// system shell. It makes use of pre-runners for connecting to the server
|
||||
@@ -89,10 +99,34 @@ var rootCmd = &cobra.Command{
|
||||
Long: ``,
|
||||
}
|
||||
|
||||
type commandExitError struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *commandExitError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e *commandExitError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func commandExitCode(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if exitErr, ok := errors.AsType[*commandExitError](err); ok {
|
||||
return exitErr.code
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// Execute - Execute root command.
|
||||
func Execute() {
|
||||
defer func() { _ = clientLogFile.Close() }()
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Printf("root command: %s\n", err)
|
||||
os.Exit(1)
|
||||
os.Exit(commandExitCode(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestPackageLoggerRemainsOpenAfterInit(t *testing.T) {
|
||||
logFile, ok := log.Writer().(*os.File)
|
||||
if !ok {
|
||||
t.Fatalf("standard logger writer = %T, want *os.File", log.Writer())
|
||||
}
|
||||
if want := filepath.Join(assets.GetRootAppDir(), logFileName); filepath.Clean(logFile.Name()) != want {
|
||||
t.Fatalf("standard logger file = %q, want %q", logFile.Name(), want)
|
||||
}
|
||||
if _, err := logFile.Stat(); err != nil {
|
||||
t.Fatalf("standard logger file is closed after package initialization: %v", err)
|
||||
}
|
||||
}
|
||||
+27
-29
@@ -20,51 +20,49 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() *assets.ClientConfig {
|
||||
configs := assets.GetConfigs()
|
||||
func selectConfig(configPath string, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if configPath != "" {
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("load client config: %w", err)
|
||||
}
|
||||
return filepath.Base(configPath), config, nil
|
||||
}
|
||||
return selectConfigFrom(assets.GetConfigs(), stdinTTY)
|
||||
}
|
||||
|
||||
func selectConfigFrom(configs map[string]*assets.ClientConfig, stdinTTY bool) (string, *assets.ClientConfig, error) {
|
||||
if len(configs) == 0 {
|
||||
return nil
|
||||
return "", nil, fmt.Errorf("no config files found at %s", assets.GetConfigDir())
|
||||
}
|
||||
|
||||
if len(configs) == 1 {
|
||||
for _, config := range configs {
|
||||
return config
|
||||
for key, config := range configs {
|
||||
return key, config, nil
|
||||
}
|
||||
}
|
||||
|
||||
answer := struct{ Config string }{}
|
||||
qs := getPromptForConfigs(configs)
|
||||
err := survey.Ask(qs, &answer)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil
|
||||
if !stdinTTY {
|
||||
return "", nil, fmt.Errorf("multiple configs found; use --config to select one")
|
||||
}
|
||||
|
||||
return configs[answer.Config]
|
||||
}
|
||||
|
||||
func getPromptForConfigs(configs map[string]*assets.ClientConfig) []*survey.Question {
|
||||
keys := []string{}
|
||||
for k := range configs {
|
||||
keys = append(keys, k)
|
||||
keys := make([]string, 0, len(configs))
|
||||
for key := range configs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return []*survey.Question{
|
||||
{
|
||||
Name: "config",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Select a server:",
|
||||
Options: keys,
|
||||
Default: keys[0],
|
||||
},
|
||||
},
|
||||
selection := keys[0]
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return selection, configs[selection], nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigFlagRoutesBeforeAndAfterConsoleAndMCP(t *testing.T) {
|
||||
flag := rootCmd.PersistentFlags().Lookup(ConsoleConfigFlagName)
|
||||
if flag == nil {
|
||||
t.Fatal("root --config flag is missing")
|
||||
}
|
||||
oldValue, oldChanged := flag.Value.String(), flag.Changed
|
||||
t.Cleanup(func() {
|
||||
_ = flag.Value.Set(oldValue)
|
||||
flag.Changed = oldChanged
|
||||
})
|
||||
|
||||
for _, command := range []string{"console", "mcp"} {
|
||||
for _, before := range []bool{true, false} {
|
||||
name := command + " after --config"
|
||||
args := []string{"--config", "selected.cfg", command}
|
||||
if !before {
|
||||
name = command + " before --config"
|
||||
args = []string{command, "--config", "selected.cfg"}
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_ = flag.Value.Set("")
|
||||
flag.Changed = false
|
||||
cmd, remaining, err := rootCmd.Traverse(args)
|
||||
if err != nil {
|
||||
t.Fatalf("traverse %q: %v", args, err)
|
||||
}
|
||||
if cmd.Name() != command {
|
||||
t.Fatalf("command = %q, want %q", cmd.Name(), command)
|
||||
}
|
||||
if err := cmd.ParseFlags(remaining); err != nil {
|
||||
t.Fatalf("parse %q: %v", remaining, err)
|
||||
}
|
||||
got, err := cmd.Flags().GetString(ConsoleConfigFlagName)
|
||||
if err != nil || got != "selected.cfg" {
|
||||
t.Fatalf("--config = %q, error = %v", got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
func TestSelectConfigFromNonTTYRequiresExplicitConfigWhenMultiple(t *testing.T) {
|
||||
configs := map[string]*assets.ClientConfig{
|
||||
"first": {Operator: "first"},
|
||||
"second": {Operator: "second"},
|
||||
}
|
||||
key, config, err := selectConfigFrom(configs, false)
|
||||
if err == nil || !strings.Contains(err.Error(), "--config") {
|
||||
t.Fatalf("error = %v, want --config guidance", err)
|
||||
}
|
||||
if key != "" || config != nil {
|
||||
t.Fatalf("selected %q (%v) without an explicit config", key, config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigFromNonTTYUsesSoleConfig(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "only"}
|
||||
key, config, err := selectConfigFrom(map[string]*assets.ClientConfig{"only": want}, false)
|
||||
if err != nil || key != "only" || config != want {
|
||||
t.Fatalf("selection = %q (%v), error = %v", key, config, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectConfigWithExplicitPathOnNonTTY(t *testing.T) {
|
||||
want := &assets.ClientConfig{Operator: "selected", LHost: "127.0.0.1"}
|
||||
data, err := json.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "selected.cfg")
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key, config, err := selectConfig(path, false)
|
||||
if err != nil {
|
||||
t.Fatalf("select explicit config: %v", err)
|
||||
}
|
||||
if key != "selected.cfg" || config.Operator != want.Operator || config.LHost != want.LHost {
|
||||
t.Fatalf("selection = %q (%v)", key, config)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
bspinner "charm.land/bubbles/v2/spinner"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
minConnectSpinnerDuration = 900 * time.Millisecond
|
||||
minConnectStatusDuration = 350 * time.Millisecond
|
||||
)
|
||||
|
||||
type connectResult struct {
|
||||
rpc rpcpb.SliverRPCClient
|
||||
conn *grpc.ClientConn
|
||||
err error
|
||||
}
|
||||
|
||||
func connectWithSpinner(out io.Writer, target string, connect func(transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error)) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusCh := make(chan string, 8)
|
||||
resultCh := make(chan connectResult, 1)
|
||||
|
||||
go func() {
|
||||
rpc, conn, err := connect(func(status string) {
|
||||
sendStatus(statusCh, status)
|
||||
})
|
||||
resultCh <- connectResult{rpc: rpc, conn: conn, err: err}
|
||||
}()
|
||||
|
||||
currentStatus := ""
|
||||
currentStatusSince := time.Time{}
|
||||
queuedStatus := ""
|
||||
lastWidth := 0
|
||||
spinner := bspinner.New(bspinner.WithSpinner(bspinner.Line))
|
||||
ticker := time.NewTicker(spinner.Spinner.FPS)
|
||||
defer ticker.Stop()
|
||||
startedAt := time.Now()
|
||||
var pendingResult *connectResult
|
||||
|
||||
setStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" || status == currentStatus {
|
||||
return
|
||||
}
|
||||
currentStatus = status
|
||||
currentStatusSince = time.Now()
|
||||
}
|
||||
|
||||
stageStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
if currentStatus == "" || time.Since(currentStatusSince) >= minConnectStatusDuration {
|
||||
queuedStatus = ""
|
||||
setStatus(status)
|
||||
return
|
||||
}
|
||||
queuedStatus = status
|
||||
}
|
||||
|
||||
flushQueuedStatus := func() {
|
||||
if queuedStatus == "" || time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return
|
||||
}
|
||||
nextStatus := queuedStatus
|
||||
queuedStatus = ""
|
||||
setStatus(nextStatus)
|
||||
}
|
||||
|
||||
canReturnPendingResult := func() bool {
|
||||
if pendingResult == nil {
|
||||
return false
|
||||
}
|
||||
if pendingResult.err != nil {
|
||||
return true
|
||||
}
|
||||
if time.Since(startedAt) < minConnectSpinnerDuration {
|
||||
return false
|
||||
}
|
||||
if queuedStatus != "" {
|
||||
return false
|
||||
}
|
||||
if !currentStatusSince.IsZero() && time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
render := func() {
|
||||
line := fmt.Sprintf("%s %s", spinner.View(), formatConnectSpinnerMessage(target, currentStatus))
|
||||
lastWidth = writeSpinnerLine(out, line, lastWidth)
|
||||
}
|
||||
|
||||
render()
|
||||
for {
|
||||
select {
|
||||
case status := <-statusCh:
|
||||
stageStatus(status)
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
render()
|
||||
|
||||
case result := <-resultCh:
|
||||
pendingResult = &result
|
||||
|
||||
case <-ticker.C:
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
flushQueuedStatus()
|
||||
render()
|
||||
if canReturnPendingResult() {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
|
||||
if pendingResult != nil && pendingResult.err != nil {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendStatus(statusCh chan string, status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
statusCh <- status
|
||||
}
|
||||
|
||||
func formatConnectSpinnerMessage(target string, status string) string {
|
||||
target = strings.TrimSpace(target)
|
||||
status = strings.TrimSpace(status)
|
||||
|
||||
if target == "" {
|
||||
if status == "" {
|
||||
return "Connecting ..."
|
||||
}
|
||||
return fmt.Sprintf("Connecting (%s) ...", status)
|
||||
}
|
||||
if status == "" {
|
||||
return fmt.Sprintf("Connecting to %s ...", target)
|
||||
}
|
||||
return fmt.Sprintf("Connecting to %s (%s) ...", target, status)
|
||||
}
|
||||
|
||||
func writeSpinnerLine(out io.Writer, line string, lastWidth int) int {
|
||||
width := utf8.RuneCountInString(line)
|
||||
padding := ""
|
||||
if lastWidth > width {
|
||||
padding = strings.Repeat(" ", lastWidth-width)
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s%s", line, padding)
|
||||
if width > lastWidth {
|
||||
return width
|
||||
}
|
||||
return lastWidth
|
||||
}
|
||||
|
||||
func clearSpinnerLine(out io.Writer, lastWidth int) {
|
||||
if lastWidth <= 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s\r", strings.Repeat(" ", lastWidth))
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestFormatConnectSpinnerMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
status string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "target only",
|
||||
target: "127.0.0.1:31337",
|
||||
want: "Connecting to 127.0.0.1:31337 ...",
|
||||
},
|
||||
{
|
||||
name: "target with status",
|
||||
target: "127.0.0.1:31337",
|
||||
status: "wireguard",
|
||||
want: "Connecting to 127.0.0.1:31337 (wireguard) ...",
|
||||
},
|
||||
{
|
||||
name: "status without target",
|
||||
status: "grpc/mtls",
|
||||
want: "Connecting (grpc/mtls) ...",
|
||||
},
|
||||
{
|
||||
name: "trim whitespace",
|
||||
target: " 127.0.0.1:31337 ",
|
||||
status: " grpc/mtls over wireguard ",
|
||||
want: "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := formatConnectSpinnerMessage(test.target, test.status); got != test.want {
|
||||
t.Fatalf("expected %q, got %q", test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerOutput(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerOutput)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerOutput(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
spinnerDelay := 2 * (100 * time.Millisecond)
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("wireguard")
|
||||
time.Sleep(spinnerDelay)
|
||||
statusFn("grpc/mtls over wireguard")
|
||||
time.Sleep(spinnerDelay)
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||
t.Fatalf("expected wireguard status in output, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||
t.Fatalf("expected grpc over wireguard status in output, got %q", got)
|
||||
}
|
||||
frameCount := 0
|
||||
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337") {
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
if frameCount < 2 {
|
||||
t.Fatalf("expected multiple spinner frames in output, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerFastSuccessStillShowsMultipleFrames)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerFastSuccessStillShowsMultipleFrames(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("grpc/mtls")
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
frameCount := 0
|
||||
for _, frame := range []string{"|", "/", "-", "\\"} {
|
||||
if strings.Contains(got, frame+" Connecting to 127.0.0.1:31337 (grpc/mtls) ...") {
|
||||
frameCount++
|
||||
}
|
||||
}
|
||||
if frameCount < 2 {
|
||||
t.Fatalf("expected multiple spinner frames for fast success, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
|
||||
synctest.Test(t, testConnectWithSpinnerFastSuccessShowsEachStatus)
|
||||
}
|
||||
|
||||
func testConnectWithSpinnerFastSuccessShowsEachStatus(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
|
||||
_, _, err := connectWithSpinner(&out, "127.0.0.1:31337", func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusFn("wireguard")
|
||||
statusFn("grpc/mtls over wireguard")
|
||||
return nil, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("connectWithSpinner returned error: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (wireguard) ...") {
|
||||
t.Fatalf("expected fast success output to include wireguard status, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Connecting to 127.0.0.1:31337 (grpc/mtls over wireguard) ...") {
|
||||
t.Fatalf("expected fast success output to include grpc over wireguard status, got %q", got)
|
||||
}
|
||||
if strings.Contains(got, "Connected to 127.0.0.1:31337") {
|
||||
t.Fatalf("did not expect past-tense success output, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
func handleConnectionLost(ln *grpc.ClientConn) {
|
||||
if ln == nil {
|
||||
return
|
||||
}
|
||||
currentState := ln.GetState()
|
||||
// currentState should be "Ready" when the connection is established.
|
||||
if ln.WaitForStateChange(context.Background(), currentState) {
|
||||
newState := ln.GetState()
|
||||
// newState will be "Idle" if the connection is lost.
|
||||
if newState == connectivity.Idle {
|
||||
fmt.Println("\nLost connection to server. Exiting now.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-46
@@ -19,20 +19,21 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// ConsoleConfigFlagName is the flag used to select a client config file.
|
||||
const ConsoleConfigFlagName = "config"
|
||||
|
||||
// consoleCmd generates the console with required pre/post runners.
|
||||
func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
consoleCmd := &cobra.Command{
|
||||
@@ -40,66 +41,61 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
Short: "Start the sliver client console",
|
||||
}
|
||||
|
||||
consoleCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
consoleCmd.RunE, consoleCmd.PersistentPostRunE = consoleRunnerCmd(con, true)
|
||||
return consoleCmd
|
||||
}
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
pre = func(_ *cobra.Command, _ []string) error {
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
config := selectConfig()
|
||||
if config == nil {
|
||||
return nil
|
||||
|
||||
rcScript, err := ReadRCScript(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := ""
|
||||
if cmd.Flags().Lookup(ConsoleConfigFlagName) != nil {
|
||||
configPath, err = cmd.Flags().GetString(ConsoleConfigFlagName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stdinTTY := term.IsTerminal(int(os.Stdin.Fd()))
|
||||
configKey, config, err := selectConfig(configPath, stdinTTY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
if run && stdinTTY {
|
||||
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
return transport.MTLSConnectWithStatus(config, statusFn)
|
||||
})
|
||||
} else {
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
}
|
||||
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var err error
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
return nil
|
||||
return fmt.Errorf("connection to server failed: %w", err)
|
||||
}
|
||||
|
||||
// Wait for any connection state changes and exit if the connection is lost.
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run)
|
||||
if err := console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript); err != nil {
|
||||
_ = con.CloseConnection()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close the RPC connection once exiting
|
||||
post = func(_ *cobra.Command, _ []string) error {
|
||||
if ln != nil {
|
||||
return ln.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
return con.CloseConnection()
|
||||
}
|
||||
|
||||
return pre, post
|
||||
}
|
||||
|
||||
func handleConnectionLost(ln *grpc.ClientConn) {
|
||||
currentState := ln.GetState()
|
||||
// currentState should be "Ready" when the connection is established.
|
||||
if ln.WaitForStateChange(context.Background(), currentState) {
|
||||
newState := ln.GetState()
|
||||
// newState will be "Idle" if the connection is lost.
|
||||
if newState == connectivity.Idle {
|
||||
fmt.Println("\nLost connection to server. Exiting now.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,9 @@ func makeRunners(implantCmd *cobra.Command, con *console.SliverClient) (pre, pos
|
||||
// The pre-run function connects to the server and sets up a "fake" console,
|
||||
// so we can have access to active sessions/beacons, and other stuff needed.
|
||||
pre = func(_ *cobra.Command, args []string) error {
|
||||
startConsole(implantCmd, args)
|
||||
if err := startConsole(implantCmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the active target.
|
||||
target, _ := implantCmd.Flags().GetString("use")
|
||||
|
||||
+21
-13
@@ -20,7 +20,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/rsteube/carapace"
|
||||
@@ -32,19 +31,12 @@ func importCmd() *cobra.Command {
|
||||
Use: "import",
|
||||
Short: "Import a client configuration file",
|
||||
Long: `import [config files]`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if 0 < len(args) {
|
||||
for _, arg := range args {
|
||||
conf, err := assets.ReadConfig(arg)
|
||||
if err != nil {
|
||||
fmt.Printf("[!] %s\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
assets.SaveConfig(conf)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Missing config file path, see --help")
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Missing config file path, see --help")
|
||||
return nil
|
||||
}
|
||||
return importConfigs(args)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{}, cobra.ShellCompDirectiveDefault
|
||||
@@ -55,3 +47,19 @@ func importCmd() *cobra.Command {
|
||||
|
||||
return cmdImport
|
||||
}
|
||||
|
||||
func importConfigs(paths []string) error {
|
||||
for _, path := range paths {
|
||||
conf, err := assets.ReadConfig(path)
|
||||
if err != nil {
|
||||
return &commandExitError{
|
||||
code: 3,
|
||||
err: fmt.Errorf("read import %q: %w", path, err),
|
||||
}
|
||||
}
|
||||
if err := assets.SaveConfig(conf); err != nil {
|
||||
return fmt.Errorf("save import %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeImportConfig(t *testing.T, name, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), name)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestImportCommandReportsInvalidConfig(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "missing-operator.cfg", `{"lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("invalid config exit code = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReportsConfigWriteFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
path := writeImportConfig(t, "alice.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
configDir := filepath.Join(clientRoot, "configs")
|
||||
if err := os.MkdirAll(filepath.Join(configDir, "alice_server.cfg"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want write error identifying %q", err, path)
|
||||
}
|
||||
if got := commandExitCode(err); got != 1 {
|
||||
t.Fatalf("config write failure exit code = %d, want 1", got)
|
||||
}
|
||||
if _, ok := errors.AsType[*os.PathError](err); !ok {
|
||||
t.Fatalf("config write failure does not wrap the filesystem error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandReadFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := filepath.Join(t.TempDir(), "missing.cfg")
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want read error identifying %q", err, path)
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("import error does not wrap the missing file error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(fmt.Errorf("command failed: %w", err)); got != 3 {
|
||||
t.Fatalf("wrapped read failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandParseFailureExitCode(t *testing.T) {
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
path := writeImportConfig(t, "malformed.cfg", `{"operator":`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{path})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(path)) {
|
||||
t.Fatalf("import error = %v, want parse error identifying %q", err, path)
|
||||
}
|
||||
if _, ok := errors.AsType[*json.SyntaxError](err); !ok {
|
||||
t.Fatalf("import error does not wrap the JSON syntax error: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 3 {
|
||||
t.Fatalf("parse failure exit code = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandStopsAfterFirstFailure(t *testing.T) {
|
||||
clientRoot := t.TempDir()
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", clientRoot)
|
||||
invalidPath := writeImportConfig(t, "invalid.cfg", `{"lhost":"server"}`)
|
||||
validPath := writeImportConfig(t, "valid.cfg", `{"operator":"alice","lhost":"server"}`)
|
||||
|
||||
cmd := importCmd()
|
||||
err := cmd.RunE(cmd, []string{invalidPath, validPath})
|
||||
if err == nil || !strings.Contains(err.Error(), strconv.Quote(invalidPath)) {
|
||||
t.Fatalf("import error = %v, want first file %q", err, invalidPath)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(clientRoot, "configs", "alice_server.cfg")); !os.IsNotExist(err) {
|
||||
t.Fatalf("later config was imported after first failure: stat error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportCommandMissingPathPreservesSuccess(t *testing.T) {
|
||||
cmd := importCmd()
|
||||
var output bytes.Buffer
|
||||
cmd.SetOut(&output)
|
||||
err := cmd.RunE(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("import without a path failed: %v", err)
|
||||
}
|
||||
if got := commandExitCode(err); got != 0 {
|
||||
t.Fatalf("missing path exit code = %d, want 0", got)
|
||||
}
|
||||
if got, want := output.String(), "Missing config file path, see --help"; got != want {
|
||||
t.Fatalf("missing path output = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cli
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2023 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
clientmcp "github.com/bishopfox/sliver/client/mcp"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
mcpCmd := &cobra.Command{
|
||||
Use: "mcp",
|
||||
Short: "Start the MCP stdio server",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runMCP(cmd, con)
|
||||
},
|
||||
}
|
||||
return mcpCmd
|
||||
}
|
||||
|
||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
rpc, ln, err := transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
defer transport.CloseGRPCConnection(ln)
|
||||
|
||||
con.Rpc = rpc
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
cfg := clientmcp.DefaultConfig()
|
||||
if err := clientmcp.ServeStdio(cfg, con.Rpc); err != nil {
|
||||
fmt.Printf("MCP server error: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadMCPClientConfig(path string) (*assets.ClientConfig, error) {
|
||||
if path != "" {
|
||||
configPath := path
|
||||
if !filepath.IsAbs(configPath) {
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
configPath = filepath.Join(assets.GetConfigDir(), configPath)
|
||||
}
|
||||
}
|
||||
return assets.ReadConfig(configPath)
|
||||
}
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
switch len(configs) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("no config files found at %s (use --config)", assets.GetConfigDir())
|
||||
case 1:
|
||||
for _, config := range configs {
|
||||
return config, nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("multiple configs found; use --config to select one")
|
||||
}
|
||||
return nil, fmt.Errorf("no config files found")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const RCFlagName = "rc"
|
||||
|
||||
func ReadRCScript(cmd *cobra.Command) (string, error) {
|
||||
if cmd == nil {
|
||||
return "", nil
|
||||
}
|
||||
if cmd.Flags().Lookup(RCFlagName) == nil {
|
||||
return "", nil
|
||||
}
|
||||
rcPath, err := cmd.Flags().GetString(RCFlagName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if rcPath == "" {
|
||||
return "", nil
|
||||
}
|
||||
data, err := os.ReadFile(rcPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read rc script %q: %w", rcPath, err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
enableWGFlag = "enable-wg"
|
||||
disableWGFlag = "disable-wg"
|
||||
)
|
||||
|
||||
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
|
||||
if cmd == nil {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
return nil
|
||||
}
|
||||
|
||||
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Flags().Changed(enableWGFlag) && cmd.Flags().Changed(disableWGFlag) {
|
||||
return fmt.Errorf("--%s and --%s cannot be used together", enableWGFlag, disableWGFlag)
|
||||
}
|
||||
|
||||
mode := transport.MultiplayerConnectAuto
|
||||
switch {
|
||||
case enableWG:
|
||||
mode = transport.MultiplayerConnectEnableWG
|
||||
case disableWG:
|
||||
mode = transport.MultiplayerConnectDisableWG
|
||||
}
|
||||
transport.SetMultiplayerConnectMode(mode)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestWireGuardOverrideFlagsDefaultToFalse(t *testing.T) {
|
||||
for _, name := range []string{enableWGFlag, disableWGFlag} {
|
||||
flag := rootCmd.PersistentFlags().Lookup(name)
|
||||
if flag == nil {
|
||||
t.Fatalf("expected --%s persistent flag", name)
|
||||
}
|
||||
if flag.DefValue != "false" {
|
||||
t.Fatalf("expected --%s to default to false, got %q", name, flag.DefValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to honor an explicitly enabled WireGuard config")
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to ignore a legacy WireGuard config without the opt-in marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeEnableWGForcesLegacyConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected --enable-wg to force a complete legacy config through WireGuard")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDisableWGOverridesEnabledConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected --disable-wg to force direct mTLS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsConflictingOverrides(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected conflicting WireGuard overrides to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsBothOverridesWhenExplicitlyFalse(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected explicitly combining both WireGuard override flags to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTransportModeTestCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool(enableWGFlag, false, "")
|
||||
cmd.Flags().Bool(disableWGFlag, false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func resetMultiplayerConnectModeForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
t.Cleanup(func() {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
})
|
||||
}
|
||||
|
||||
func enabledWGClientConfig() *assets.ClientConfig {
|
||||
config := legacyWGClientConfig()
|
||||
config.WG.Enabled = true
|
||||
return config
|
||||
}
|
||||
|
||||
func legacyWGClientConfig() *assets.ClientConfig {
|
||||
return &assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientPrivateKey: "client-priv",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
+65
-11
@@ -1,14 +1,68 @@
|
||||
Command
|
||||
=======
|
||||
# client/command
|
||||
|
||||
This package contains all of the client command implementations that are shared between the client and the server console.
|
||||
## Overview
|
||||
|
||||
### Developers
|
||||
Shared registration glue, scaffolding, and helpers for Sliver client console command groups. Hosts common utilities for binding Cobra commands to RPC-backed actions. Core logic addresses sliver within the command package.
|
||||
|
||||
General guidance on the structure of this package:
|
||||
* One command per file and the name of the file should reflect the command it implements (or there about, trivial commands can go into the same file). This makes it easy to find where a command implementation lives within a package or to search by file name if it is not clear which package implements a given command.
|
||||
* The root command, and reused code should go into the file named after the command
|
||||
* For example, code shared between the `generate` and the `regenerate` command should go in `generate.go`, and any `regenerate` specific code should go in `regenerate.go`
|
||||
* Command entrypoint functions should have the suffix `Cmd` e.g., `GenerateCmd` is the entrypoint for `generate`
|
||||
* Command entrypoints should always a function signature of `func (ctx *grumble.Context, con *console.SliverConsoleClient)`
|
||||
* Functions that are only ever exported for other commands to use should go in a `helpers.go`, if the function is used internally and exported follow the guidance above on shared code.
|
||||
## Go Files
|
||||
|
||||
- `command.go` – Provides helpers for binding Cobra commands, flag sets, and visibility filters into the client command tree.
|
||||
- `server.go` – Defines the server menu composition, wiring each server-side command group onto the root console structure at runtime.
|
||||
- `main.go` – Builds the implant/Sliver menu, attaching per-target command groups and loading alias/extension manifests before exposure.
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
|
||||
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||
- `armory/` – Implements the 'armory' command group for the Sliver client console.
|
||||
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
|
||||
- `beacons/` – Implements the 'beacons' command group for the Sliver client console.
|
||||
- `builders/` – Implements the 'builders' command group for the Sliver client console.
|
||||
- `c2profiles/` – Implements the 'c2profiles' command group for the Sliver client console.
|
||||
- `certificates/` – Implements the 'certificates' command group for the Sliver client console.
|
||||
- `clean/` – Implements the 'clean' command group for the Sliver client console.
|
||||
- `completers/` – Implements the 'completers' command group for the Sliver client console.
|
||||
- `crack/` – Implements the 'crack' command group for the Sliver client console.
|
||||
- `creds/` – Implements the 'creds' command group for the Sliver client console.
|
||||
- `cursed/` – Implements the 'cursed' command group for the Sliver client console.
|
||||
- `dllhijack/` – Implements the 'dllhijack' command group for the Sliver client console.
|
||||
- `edit/` – Implements the 'edit' command for the Sliver client console.
|
||||
- `environment/` – Implements the 'environment' command group for the Sliver client console.
|
||||
- `exec/` – Implements the 'exec' command group for the Sliver client console.
|
||||
- `exit/` – Implements the 'exit' command group for the Sliver client console.
|
||||
- `extensions/` – Implements the 'extensions' command group for the Sliver client console.
|
||||
- `filesystem/` – Implements the 'filesystem' command group for the Sliver client console.
|
||||
- `flags/` – Implements the 'flags' command group for the Sliver client console.
|
||||
- `generate/` – Implements the 'generate' command group for the Sliver client console.
|
||||
- `help/` – Implements the 'help' command group for the Sliver client console.
|
||||
- `hosts/` – Implements the 'hosts' command group for the Sliver client console.
|
||||
- `info/` – Implements the 'info' command group for the Sliver client console.
|
||||
- `jobs/` – Implements the 'jobs' command group for the Sliver client console.
|
||||
- `kill/` – Implements the 'kill' command group for the Sliver client console.
|
||||
- `licenses/` – Implements the 'licenses' command group for the Sliver client console.
|
||||
- `loot/` – Implements the 'loot' command group for the Sliver client console.
|
||||
- `monitor/` – Implements the 'monitor' command group for the Sliver client console.
|
||||
- `mcp/` – Implements the 'mcp' command group for the Sliver client console.
|
||||
- `network/` – Implements the 'network' command group for the Sliver client console.
|
||||
- `operators/` – Implements the 'operators' command group for the Sliver client console.
|
||||
- `pivots/` – Implements the 'pivots' command group for the Sliver client console.
|
||||
- `portfwd/` – Implements the 'portfwd' command group for the Sliver client console.
|
||||
- `privilege/` – Implements the 'privilege' command group for the Sliver client console.
|
||||
- `processes/` – Implements the 'processes' command group for the Sliver client console.
|
||||
- `reaction/` – Implements the 'reaction' command group for the Sliver client console.
|
||||
- `reconfig/` – Implements the 'reconfig' command group for the Sliver client console.
|
||||
- `registry/` – Implements the 'registry' command group for the Sliver client console.
|
||||
- `rportfwd/` – Implements the 'rportfwd' command group for the Sliver client console.
|
||||
- `screenshot/` – Implements the 'screenshot' command group for the Sliver client console.
|
||||
- `sessions/` – Implements the 'sessions' command group for the Sliver client console.
|
||||
- `settings/` – Implements the 'settings' command group for the Sliver client console.
|
||||
- `shell/` – Implements the 'shell' command group for the Sliver client console.
|
||||
- `shikata-ga-nai/` – Implements the 'shikata-ga-nai' command group for the Sliver client console.
|
||||
- `socks/` – Implements the 'socks' command group for the Sliver client console.
|
||||
- `taskmany/` – Implements the 'taskmany' command group for the Sliver client console.
|
||||
- `tasks/` – Implements the 'tasks' command group for the Sliver client console.
|
||||
- `update/` – Implements the 'update' command group for the Sliver client console.
|
||||
- `use/` – Implements the 'use' command group for the Sliver client console.
|
||||
- `wasm/` – Implements the 'wasm' command group for the Sliver client console.
|
||||
- `websites/` – Implements the 'websites' command group for the Sliver client console.
|
||||
- `wireguard/` – Implements the 'wireguard' command group for the Sliver client console.
|
||||
@@ -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,23 @@
|
||||
# client/command/aka
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'aka' command group for the Sliver client console. "Aka"s are
|
||||
command aliases (like 'alias' in 'bash') that allow operators to specify
|
||||
shorthand commands and default arguments for commonly used commands.
|
||||
|
||||
### Examples:
|
||||
|
||||
- `aka create bg background`: Allows an operator to run `bg` from the implant
|
||||
menu to quickly move back to the server menu
|
||||
- `aka create execw execute -o "C:\Windows\System32\cmd.exe" /c`: Shorthand for
|
||||
executing binaries on Windows targets like: `execw arp -a`
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Wires Cobra commands for listing, creating, and deleting command aliases in the client console.
|
||||
- `create.go` – Handles creating command aliases to use by the client.
|
||||
- `delete.go` – Removes command aliases currently tracked by the client.
|
||||
- `hook.go` – The main hooking logic to intercept commands ran by the client console. The hook will intercept processing before hitting the Cobra tree to transform the command alias to a full command with default arguments from the command alias and user-provided arguments to the alias.
|
||||
- `list.go` - Lists the current command aliases tracked by the client.
|
||||
- `storage.go` - Supports the saving of aliases to disk to keep between runs of the client.
|
||||
@@ -0,0 +1,59 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return commands(con, constants.GenericHelpGroup)
|
||||
}
|
||||
|
||||
func ImplantCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return commands(con, constants.SliverCoreHelpGroup)
|
||||
}
|
||||
|
||||
func commands(con *console.SliverClient, group string) []*cobra.Command {
|
||||
akaCommand := &cobra.Command{
|
||||
Use: "aka",
|
||||
Short: "Manage command aliases (aka's)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AkaListCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: group,
|
||||
}
|
||||
|
||||
createAka := &cobra.Command{
|
||||
Use: "create <alias> <command> [args...]",
|
||||
Short: "Create a new command alias (aka)",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
DisableFlagParsing: true, // No flags with aka create so we capture flags to other commands
|
||||
DisableFlagsInUseLine: true,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AkaCreateCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
createAka.InitDefaultHelpFlag()
|
||||
createAka.Flags().Lookup("help").Hidden = true
|
||||
akaCommand.AddCommand(createAka)
|
||||
|
||||
deleteAka := &cobra.Command{
|
||||
Use: "delete <alias>",
|
||||
Short: "Delete a command alias (aka)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AkaDeleteCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
akaCommand.AddCommand(deleteAka)
|
||||
|
||||
con.App.PreCmdRunLineHooks = append(con.App.PreCmdRunLineHooks, Cmdhook)
|
||||
|
||||
err := LoadAkaAliases()
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to load aka alises: %s\n", err)
|
||||
}
|
||||
|
||||
return []*cobra.Command{akaCommand}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AkaCreateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
aliasName := args[0]
|
||||
command := args[1]
|
||||
defaultArgs := args[2:] // anything else is going to be the default args to the alias
|
||||
|
||||
if _, exists := akaAliases[aliasName]; exists {
|
||||
con.PrintErrorf("Alias '%s' already exists\n", aliasName)
|
||||
return
|
||||
}
|
||||
|
||||
var desc string
|
||||
if len(defaultArgs) == 0 {
|
||||
desc = fmt.Sprintf("%s", command)
|
||||
} else {
|
||||
desc = fmt.Sprintf("%s %s", command, strings.Join(defaultArgs, " "))
|
||||
}
|
||||
|
||||
alias := &AkaAlias{
|
||||
Name: aliasName,
|
||||
Command: command,
|
||||
DefaultArgs: defaultArgs,
|
||||
Description: desc,
|
||||
}
|
||||
|
||||
akaAliases[aliasName] = alias
|
||||
|
||||
// save this new alias to disk
|
||||
err := SaveAkaAliases()
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save alias '%s' to disk: %s\n", aliasName, err)
|
||||
// we still technically added it to the map, so the alias did get created in
|
||||
// memory so we should still print the Info message below
|
||||
}
|
||||
|
||||
con.PrintInfof("Create alias for '%s' -> '%v'\n", aliasName, alias.Description)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AkaDeleteCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
aliasName := args[0]
|
||||
if _, exists := akaAliases[aliasName]; !exists {
|
||||
con.PrintErrorf("Alias '%s' does not exist\n", aliasName)
|
||||
return
|
||||
}
|
||||
|
||||
delete(akaAliases, aliasName)
|
||||
|
||||
// Save updated map to disk
|
||||
err := SaveAkaAliases()
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to save aliases to disk: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
con.PrintInfof("Deleted alias '%s'\n", aliasName)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Cmdhook(args []string) ([]string, error) {
|
||||
if len(args) == 0 {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
if aliasArgs, exists := akaAliases[args[0]]; exists {
|
||||
var expandedArgs []string
|
||||
|
||||
expandedArgs = append(expandedArgs, aliasArgs.Command)
|
||||
expandedArgs = append(expandedArgs, aliasArgs.DefaultArgs...)
|
||||
// join the user passed args so the shell processes it without flags if
|
||||
// available
|
||||
if len(args[1:]) > 0 {
|
||||
expandedArgs = append(expandedArgs, strings.Join(args[1:], " "))
|
||||
}
|
||||
|
||||
return expandedArgs, nil
|
||||
}
|
||||
|
||||
return args, nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AkaListCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if len(akaAliases) == 0 {
|
||||
con.PrintInfof("No command aliases created. Use `aka create` to create one.\n")
|
||||
return
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Alias",
|
||||
"Command",
|
||||
})
|
||||
|
||||
for alias, cmd := range akaAliases {
|
||||
tw.AppendRow(table.Row{
|
||||
alias,
|
||||
cmd.Description,
|
||||
})
|
||||
}
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package aka
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
)
|
||||
|
||||
type AkaAlias struct {
|
||||
Name string `json:"name"` // alias Name
|
||||
Command string `json:"command"` // command being aliased
|
||||
DefaultArgs []string `json:"default_args"` // args passed by the user
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
var akaAliases = make(map[string]*AkaAlias)
|
||||
|
||||
const (
|
||||
akaAliasFileName = "aka-aliases.json"
|
||||
)
|
||||
|
||||
func GetAkaAliasesFilePath() string {
|
||||
return filepath.Join(assets.GetRootAppDir(), akaAliasFileName)
|
||||
}
|
||||
|
||||
func LoadAkaAliases() error {
|
||||
filepath := GetAkaAliasesFilePath()
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
// if file hasn't been created yet, we fail gracefully
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
// otherwise, return the actual error; something else went wrong
|
||||
return err
|
||||
}
|
||||
|
||||
aliases := []AkaAlias{}
|
||||
err = json.Unmarshal(data, &aliases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
akaAliases = make(map[string]*AkaAlias)
|
||||
|
||||
for _, alias := range aliases {
|
||||
akaAliases[alias.Name] = &alias
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveAkaAliases() error {
|
||||
filepath := GetAkaAliasesFilePath()
|
||||
aliases := make([]*AkaAlias, 0, len(akaAliases))
|
||||
for _, alias := range akaAliases {
|
||||
aliases = append(aliases, alias)
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(aliases, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath, data, 0o600)
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
Alias
|
||||
===========
|
||||
# client/command/alias
|
||||
|
||||
Wraps external DLLs/tool in Sliver console commands.
|
||||
## Overview
|
||||
|
||||
Implements the 'alias' command group for the Sliver client console. Handlers map Cobra invocations to alias workflows such as install, load, and remove.
|
||||
|
||||
## Go Files
|
||||
|
||||
### Format
|
||||
|
||||
* `alias-dir/`
|
||||
* * `manifest.json` -
|
||||
- `alias.go` – Implements runtime alias management, printing installed aliases and exposing tab-completion helpers.
|
||||
- `alias_test.go` *(tests)* – Exercises manifest parsing logic to ensure alias metadata is interpreted correctly.
|
||||
- `commands.go` – Wires Cobra commands for listing, installing, loading, and removing aliases into the client console.
|
||||
- `install.go` – Handles installing alias bundles from directories or archives, writing manifests and payload artifacts to disk.
|
||||
- `load.go` – Loads alias manifests, validates target compatibility, and registers commands against the active console.
|
||||
- `remove.go` – Removes installed aliases, prompting for confirmation and deleting associated on-disk assets.
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
aliasCmd.AddCommand(aliasLoadCmd)
|
||||
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
|
||||
|
||||
aliasInstallCmd := &cobra.Command{
|
||||
Use: consts.InstallStr + " [ALIAS]",
|
||||
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
aliasCmd.AddCommand(aliasInstallCmd)
|
||||
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
|
||||
|
||||
aliasRemove := &cobra.Command{
|
||||
Use: consts.RmStr + " [ALIAS]",
|
||||
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -62,8 +62,7 @@ func installFromDir(aliasLocalPath string, con *console.SliverClient) {
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
con.PrintInfof("Alias '%s' already exists", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -114,7 +113,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
|
||||
} else {
|
||||
errorMsg = fmt.Sprintf("Failed to parse %s: %s\n", ManifestFileName, err)
|
||||
}
|
||||
con.PrintErrorf(errorMsg)
|
||||
con.PrintErrorf("%s", errorMsg)
|
||||
return nil
|
||||
}
|
||||
installPath := filepath.Join(assets.GetAliasesDir(), filepath.Base(manifest.CommandName))
|
||||
@@ -122,8 +121,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
|
||||
if promptToOverwrite {
|
||||
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
|
||||
+209
-17
@@ -27,15 +27,16 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/packages"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/carapace-sh/carapace"
|
||||
app "github.com/reeflective/console"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -76,6 +77,16 @@ type AliasFile struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// AliasArgument - An argument for an alias command.
|
||||
type AliasArgument struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Desc string `json:"desc"`
|
||||
Optional bool `json:"optional"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Choices []string `json:"choices,omitempty"`
|
||||
}
|
||||
|
||||
// AliasManifest - The manifest for an alias, contains metadata.
|
||||
type AliasManifest struct {
|
||||
Name string `json:"name"`
|
||||
@@ -89,6 +100,7 @@ type AliasManifest struct {
|
||||
Entrypoint string `json:"entrypoint"`
|
||||
AllowArgs bool `json:"allow_args"`
|
||||
DefaultArgs string `json:"default_args"`
|
||||
Arguments []*AliasArgument `json:"arguments"`
|
||||
Files []*AliasFile `json:"files"`
|
||||
IsReflective bool `json:"is_reflective"`
|
||||
IsAssembly bool `json:"is_assembly"`
|
||||
@@ -158,21 +170,69 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
}
|
||||
aliasManifest.RootPath = filepath.Dir(manifestPath)
|
||||
|
||||
// Build usage string including arguments
|
||||
usage := strings.Builder{}
|
||||
usage.WriteString(aliasManifest.CommandName)
|
||||
for _, arg := range aliasManifest.Arguments {
|
||||
usage.WriteString(" ")
|
||||
if arg.Optional {
|
||||
usage.WriteString("[")
|
||||
}
|
||||
usage.WriteString(strings.ToUpper(arg.Name))
|
||||
if arg.Optional {
|
||||
usage.WriteString("]")
|
||||
}
|
||||
}
|
||||
|
||||
// Build long help message
|
||||
longHelp := strings.Builder{}
|
||||
longHelp.WriteString("[[.Bold]]Command:[[.Normal]]")
|
||||
longHelp.WriteString(usage.String())
|
||||
longHelp.WriteString("\n")
|
||||
if len(aliasManifest.Help) > 0 || len(aliasManifest.LongHelp) > 0 {
|
||||
longHelp.WriteString("[[.Bold]]About:[[.Normal]]")
|
||||
if len(aliasManifest.Help) > 0 {
|
||||
longHelp.WriteString(aliasManifest.Help)
|
||||
longHelp.WriteString("\n")
|
||||
}
|
||||
if len(aliasManifest.LongHelp) > 0 {
|
||||
longHelp.WriteString(aliasManifest.LongHelp)
|
||||
longHelp.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if len(aliasManifest.Arguments) > 0 {
|
||||
longHelp.WriteString("[[.Bold]]Arguments:[[.Normal]]")
|
||||
for _, arg := range aliasManifest.Arguments {
|
||||
longHelp.WriteString("\n\t")
|
||||
optStr := ""
|
||||
if arg.Optional {
|
||||
optStr = "[OPTIONAL]"
|
||||
}
|
||||
aType := arg.Type
|
||||
if aType == "wstring" {
|
||||
aType = "string"
|
||||
}
|
||||
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
|
||||
}
|
||||
}
|
||||
|
||||
// for each alias command, add a new app command
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
longHelpMsg := help.FormatHelpTmpl(aliasManifest.LongHelp)
|
||||
longHelpMsg += "\n\n⚠️ If you're having issues passing arguments to the alias please read:\n"
|
||||
longHelpMsg += "https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing"
|
||||
addAliasCmd := &cobra.Command{
|
||||
Use: aliasManifest.CommandName,
|
||||
Use: usage.String(),
|
||||
Short: helpMsg,
|
||||
Long: longHelpMsg,
|
||||
Long: help.FormatHelpTmpl(longHelp.String()),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runAliasCommand(cmd, con, args)
|
||||
},
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
// DisableFlagParsing keeps the alias payload's own flags (which are
|
||||
// unknown to pflag) from being rejected or swallowed; we split the
|
||||
// Sliver-owned flags out of the raw slice ourselves in
|
||||
// runAliasCommand (#2264, same treatment as extensions).
|
||||
DisableFlagParsing: true,
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
}
|
||||
|
||||
if aliasManifest.IsAssembly {
|
||||
@@ -182,9 +242,9 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("app-domain", "d", "", "AppDomain name to create for .NET assembly. Generated randomly if not set.")
|
||||
f.StringP("arch", "a", "x84", "Assembly target architecture: x86, x64, x84 (x86+x64)")
|
||||
f.BoolP("in-process", "i", false, "Run in the current sliver process")
|
||||
f.StringP("runtime", "r", "", "Runtime to use for running the assembly (only supported when used with --in-process)")
|
||||
f.BoolP("amsi-bypass", "M", false, "Bypass AMSI on Windows (only supported when used with --in-process)")
|
||||
f.BoolP("etw-bypass", "E", false, "Bypass ETW on Windows (only supported when used with --in-process)")
|
||||
f.StringP("runtime", "r", "v4.0.30319", "Runtime to use for running the assembly")
|
||||
f.BoolP("amsi-bypass", "M", false, "Bypass AMSI on Windows")
|
||||
f.BoolP("etw-bypass", "E", false, "Bypass ETW on Windows")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
}
|
||||
|
||||
@@ -193,9 +253,13 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
|
||||
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
|
||||
// Setup completions for alias arguments
|
||||
comps := carapace.Gen(addAliasCmd)
|
||||
makeAliasArgCompleter(aliasManifest, comps)
|
||||
|
||||
cmd.AddCommand(addAliasCmd)
|
||||
|
||||
// Have to use a global map here, as passing the aliasCmd
|
||||
@@ -272,6 +336,27 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
aliasManifest := loadedAlias.Manifest
|
||||
|
||||
// The command runs with DisableFlagParsing, so args is the raw, unstripped
|
||||
// slice. Separate the Sliver-owned flags from the payload's own arguments
|
||||
// and re-publish them on the flag set (so the Get* readers below and
|
||||
// Request()'s --timeout still work), then hand only the payload arguments
|
||||
// to the execution paths (#2264, same treatment as extensions in #2323).
|
||||
ownedValues, helpRequested, payloadArgs, err := splitAliasArgs(args, aliasOwnedFlagSpecs(aliasManifest.IsAssembly))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if helpRequested {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
if err := applyOwnedAliasFlags(cmd, ownedValues); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
args = payloadArgs
|
||||
|
||||
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Fail to find alias file: %s\n", err)
|
||||
@@ -311,10 +396,9 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
} else if !aliasManifest.IsReflective {
|
||||
msgStr = " Arguments are limited to 256 characters when using the default fork/exec model for non-reflective PE payloads.\n"
|
||||
}
|
||||
con.PrintWarnf(msgStr)
|
||||
con.PrintWarnf("%s", msgStr)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Do you want to continue?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -340,7 +424,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
var outFilePath *os.File
|
||||
if save, _ := cmd.Flags().GetBool("save"); save {
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
|
||||
hostname := ""
|
||||
if session != nil {
|
||||
hostname = session.GetHostname()
|
||||
} else if beacon != nil {
|
||||
hostname = beacon.GetHostname()
|
||||
}
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(hostname)))
|
||||
outFilePath, err = os.CreateTemp("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
@@ -590,3 +680,105 @@ func makeAliasPlatformFilters(alias *AliasManifest) map[string]string {
|
||||
app.CommandFilterKey: strings.Join(all, ","),
|
||||
}
|
||||
}
|
||||
|
||||
// makeAliasArgCompleter builds the positional and dash arguments completer for the alias.
|
||||
// It provides completion for:
|
||||
// 1. Positional arguments (before --)
|
||||
// 2. Flag-style arguments after -- (e.g., --target, --port)
|
||||
func makeAliasArgCompleter(alias *AliasManifest, comps *carapace.Carapace) {
|
||||
if len(alias.Arguments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var actions []carapace.Action
|
||||
|
||||
for _, arg := range alias.Arguments {
|
||||
var action carapace.Action
|
||||
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
action = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
action = carapace.ActionFiles().Tag("alias data")
|
||||
default:
|
||||
action = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
|
||||
usage := fmt.Sprintf("(%s) %s", arg.Type, arg.Desc)
|
||||
if arg.Optional {
|
||||
usage += " (optional)"
|
||||
}
|
||||
|
||||
actions = append(actions, action.Usage("%s", usage))
|
||||
}
|
||||
|
||||
comps.PositionalCompletion(actions...)
|
||||
|
||||
// Add dash completion for flag-style arguments after --
|
||||
// Pre-build the flag completions at registration time (not in a callback)
|
||||
flagCompletion := makeAliasFlagNameCompletion(alias)
|
||||
|
||||
// Build value completions for each argument type
|
||||
valueCompletions := make(map[string]carapace.Action)
|
||||
for _, arg := range alias.Arguments {
|
||||
// If choices are defined, use them for completion
|
||||
if len(arg.Choices) > 0 {
|
||||
valueCompletions[arg.Name] = carapace.ActionValues(arg.Choices...).Tag("choices")
|
||||
} else {
|
||||
// Fall back to type-based completion
|
||||
switch arg.Type {
|
||||
case "file":
|
||||
valueCompletions[arg.Name] = carapace.ActionFiles().Tag("file path")
|
||||
case "bool":
|
||||
valueCompletions[arg.Name] = carapace.ActionValues("true", "false").Tag("boolean")
|
||||
default:
|
||||
valueCompletions[arg.Name] = carapace.ActionValues()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use DashAnyCompletion with a smart action that determines context
|
||||
comps.DashAnyCompletion(
|
||||
carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
// If typing a flag (starts with -)
|
||||
if strings.HasPrefix(c.Value, "-") {
|
||||
return flagCompletion
|
||||
}
|
||||
|
||||
// If previous arg was a flag, complete its value
|
||||
if len(c.Args) > 0 {
|
||||
lastArg := c.Args[len(c.Args)-1]
|
||||
if strings.HasPrefix(lastArg, "-") {
|
||||
flagName := strings.TrimLeft(lastArg, "-")
|
||||
if action, ok := valueCompletions[flagName]; ok {
|
||||
return action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: show flag names
|
||||
return flagCompletion
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// makeAliasFlagNameCompletion creates completion for flag names
|
||||
func makeAliasFlagNameCompletion(alias *AliasManifest) carapace.Action {
|
||||
var results []string
|
||||
|
||||
for _, arg := range alias.Arguments {
|
||||
flagName := fmt.Sprintf("--%s", arg.Name)
|
||||
desc := arg.Desc
|
||||
if arg.Optional {
|
||||
desc = fmt.Sprintf("[optional] %s", desc)
|
||||
}
|
||||
desc = fmt.Sprintf("(%s) %s", arg.Type, desc)
|
||||
results = append(results, flagName, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("alias arguments")
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -39,8 +39,7 @@ func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' alias?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' alias?", name), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# client/command/armory
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'armory' command group for the Sliver client console. Handlers map Cobra invocations to armory workflows such as install, manage, parsers, and search.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `armory.go` – Maintains the armory cache lifecycle, fetching package metadata, verifying signatures, and presenting bundle listings.
|
||||
- `armory_test.go` *(tests)* – Placeholder for armory command tests; currently used to keep the package's test harness wired up.
|
||||
- `commands.go` – Declares the Cobra command hierarchy for armory actions and binds shared flag/completion plumbing.
|
||||
- `install.go` – Resolves dependencies and installs aliases or extensions from cached metadata, prompting when overwriting existing packages.
|
||||
- `manage.go` – Implements management helpers for viewing, adding, verifying, and persisting armory configurations.
|
||||
- `parsers.go` – Provides parsers and HTTP helpers for retrieving armory indexes and packages, including Minisign verification.
|
||||
- `search.go` – Implements the regex-driven search command that filters cached armory packages.
|
||||
- `update.go` – Calculates available package updates, presents options to the user, and orchestrates update installation.
|
||||
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Something is wrong with this entry
|
||||
pkgCache.Delete(value)
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
||||
@@ -428,7 +428,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Packages" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Packages"))
|
||||
|
||||
urlMargin := 150 // Extra margin needed to show URL column
|
||||
|
||||
@@ -493,26 +493,26 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
|
||||
rows := []table.Row{}
|
||||
for _, pkg := range entries {
|
||||
color := console.Normal
|
||||
style := console.StyleNormal
|
||||
if extensions.CmdExists(pkg.CommandName, sliverMenu.Command) {
|
||||
color = console.Green
|
||||
style = console.StyleGreen
|
||||
}
|
||||
if con.Settings.SmallTermWidth+urlMargin < width {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.URL),
|
||||
style.Render(pkg.ArmoryName),
|
||||
style.Render(pkg.CommandName),
|
||||
style.Render(pkg.Version),
|
||||
style.Render(pkg.Type),
|
||||
style.Render(pkg.Help),
|
||||
style.Render(pkg.URL),
|
||||
})
|
||||
} else {
|
||||
rows = append(rows, table.Row{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.ArmoryName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.CommandName),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Version),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Type),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, pkg.Help),
|
||||
style.Render(pkg.ArmoryName),
|
||||
style.Render(pkg.CommandName),
|
||||
style.Render(pkg.Version),
|
||||
style.Render(pkg.Type),
|
||||
style.Render(pkg.Help),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ func PrintArmoryPackages(aliases []*alias.AliasManifest, exts []*extensions.Exte
|
||||
func PrintArmoryBundles(bundles []*ArmoryBundle, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Bundles" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Bundles"))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Armory Name",
|
||||
"Name",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -37,7 +55,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
Use: consts.InstallStr,
|
||||
Short: "Install an alias or extension",
|
||||
Long: help.GetHelpFor([]string{consts.ArmoryStr, consts.InstallStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ArmoryInstallCmd(cmd, con, args)
|
||||
},
|
||||
|
||||
@@ -22,12 +22,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
@@ -35,7 +37,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
|
||||
@@ -51,10 +53,9 @@ const (
|
||||
// ArmoryInstallCmd - The armory install command
|
||||
func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var promptToOverwrite bool
|
||||
name := args[0]
|
||||
if name == "" {
|
||||
con.PrintErrorf("A package or bundle name is required")
|
||||
return
|
||||
names := []string{}
|
||||
if len(args) > 0 {
|
||||
names = []string{args[0]}
|
||||
}
|
||||
forceInstallation, err := cmd.Flags().GetBool("force")
|
||||
if err != nil {
|
||||
@@ -83,51 +84,92 @@ func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
|
||||
clientConfig := parseArmoryHTTPConfig(cmd)
|
||||
refresh(clientConfig)
|
||||
if name == "all" {
|
||||
aliases, extensions := packageManifestsInCache()
|
||||
aliasCount, extCount := countUniqueCommandsFromManifests(aliases, extensions)
|
||||
confirm := false
|
||||
pluralAliases := "es"
|
||||
if aliasCount == 1 {
|
||||
pluralAliases = ""
|
||||
|
||||
if len(names) == 0 {
|
||||
armoryFilterName := ""
|
||||
if armoryPK != "" {
|
||||
armoryFilterName = armoryName
|
||||
}
|
||||
pluralExtensions := "s"
|
||||
if extCount == 1 {
|
||||
pluralExtensions = ""
|
||||
}
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Install %d alias%s and %d extension%s?",
|
||||
aliasCount, pluralAliases, extCount, pluralExtensions,
|
||||
),
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if !confirm {
|
||||
options := armoryInstallOptions(armoryPK, armoryFilterName)
|
||||
if len(options) == 0 {
|
||||
con.PrintInfof("No packages or bundles found\n")
|
||||
return
|
||||
}
|
||||
promptToOverwrite = false
|
||||
}
|
||||
err = installPackageByName(name, armoryPK, forceInstallation, promptToOverwrite, clientConfig, con)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrPackageNotFound) {
|
||||
bundles := bundlesInCache()
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Name == name {
|
||||
installBundle(bundle, armoryPK, forceInstallation, clientConfig, con)
|
||||
result, err := forms.ArmoryInstallForm(options)
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("Armory install form failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
// If we have made it here, then there was not a bundle or package that matched the provided name
|
||||
if armoryPK == "" {
|
||||
con.PrintErrorf("No package or bundle named %q was found\n", name)
|
||||
if len(result.SelectedNames) == 0 {
|
||||
return
|
||||
}
|
||||
names = result.SelectedNames
|
||||
}
|
||||
if len(names) == 0 {
|
||||
con.PrintErrorf("A package or bundle name is required")
|
||||
return
|
||||
}
|
||||
if slices.Contains(names, "all") {
|
||||
names = []string{"all"}
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
promptOverwrite := promptToOverwrite
|
||||
if name == "all" {
|
||||
aliases, extensions := packageManifestsInCache()
|
||||
aliasCount, extCount := countUniqueCommandsFromManifests(aliases, extensions)
|
||||
confirm := false
|
||||
pluralAliases := "es"
|
||||
if aliasCount == 1 {
|
||||
pluralAliases = ""
|
||||
}
|
||||
pluralExtensions := "s"
|
||||
if extCount == 1 {
|
||||
pluralExtensions = ""
|
||||
}
|
||||
forms.Confirm(fmt.Sprintf("Install %d alias%s and %d extension%s?",
|
||||
aliasCount, pluralAliases, extCount, pluralExtensions,
|
||||
), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
promptOverwrite = false
|
||||
}
|
||||
|
||||
err = installPackageByName(name, armoryPK, forceInstallation, promptOverwrite, clientConfig, con)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, ErrPackageNotFound) {
|
||||
bundles := bundlesInCache()
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Name == name {
|
||||
installBundle(bundle, armoryPK, forceInstallation, clientConfig, con)
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
// If we have made it here, then there was not a bundle or package that matched the provided name
|
||||
if armoryPK == "" {
|
||||
con.PrintErrorf("No package or bundle named %q was found\n", name)
|
||||
} else {
|
||||
con.PrintErrorf("No package or bundle named %q was found in armory %s\n", name, armoryName)
|
||||
}
|
||||
} else if errors.Is(err, ErrPackageAlreadyInstalled) {
|
||||
con.PrintErrorf("Package %q is already installed - use the force option to overwrite it\n", name)
|
||||
} else {
|
||||
con.PrintErrorf("No package or bundle named %q was found in armory %s\n", name, armoryName)
|
||||
con.PrintErrorf("Could not install package: %s\n", err)
|
||||
}
|
||||
} else if errors.Is(err, ErrPackageAlreadyInstalled) {
|
||||
con.PrintErrorf("Package %q is already installed - use the force option to overwrite it\n", name)
|
||||
} else {
|
||||
con.PrintErrorf("Could not install package: %s\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -232,19 +274,22 @@ func getInstalledPackageNames() []string {
|
||||
}
|
||||
|
||||
// This is a convenience function to get the names of the commands in the cache
|
||||
func getCommandsInCache() []string {
|
||||
func getCommandsInCache(armoryPK string) []string {
|
||||
commandNames := []string{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
|
||||
if cacheEntry.Alias.CommandName != "" && !slices.Contains(commandNames, cacheEntry.Alias.CommandName) {
|
||||
commandNames = append(commandNames, cacheEntry.Alias.CommandName)
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if !slices.Contains(commandNames, command.CommandName) {
|
||||
if command.CommandName != "" && !slices.Contains(commandNames, command.CommandName) {
|
||||
commandNames = append(commandNames, command.CommandName)
|
||||
}
|
||||
}
|
||||
@@ -256,32 +301,44 @@ func getCommandsInCache() []string {
|
||||
return commandNames
|
||||
}
|
||||
|
||||
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
|
||||
}
|
||||
if cacheEntry.Extension == nil {
|
||||
return false
|
||||
}
|
||||
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
|
||||
return true
|
||||
}
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
|
||||
packages := []*pkgCacheEntry{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if cacheEntry.Alias.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cacheEntry.LastErr != nil || !packageMatchesInstallName(cacheEntry, name) {
|
||||
return true
|
||||
}
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
|
||||
packageVersion := ""
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
packageVersion = cacheEntry.Alias.Version
|
||||
} else {
|
||||
packageVersion = cacheEntry.Extension.Version
|
||||
}
|
||||
if minimumVersion == "" || packageVersion >= minimumVersion {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -291,16 +348,12 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
|
||||
|
||||
func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
selectedPackageKey := ""
|
||||
optionKeys := util.Keys(options)
|
||||
optionKeys := slices.Collect(maps.Keys(options))
|
||||
slices.Sort(optionKeys)
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
options[doNotInstallOption] = doNotInstallPackageName
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name),
|
||||
Options: optionKeys,
|
||||
}
|
||||
survey.AskOne(prompt, &selectedPackageKey)
|
||||
forms.Select(fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name), optionKeys, &selectedPackageKey)
|
||||
selectedPackageID := options[selectedPackageKey]
|
||||
|
||||
return selectedPackageID
|
||||
@@ -360,7 +413,7 @@ func buildInstallList(name, armoryPK string, forceInstallation bool, pendingPack
|
||||
var requestedPackageList []string
|
||||
if name == "all" {
|
||||
requestedPackageList = []string{}
|
||||
allCommands := getCommandsInCache()
|
||||
allCommands := getCommandsInCache(armoryPK)
|
||||
for _, cmdName := range allCommands {
|
||||
if !slices.Contains(installedPackages, cmdName) || forceInstallation {
|
||||
// Check to see if there is a package pending with that name
|
||||
@@ -420,6 +473,15 @@ func installPackageByName(name, armoryPK string, forceInstallation, promptToOver
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(packageInstallList) == 0 && name == "all" {
|
||||
availableCommands := getCommandsInCache(armoryPK)
|
||||
if len(availableCommands) == 0 {
|
||||
con.PrintInfof("No packages or bundles found\n")
|
||||
} else {
|
||||
con.PrintInfof("All available packages are already installed\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(packageInstallList) > 0 {
|
||||
for _, packageID := range packageInstallList {
|
||||
entry := packageCacheLookupByID(packageID)
|
||||
@@ -444,12 +506,114 @@ func installPackageByName(name, armoryPK string, forceInstallation, promptToOver
|
||||
|
||||
if name == "all" {
|
||||
con.Printf("\n")
|
||||
con.PrintInfof("Operation complete\n")
|
||||
con.PrintSuccessf("Operation complete\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func armoryInstallOptions(armoryPK, armoryName string) []forms.ArmoryInstallOption {
|
||||
options := []forms.ArmoryInstallOption{}
|
||||
seen := make(map[string]bool)
|
||||
packageCount := 0
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if cacheEntry.LastErr != nil {
|
||||
return true
|
||||
}
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
name := cacheEntry.Alias.CommandName
|
||||
if name == "" {
|
||||
return true
|
||||
}
|
||||
if seen[name] {
|
||||
return true
|
||||
}
|
||||
label := formatArmoryInstallLabel("alias", name, cacheEntry.Alias.Help)
|
||||
options = append(options, forms.ArmoryInstallOption{
|
||||
Value: name,
|
||||
Label: label,
|
||||
})
|
||||
seen[name] = true
|
||||
packageCount++
|
||||
return true
|
||||
}
|
||||
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
name := command.CommandName
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if seen[name] {
|
||||
continue
|
||||
}
|
||||
label := formatArmoryInstallLabel("extension", name, command.Help)
|
||||
options = append(options, forms.ArmoryInstallOption{
|
||||
Value: name,
|
||||
Label: label,
|
||||
})
|
||||
seen[name] = true
|
||||
packageCount++
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
for _, bundle := range bundlesInCache() {
|
||||
if bundle.Name == "" {
|
||||
continue
|
||||
}
|
||||
if armoryName != "" && bundle.ArmoryName != armoryName {
|
||||
continue
|
||||
}
|
||||
if seen[bundle.Name] {
|
||||
continue
|
||||
}
|
||||
label := fmt.Sprintf("bundle: %s (%d packages)", bundle.Name, len(bundle.Packages))
|
||||
options = append(options, forms.ArmoryInstallOption{
|
||||
Value: bundle.Name,
|
||||
Label: label,
|
||||
})
|
||||
seen[bundle.Name] = true
|
||||
}
|
||||
|
||||
if len(options) == 0 {
|
||||
return options
|
||||
}
|
||||
|
||||
sort.Slice(options, func(i, j int) bool {
|
||||
return options[i].Label < options[j].Label
|
||||
})
|
||||
|
||||
if packageCount > 0 {
|
||||
options = append([]forms.ArmoryInstallOption{
|
||||
{
|
||||
Value: "all",
|
||||
Label: "all packages (aliases + extensions)",
|
||||
},
|
||||
}, options...)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func formatArmoryInstallLabel(kind, name, help string) string {
|
||||
help = strings.TrimSpace(help)
|
||||
if help == "" {
|
||||
return fmt.Sprintf("%s: %s", kind, name)
|
||||
}
|
||||
help = strings.Join(strings.Fields(help), " ")
|
||||
return fmt.Sprintf("%s: %s - %s", kind, name, help)
|
||||
}
|
||||
|
||||
func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientConfig ArmoryHTTPConfig, con *console.SliverClient) error {
|
||||
if entry == nil {
|
||||
return errors.New("package not found")
|
||||
@@ -483,20 +647,15 @@ func installAliasPackage(entry *pkgCacheEntry, promptToOverwrite bool, clientCon
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "sliver-armory-")
|
||||
tmpFileName, err := writeArmoryTempFile(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
_, err = tmpFile.Write(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFile.Close()
|
||||
defer os.Remove(tmpFileName)
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear the line
|
||||
|
||||
installPath := alias.InstallFromFile(tmpFile.Name(), entry.Alias.CommandName, promptToOverwrite, con)
|
||||
installPath := alias.InstallFromFile(tmpFileName, entry.Alias.CommandName, promptToOverwrite, con)
|
||||
if installPath == nil {
|
||||
return errors.New("failed to install alias")
|
||||
}
|
||||
@@ -577,23 +736,43 @@ func installExtensionPackage(entry *pkgCacheEntry, promptToOverwrite bool, clien
|
||||
return errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "sliver-armory-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
_, err = tmpFile.Write(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tmpFile.Sync()
|
||||
tmpFileName, err := writeArmoryTempFile(tarGz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpFileName)
|
||||
|
||||
con.Printf(console.Clearln + "\r") // Clear download message
|
||||
|
||||
extensions.InstallFromDir(tmpFile.Name(), promptToOverwrite, con, true)
|
||||
return extensions.InstallFromDir(tmpFileName, promptToOverwrite, con, true)
|
||||
}
|
||||
|
||||
return nil
|
||||
func writeArmoryTempFile(data []byte) (string, error) {
|
||||
if len(data) == 0 {
|
||||
return "", errors.New("downloaded archive is empty")
|
||||
}
|
||||
tmpFile, err := os.CreateTemp("", "sliver-armory-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpFileName := tmpFile.Name()
|
||||
for len(data) > 0 {
|
||||
n, err := tmpFile.Write(data)
|
||||
if err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFileName)
|
||||
return "", err
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
if err := tmpFile.Sync(); err != nil {
|
||||
tmpFile.Close()
|
||||
os.Remove(tmpFileName)
|
||||
return "", err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
os.Remove(tmpFileName)
|
||||
return "", err
|
||||
}
|
||||
return tmpFileName, nil
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package armory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/alias"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
)
|
||||
|
||||
func resetPackageCache(t *testing.T) {
|
||||
t.Helper()
|
||||
pkgCache.Range(func(key, _ any) bool {
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
pkgCache.Range(func(key, _ any) bool {
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testExtensionCacheEntry() pkgCacheEntry {
|
||||
manifest := &extensions.ExtensionManifest{
|
||||
Name: "ADCS Request (Remote)",
|
||||
PackageName: "remote-adcs-package",
|
||||
Version: "v0.1.5",
|
||||
ArmoryName: "Default Armory",
|
||||
ArmoryPK: "armory-public-key",
|
||||
ExtCommand: []*extensions.ExtCommand{
|
||||
{CommandName: "remote-adcs-request"},
|
||||
{CommandName: "remote-adcs-request-on-behalf"},
|
||||
},
|
||||
}
|
||||
return pkgCacheEntry{
|
||||
ArmoryConfig: &assets.ArmoryConfig{PublicKey: "armory-public-key"},
|
||||
Pkg: ArmoryPackage{
|
||||
Name: "Remote ADCS package",
|
||||
CommandName: "remote-adcs-index",
|
||||
},
|
||||
Extension: manifest,
|
||||
ID: "remote-adcs-request-id",
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPackageForCommandAcceptsStableExtensionIdentities(t *testing.T) {
|
||||
resetPackageCache(t)
|
||||
entry := testExtensionCacheEntry()
|
||||
pkgCache.Store(entry.ID, entry)
|
||||
|
||||
for _, name := range []string{
|
||||
"remote-adcs-index",
|
||||
"remote-adcs-request",
|
||||
"remote-adcs-request-on-behalf",
|
||||
"remote-adcs-package",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
found, err := getPackageForCommand(name, "armory-public-key", "v0.1.5")
|
||||
if err != nil {
|
||||
t.Fatalf("lookup %q failed: %v", name, err)
|
||||
}
|
||||
if found.ID != entry.ID {
|
||||
t.Fatalf("lookup %q returned %q, want %q", name, found.ID, entry.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := getPackageForCommand("remote-adcs-package", "other-armory", "v0.1.5"); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("wrong-armory lookup error = %v, want %v", err, ErrPackageNotFound)
|
||||
}
|
||||
if _, err := getPackageForCommand("remote-adcs-package", "armory-public-key", "v0.1.6"); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("newer-version lookup error = %v, want %v", err, ErrPackageNotFound)
|
||||
}
|
||||
for _, name := range []string{"ADCS Request (Remote)", "Remote ADCS package", "unknown-package"} {
|
||||
if _, err := getPackageForCommand(name, "", ""); !errors.Is(err, ErrPackageNotFound) {
|
||||
t.Fatalf("unstable identity %q lookup error = %v, want %v", name, err, ErrPackageNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageMatchesInstallNameLeavesAliasesCommandOnly(t *testing.T) {
|
||||
entry := pkgCacheEntry{
|
||||
Pkg: ArmoryPackage{
|
||||
Name: "Human Alias Name",
|
||||
CommandName: "alias-command",
|
||||
IsAlias: true,
|
||||
},
|
||||
Alias: &alias.AliasManifest{CommandName: "alias-command"},
|
||||
}
|
||||
if packageMatchesInstallName(entry, "Human Alias Name") {
|
||||
t.Fatal("alias unexpectedly matched its display name")
|
||||
}
|
||||
if !packageMatchesInstallName(entry, "alias-command") {
|
||||
t.Fatal("alias did not match its command name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionManifestsMatch(t *testing.T) {
|
||||
manifest := func(name, packageName string, commands ...string) *extensions.ExtensionManifest {
|
||||
extCommands := make([]*extensions.ExtCommand, 0, len(commands))
|
||||
for _, command := range commands {
|
||||
extCommands = append(extCommands, &extensions.ExtCommand{CommandName: command})
|
||||
}
|
||||
return &extensions.ExtensionManifest{Name: name, PackageName: packageName, ExtCommand: extCommands}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
local *extensions.ExtensionManifest
|
||||
latest *extensions.ExtensionManifest
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "v2 package identity",
|
||||
local: manifest("Old Display Name", "stable-package", "old-command"),
|
||||
latest: manifest("New Display Name", "stable-package", "new-command"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different v2 packages",
|
||||
local: manifest("Shared Display Name", "package-one", "shared-command"),
|
||||
latest: manifest("Shared Display Name", "package-two", "shared-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "v2 does not downgrade to legacy identity",
|
||||
local: manifest("Human Display Name", "stable-package", "shared-command"),
|
||||
latest: manifest("legacy-command", "", "shared-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "legacy command identity",
|
||||
local: manifest("legacy-command", "", "legacy-command"),
|
||||
latest: manifest("Human Display Name", "stable-package", "other-command", "legacy-command"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "display names are not identity",
|
||||
local: manifest("Shared Name", "", "old-command"),
|
||||
latest: manifest("Shared Name", "stable-package", "new-command"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated legacy package",
|
||||
local: manifest("Old Name", "", "old-command"),
|
||||
latest: manifest("New Name", "stable-package", "new-command"),
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := extensionManifestsMatch(test.local, test.latest); got != test.want {
|
||||
t.Fatalf("extensionManifestsMatch() = %t, want %t", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionUpdateDisplayNameResolvesPackage(t *testing.T) {
|
||||
resetPackageCache(t)
|
||||
t.Setenv("SLIVER_CLIENT_ROOT_DIR", t.TempDir())
|
||||
|
||||
installedManifest := []byte(`{
|
||||
"name":"ADCS Request (Remote)",
|
||||
"package_name":"remote-adcs-package",
|
||||
"version":"v0.1.3",
|
||||
"commands":[{
|
||||
"command_name":"remote-adcs-request",
|
||||
"help":"Request a certificate",
|
||||
"files":[{"os":"windows","arch":"amd64","path":"adcs_request.x64.o"}]
|
||||
}]
|
||||
}`)
|
||||
installDir := filepath.Join(assets.GetExtensionsDir(), "remote-adcs-request")
|
||||
if err := os.MkdirAll(installDir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(installDir, extensions.ManifestFileName), installedManifest, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
entry := testExtensionCacheEntry()
|
||||
pkgCache.Store(entry.ID, entry)
|
||||
otherEntry := testExtensionCacheEntry()
|
||||
otherEntry.ID = "other-armory-package-id"
|
||||
otherEntry.ArmoryConfig = &assets.ArmoryConfig{PublicKey: "other-armory"}
|
||||
otherManifest := *otherEntry.Extension
|
||||
otherManifest.Version = "v9.0.0"
|
||||
otherManifest.ArmoryName = "Other Armory"
|
||||
otherManifest.ArmoryPK = "other-armory"
|
||||
otherEntry.Extension = &otherManifest
|
||||
pkgCache.Store(otherEntry.ID, otherEntry)
|
||||
|
||||
updates := checkForExtensionUpdates("armory-public-key")
|
||||
versionInfo, ok := updates["ADCS Request (Remote)"]
|
||||
if !ok {
|
||||
t.Fatal("display-name update was not detected")
|
||||
}
|
||||
if versionInfo.OldVersion != "v0.1.3" || versionInfo.NewVersion != "v0.1.5" {
|
||||
t.Fatalf("unexpected versions: %#v", versionInfo)
|
||||
}
|
||||
if versionInfo.PackageID != entry.ID {
|
||||
t.Fatalf("update package ID = %q, want %q", versionInfo.PackageID, entry.ID)
|
||||
}
|
||||
|
||||
found := packageCacheLookupByID(versionInfo.PackageID)
|
||||
if found == nil {
|
||||
t.Fatal("detected update package is missing from the cache")
|
||||
}
|
||||
if found.ID != entry.ID {
|
||||
t.Fatalf("resolved package %q, want %q", found.ID, entry.ID)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,23 @@
|
||||
package armory
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -8,10 +26,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -602,8 +620,7 @@ func ModifyArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not modify armory: %s\n", err)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Would you like to revert the properties of the armory back to their previous values?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Would you like to revert the properties of the armory back to their previous values?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
} else {
|
||||
|
||||
@@ -19,13 +19,13 @@ package armory
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -34,13 +34,14 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
type VersionInformation struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
ArmoryName string
|
||||
PackageID string
|
||||
}
|
||||
|
||||
type PackageType uint
|
||||
@@ -87,16 +88,19 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if len(aliasUpdates) > 0 || len(extUpdates) > 0 {
|
||||
updateKeys := sortUpdateIdentifiers(aliasUpdates, extUpdates)
|
||||
displayAvailableUpdates(con, updateKeys, aliasUpdates, extUpdates)
|
||||
selectedUpdates, err = getUpdatesFromUser(con, updateKeys)
|
||||
selectedUpdates, err = getUpdatesFromUser(updateKeys)
|
||||
if err != nil {
|
||||
con.PrintErrorf(err.Error() + "\n")
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if len(selectedUpdates) == 0 {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
con.PrintSuccessf("All packages are up to date")
|
||||
con.PrintSuccessf("All packages are up to date\n")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,9 +126,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
|
||||
updatedPackage := packageCacheLookupByID(extVersionInfo.PackageID)
|
||||
if updatedPackage == nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, ErrPackageNotFound)
|
||||
continue
|
||||
}
|
||||
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
|
||||
@@ -171,8 +175,21 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
|
||||
return results
|
||||
}
|
||||
|
||||
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
|
||||
if localManifest.PackageName != "" {
|
||||
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
|
||||
}
|
||||
for _, localCommand := range localManifest.ExtCommand {
|
||||
for _, latestCommand := range latestManifest.ExtCommand {
|
||||
if localCommand.CommandName == latestCommand.CommandName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
_, cachedExtensions := packageManifestsInCache()
|
||||
// Return a map of extension name to minimum version to install
|
||||
results := make(map[string]VersionInformation)
|
||||
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
|
||||
@@ -184,7 +201,12 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, latestExt := range cachedExtensions {
|
||||
pkgCache.Range(func(_, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
|
||||
return true
|
||||
}
|
||||
latestExt := cacheEntry.Extension
|
||||
/*
|
||||
We used to check if the version identifiers were different between the installed version and the
|
||||
version in the armory. This worked when we had one armory, but with multiple potential armories,
|
||||
@@ -192,16 +214,22 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
so we will rely on the author of the package incrementing the version identifier somehow to determine
|
||||
if a package is newer.
|
||||
*/
|
||||
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
|
||||
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
|
||||
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
current, found := results[localManifest.Name]
|
||||
if !found || latestExt.Version > current.NewVersion ||
|
||||
(latestExt.Version == current.NewVersion && cacheEntry.ID < current.PackageID) {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
PackageID: cacheEntry.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -217,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
|
||||
|
||||
result := []UpdateIdentifier{}
|
||||
|
||||
aliasNames := util.Keys(aliasUpdates)
|
||||
extensionNames := util.Keys(extensionUpdates)
|
||||
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
|
||||
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
|
||||
for _, name := range aliasNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: AliasPackage,
|
||||
@@ -255,7 +283,7 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
|
||||
if len(extensionUpdates) != 1 {
|
||||
extensionSuffix = "s"
|
||||
}
|
||||
tw.SetTitle(console.Bold + fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix) + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render(fmt.Sprintf(title, len(aliasUpdates), aliasSuffix, len(extensionUpdates), extensionSuffix)))
|
||||
tw.AppendHeader(table.Row{
|
||||
"Package Name",
|
||||
"Package Type",
|
||||
@@ -299,76 +327,35 @@ func displayAvailableUpdates(con *console.SliverClient, updateKeys []UpdateIdent
|
||||
con.Printf("%s\n\n", tw.Render())
|
||||
}
|
||||
|
||||
func getUpdatesFromUser(con *console.SliverClient, updateKeys []UpdateIdentifier) (chosenUpdates []UpdateIdentifier, selectionError error) {
|
||||
chosenUpdates = []UpdateIdentifier{}
|
||||
func getUpdatesFromUser(updateKeys []UpdateIdentifier) ([]UpdateIdentifier, error) {
|
||||
options := make([]forms.ArmoryUpdateOption, 0, len(updateKeys))
|
||||
optionIndex := make(map[string]UpdateIdentifier, len(updateKeys))
|
||||
|
||||
var updateResponse string
|
||||
for _, key := range updateKeys {
|
||||
packageType := "Alias"
|
||||
if key.Type == ExtensionPackage {
|
||||
packageType = "Extension"
|
||||
}
|
||||
optionID := fmt.Sprintf("%d:%s", key.Type, key.Name)
|
||||
optionLabel := fmt.Sprintf("%s (%s)", key.Name, packageType)
|
||||
options = append(options, forms.ArmoryUpdateOption{
|
||||
ID: optionID,
|
||||
Label: optionLabel,
|
||||
})
|
||||
optionIndex[optionID] = key
|
||||
}
|
||||
|
||||
con.Println("You can apply all, none, or some updates.\nTo apply some updates, specify the number of a single update, a range (1-3), or a combination of the two (1, 3-5, 7)\n")
|
||||
err := survey.AskOne(&survey.Input{
|
||||
Message: "Which updates would you like to apply? [A]ll, [N]one, or some:",
|
||||
}, &updateResponse)
|
||||
result, err := forms.ArmoryUpdateForm(options)
|
||||
if err != nil {
|
||||
// We do not need to set selectionError because an error here means the same thing as responding with "none"
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateResponse = strings.ToLower(updateResponse)
|
||||
updateResponse = strings.Replace(updateResponse, " ", "", -1)
|
||||
if updateResponse == "n" || updateResponse == "none" {
|
||||
return
|
||||
}
|
||||
|
||||
if updateResponse == "a" || updateResponse == "all" {
|
||||
chosenUpdates = updateKeys
|
||||
return
|
||||
}
|
||||
|
||||
selections := strings.Split(updateResponse, ",")
|
||||
|
||||
for _, selection := range selections {
|
||||
// Handle a range
|
||||
if strings.Contains(selection, "-") {
|
||||
rangeParts := strings.Split(selection, "-")
|
||||
start, err := strconv.Atoi(rangeParts[0])
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[0])
|
||||
return
|
||||
}
|
||||
end, err := strconv.Atoi(rangeParts[1])
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", rangeParts[1])
|
||||
return
|
||||
}
|
||||
// Adjust for the 0 indexed slice we are working with
|
||||
start -= 1
|
||||
end -= 1
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if start > end {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", selection)
|
||||
return
|
||||
}
|
||||
if end >= len(updateKeys) {
|
||||
end = len(updateKeys) - 1
|
||||
}
|
||||
|
||||
for i := start; i <= end; i++ {
|
||||
chosenUpdates = append(chosenUpdates, updateKeys[i])
|
||||
}
|
||||
} else {
|
||||
// Single entry
|
||||
index, err := strconv.Atoi(selection)
|
||||
if err != nil {
|
||||
selectionError = fmt.Errorf("%s is not a valid range", selection)
|
||||
return
|
||||
}
|
||||
index -= 1
|
||||
if index >= 0 && index < len(updateKeys) {
|
||||
chosenUpdates = append(chosenUpdates, updateKeys[index])
|
||||
}
|
||||
chosenUpdates := make([]UpdateIdentifier, 0, len(result.SelectedIDs))
|
||||
for _, selectedID := range result.SelectedIDs {
|
||||
if update, ok := optionIndex[selectedID]; ok {
|
||||
chosenUpdates = append(chosenUpdates, update)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
return chosenUpdates, nil
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
Backdoor
|
||||
========
|
||||
# client/command/backdoor
|
||||
|
||||
Implements command related to `backdoor`
|
||||
## Overview
|
||||
|
||||
Implements the 'backdoor' command group for the Sliver client console.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `backdoor.go` – Implements the RPC workflow that backdoors a remote binary with Sliver shellcode for the active session.
|
||||
- `commands.go` – Registers the Cobra command, binds flags/completions, and restricts usage to applicable targets.
|
||||
@@ -1,4 +1,14 @@
|
||||
Beacons
|
||||
========
|
||||
# client/command/beacons
|
||||
|
||||
This package implements the beacons command, which can be used to list and interact with beacons.
|
||||
## Overview
|
||||
|
||||
Implements the 'beacons' command group for the Sliver client console. Handlers map Cobra invocations to beacons workflows such as prune, RM, and watch.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `beacons.go` – Retrieves beacon inventories and renders them in tabular form with optional filtering.
|
||||
- `commands.go` – Declares the beacons command set, binds flags/completions, and wires subcommands like prune, rm, and watch.
|
||||
- `helpers.go` – Provides helper RPC wrappers for selecting, fetching, and listing beacons.
|
||||
- `prune.go` – Implements the prune routine that removes stale beacon entries via the backend API.
|
||||
- `rm.go` – Removes a specific beacon by ID, coordinating confirmation and RPC teardown.
|
||||
- `watch.go` – Streams beacon events in a watch loop, handling cancellation and console interaction.
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
killFlag, _ := cmd.Flags().GetString("kill")
|
||||
killAll, _ := cmd.Flags().GetBool("kill-all")
|
||||
interact, _ := cmd.Flags().GetString("interact")
|
||||
|
||||
// Handle kill
|
||||
if killFlag != "" {
|
||||
@@ -72,6 +74,18 @@ func BeaconsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintInfof("Killed %s (%s)\n", beacon.Name, beacon.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if interact != "" {
|
||||
beacon, err := GetBeacon(con, interact)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.ActiveTarget.Set(nil, beacon)
|
||||
con.PrintInfof("Active beacon %s (%s)\n", beacon.Name, strings.Split(beacon.ID, "-")[0])
|
||||
return
|
||||
}
|
||||
|
||||
filter, _ := cmd.Flags().GetString("filter")
|
||||
var filterRegex *regexp.Regexp
|
||||
if filterRe, _ := cmd.Flags().GetString("filter-re"); filterRe != "" {
|
||||
@@ -108,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
wideTermWidth := con.Settings.SmallTermWidth < width
|
||||
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
|
||||
// preventing a second boundary from changing the table width while it is fitted.
|
||||
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
|
||||
windowsBeaconInList := false
|
||||
for _, beacon := range beacons {
|
||||
if beacon.OS == "windows" {
|
||||
windowsBeaconInList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if wideTermWidth {
|
||||
if windowsBeaconInList {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Integrity",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
|
||||
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
|
||||
})
|
||||
}
|
||||
|
||||
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
|
||||
if layout.Compact {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
@@ -162,79 +156,154 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
header := table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
}
|
||||
if includeIntegrity {
|
||||
header = append(header, "Integrity")
|
||||
}
|
||||
header = append(header, "Operating System")
|
||||
if layout.IncludeLocale {
|
||||
header = append(header, "Locale")
|
||||
}
|
||||
header = append(header, "Last Check-in", "Next Check-in")
|
||||
tw.AppendHeader(header)
|
||||
}
|
||||
|
||||
activeBeaconID := ""
|
||||
if con.ActiveTarget != nil {
|
||||
if activeBeacon := con.ActiveTarget.GetBeacon(); activeBeacon != nil {
|
||||
activeBeaconID = activeBeacon.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, beacon := range beacons {
|
||||
color := console.Normal
|
||||
activeBeacon := con.ActiveTarget.GetBeacon()
|
||||
if activeBeacon != nil && activeBeacon.ID == beacon.ID {
|
||||
color = console.Green
|
||||
integrity := beacon.Integrity
|
||||
if integrity == "" {
|
||||
integrity = "-"
|
||||
}
|
||||
if beacon.Integrity == "" {
|
||||
beacon.Integrity = "-"
|
||||
username := strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")
|
||||
lastCheckin := time.Unix(beacon.LastCheckin, 0)
|
||||
nextCheckin := time.Unix(beacon.NextCheckin, 0)
|
||||
canonicalEntries := []string{
|
||||
strings.Split(beacon.ID, "-")[0],
|
||||
beacon.Name,
|
||||
fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount),
|
||||
beacon.Transport,
|
||||
beacon.RemoteAddress,
|
||||
beacon.Hostname,
|
||||
username,
|
||||
fmt.Sprintf("%s (%d)", beacon.Filename, beacon.PID),
|
||||
integrity,
|
||||
fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch),
|
||||
beacon.Locale,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, false),
|
||||
}
|
||||
if !beaconMatchesFilter(canonicalEntries, filter, filterRegex) {
|
||||
continue
|
||||
}
|
||||
|
||||
style := console.StyleNormal
|
||||
if activeBeaconID == beacon.ID {
|
||||
style = console.StyleGreen
|
||||
}
|
||||
|
||||
// We need a slice of strings so we can apply filters
|
||||
var rowEntries []string
|
||||
|
||||
if wideTermWidth {
|
||||
if layout.Compact {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
||||
fmt.Sprintf(color+"%d/%d"+console.Normal, beacon.TasksCountCompleted, beacon.TasksCount),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.RemoteAddress),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
fmt.Sprintf(color+"%s (%d)"+console.Normal, beacon.Filename, beacon.PID),
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)),
|
||||
formatBeaconDateDelta(lastCheckin, now, false, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, false, true),
|
||||
}
|
||||
|
||||
if windowsBeaconInList {
|
||||
rowEntries = append(rowEntries, fmt.Sprintf(color+"%s"+console.Normal, beacon.Integrity))
|
||||
}
|
||||
|
||||
rowEntries = append(rowEntries, []string{
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Locale),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
}...)
|
||||
} else {
|
||||
rowEntries = []string{
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.Split(beacon.ID, "-")[0]),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Name),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Transport),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, beacon.Hostname),
|
||||
fmt.Sprintf(color+"%s"+console.Normal, strings.TrimPrefix(beacon.Username, beacon.Hostname+"\\")),
|
||||
fmt.Sprintf(color+"%s/%s"+console.Normal, beacon.OS, beacon.Arch),
|
||||
con.FormatDateDelta(time.Unix(beacon.LastCheckin, 0), wideTermWidth, false),
|
||||
con.FormatDateDelta(time.Unix(beacon.NextCheckin, 0), wideTermWidth, true),
|
||||
processName := beacon.Filename
|
||||
if layout.ProcessBasename {
|
||||
processName = targettable.ProcessName(processName)
|
||||
}
|
||||
rowEntries = []string{
|
||||
style.Render(strings.Split(beacon.ID, "-")[0]),
|
||||
style.Render(beacon.Name),
|
||||
style.Render(fmt.Sprintf("%d/%d", beacon.TasksCountCompleted, beacon.TasksCount)),
|
||||
style.Render(beacon.Transport),
|
||||
style.Render(beacon.RemoteAddress),
|
||||
style.Render(beacon.Hostname),
|
||||
style.Render(username),
|
||||
style.Render(fmt.Sprintf("%s (%d)", processName, beacon.PID)),
|
||||
}
|
||||
if includeIntegrity {
|
||||
rowEntries = append(rowEntries, style.Render(integrity))
|
||||
}
|
||||
rowEntries = append(rowEntries, style.Render(fmt.Sprintf("%s/%s", beacon.OS, beacon.Arch)))
|
||||
if layout.IncludeLocale {
|
||||
rowEntries = append(rowEntries, style.Render(beacon.Locale))
|
||||
}
|
||||
rowEntries = append(rowEntries,
|
||||
formatBeaconDateDelta(lastCheckin, now, true, false),
|
||||
formatBeaconDateDelta(nextCheckin, now, true, true),
|
||||
)
|
||||
}
|
||||
// Build the row struct
|
||||
|
||||
row := table.Row{}
|
||||
for _, entry := range rowEntries {
|
||||
row = append(row, entry)
|
||||
}
|
||||
// Apply filters if any
|
||||
if filter == "" && filterRegex == nil {
|
||||
tw.AppendRow(row)
|
||||
} else {
|
||||
for _, rowEntry := range rowEntries {
|
||||
if filter != "" {
|
||||
if strings.Contains(rowEntry, filter) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
if filterRegex != nil {
|
||||
if filterRegex.MatchString(rowEntry) {
|
||||
tw.AppendRow(row)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tw.AppendRow(row)
|
||||
}
|
||||
return tw
|
||||
}
|
||||
|
||||
func beaconMatchesFilter(entries []string, filter string, filterRegex *regexp.Regexp) bool {
|
||||
if filter == "" && filterRegex == nil {
|
||||
return true
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if filter != "" && strings.Contains(entry, filter) {
|
||||
return true
|
||||
}
|
||||
if filterRegex != nil && filterRegex.MatchString(entry) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func formatBeaconDateDelta(timestamp time.Time, now time.Time, includeDate bool, color bool) string {
|
||||
date := timestamp.Format(time.UnixDate)
|
||||
interval := ""
|
||||
if timestamp.Before(now) {
|
||||
delta := now.Sub(timestamp).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (%s ago)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldRed.Render(interval)
|
||||
}
|
||||
} else {
|
||||
delta := timestamp.Sub(now).Round(time.Second)
|
||||
if includeDate {
|
||||
interval = fmt.Sprintf("%s (in %s)", date, delta)
|
||||
} else {
|
||||
interval = delta.String()
|
||||
}
|
||||
if color {
|
||||
interval = console.StyleBoldGreen.Render(interval)
|
||||
}
|
||||
}
|
||||
return interval
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package beacons
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
var beaconTableTestNow = time.Date(2026, time.September, 3, 21, 25, 41, 0, time.UTC)
|
||||
|
||||
func TestRenderBeaconsUsesFullProcessPathWhenItFits(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
if !strings.Contains(rendered, "(36s ago)") || !strings.Contains(rendered, "(in 24s)") {
|
||||
t.Fatalf("wide check-in timestamps changed unexpectedly:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsUsesPortableProcessBasenameWhenRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
beacon *clientpb.Beacon
|
||||
hasIntegrity bool
|
||||
wantProcess string
|
||||
}{
|
||||
{
|
||||
name: "windows path",
|
||||
beacon: testWindowsBeacon(),
|
||||
hasIntegrity: true,
|
||||
wantProcess: "winterfell.exe (2020)",
|
||||
},
|
||||
{
|
||||
name: "posix path",
|
||||
beacon: testLinuxBeacon(),
|
||||
hasIntegrity: false,
|
||||
wantProcess: "winterfell-agent (2020)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
beacons := []*clientpb.Beacon{tt.beacon}
|
||||
fullLayout := targettable.Layout{
|
||||
IncludeIntegrity: tt.hasIntegrity,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameLayout := fullLayout
|
||||
basenameLayout.ProcessBasename = true
|
||||
fullWidth := beaconCandidateWidth(t, beacons, fullLayout, tt.hasIntegrity)
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, tt.hasIntegrity)
|
||||
if basenameWidth >= fullWidth {
|
||||
t.Fatalf("basename candidate width %d is not narrower than full-path width %d", basenameWidth, fullWidth)
|
||||
}
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, basenameWidth)
|
||||
if !strings.Contains(rendered, tt.wantProcess) {
|
||||
t.Fatalf("process basename missing from table:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, tt.beacon.Filename) {
|
||||
t.Fatalf("full process path remained in basename layout:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, basenameWidth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsOmitsIntegrityBeforeLocale(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
withAll := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
withoutIntegrity := withAll
|
||||
withoutIntegrity.IncludeIntegrity = false
|
||||
withoutLocale := withoutIntegrity
|
||||
withoutLocale.IncludeLocale = false
|
||||
|
||||
allWidth := beaconCandidateWidth(t, beacons, withAll, true)
|
||||
withoutIntegrityWidth := beaconCandidateWidth(t, beacons, withoutIntegrity, true)
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
if !(withoutLocaleWidth < withoutIntegrityWidth && withoutIntegrityWidth < allWidth) {
|
||||
t.Fatalf("candidate widths did not shrink in omission order: all=%d no-integrity=%d no-locale=%d", allWidth, withoutIntegrityWidth, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
withoutIntegrityRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutIntegrityWidth)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutIntegrityRendered, "Locale", true)
|
||||
assertBeaconTableFits(t, withoutIntegrityRendered, withoutIntegrityWidth)
|
||||
|
||||
withoutLocaleRendered := renderBeaconTableAtWidth(t, beacons, "", nil, withoutLocaleWidth)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Integrity", false)
|
||||
assertHeaderPresence(t, withoutLocaleRendered, "Locale", false)
|
||||
assertBeaconTableFits(t, withoutLocaleRendered, withoutLocaleWidth)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotAddIntegrityForNonWindowsTargets(t *testing.T) {
|
||||
beacon := testLinuxBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
layout := targettable.Layout{IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, beacons, layout, false)
|
||||
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, width)
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
assertHeaderPresence(t, rendered, "Locale", true)
|
||||
if !strings.Contains(rendered, beacon.Filename+" (2020)") {
|
||||
t.Fatalf("non-Windows full process path missing from table:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, width)
|
||||
}
|
||||
|
||||
func TestRenderBeaconsFiltersCanonicalFullRows(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
basenameLayout := targettable.Layout{
|
||||
ProcessBasename: true,
|
||||
IncludeIntegrity: true,
|
||||
IncludeLocale: true,
|
||||
}
|
||||
basenameWidth := beaconCandidateWidth(t, beacons, basenameLayout, true)
|
||||
withoutLocale := basenameLayout
|
||||
withoutLocale.IncludeIntegrity = false
|
||||
withoutLocale.IncludeLocale = false
|
||||
withoutLocaleWidth := beaconCandidateWidth(t, beacons, withoutLocale, true)
|
||||
|
||||
t.Run("full process path survives basename projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, `AppData\Local\Temp`, nil, basenameWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching canonical process path removed beacon:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, `AppData\Local\Temp`) {
|
||||
t.Fatalf("basename layout unexpectedly displayed filtered directory:\n%s", rendered)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hidden integrity survives column projection", func(t *testing.T) {
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", regexp.MustCompile(`^High$`), withoutLocaleWidth)
|
||||
if !strings.Contains(rendered, beacon.Name) {
|
||||
t.Fatalf("filter matching hidden integrity removed beacon:\n%s", rendered)
|
||||
}
|
||||
assertHeaderPresence(t, rendered, "Integrity", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenderBeaconsDoesNotMutateEmptyIntegrity(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.Integrity = ""
|
||||
layout := targettable.Layout{IncludeIntegrity: true, IncludeLocale: true}
|
||||
width := beaconCandidateWidth(t, []*clientpb.Beacon{beacon}, layout, true)
|
||||
|
||||
_ = renderBeaconTableAtWidth(t, []*clientpb.Beacon{beacon}, "", nil, width)
|
||||
if beacon.Integrity != "" {
|
||||
t.Fatalf("beacon integrity mutated to %q", beacon.Integrity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBeaconsCompactFallbackPreservesLegacyColumnsAndTimes(t *testing.T) {
|
||||
beacon := testWindowsBeacon()
|
||||
beacons := []*clientpb.Beacon{beacon}
|
||||
compactWidth := beaconCandidateWidth(t, beacons, targettable.Layout{Compact: true}, true)
|
||||
withoutOptionalWidth := beaconCandidateWidth(t, beacons, targettable.Layout{ProcessBasename: true}, true)
|
||||
if compactWidth >= withoutOptionalWidth {
|
||||
t.Fatalf("test fixture does not require compact fallback: compact=%d detailed=%d", compactWidth, withoutOptionalWidth)
|
||||
}
|
||||
rendered := renderBeaconTableAtWidth(t, beacons, "", nil, compactWidth)
|
||||
|
||||
for _, omitted := range []string{"Tasks", "Remote Address", "Process (PID)", "Integrity", "Locale"} {
|
||||
assertHeaderPresence(t, rendered, omitted, false)
|
||||
}
|
||||
if !strings.Contains(rendered, "36s") || !strings.Contains(rendered, "24s") {
|
||||
t.Fatalf("compact relative check-in times missing:\n%s", rendered)
|
||||
}
|
||||
if strings.Contains(rendered, "36s ago") || strings.Contains(rendered, "in 24s") {
|
||||
t.Fatalf("compact fallback did not preserve legacy timestamp format:\n%s", rendered)
|
||||
}
|
||||
assertBeaconTableFits(t, rendered, compactWidth)
|
||||
}
|
||||
|
||||
func testWindowsBeacon() *clientpb.Beacon {
|
||||
return &clientpb.Beacon{
|
||||
ID: "0c9f6b27-1111-2222-3333-444444444444",
|
||||
Name: "winterfell-session",
|
||||
TasksCount: 3,
|
||||
TasksCountCompleted: 2,
|
||||
Transport: "mtls",
|
||||
RemoteAddress: "10.13.37.11:49760",
|
||||
Hostname: "winterfell",
|
||||
Username: `winterfell\vagrant`,
|
||||
Filename: `C:\Users\vagrant\AppData\Local\Temp\sliver-range\winterfell.exe`,
|
||||
PID: 2020,
|
||||
Integrity: "High",
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Locale: "en-US",
|
||||
LastCheckin: beaconTableTestNow.Add(-36 * time.Second).Unix(),
|
||||
NextCheckin: beaconTableTestNow.Add(24 * time.Second).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func testLinuxBeacon() *clientpb.Beacon {
|
||||
beacon := testWindowsBeacon()
|
||||
beacon.OS = "linux"
|
||||
beacon.Hostname = "winterfell-linux"
|
||||
beacon.Username = "vagrant"
|
||||
beacon.Filename = "/opt/sliver/temporary/build/output/winterfell-agent"
|
||||
beacon.Integrity = ""
|
||||
return beacon
|
||||
}
|
||||
|
||||
func testBeaconConsole() *console.SliverClient {
|
||||
return &console.SliverClient{
|
||||
Settings: &assets.ClientSettings{TableStyle: "SliverDefault"},
|
||||
}
|
||||
}
|
||||
|
||||
func beaconCandidateWidth(t *testing.T, beacons []*clientpb.Beacon, layout targettable.Layout, hasIntegrity bool) int {
|
||||
t.Helper()
|
||||
tw := buildBeaconTable(beacons, "", nil, testBeaconConsole(), layout, hasIntegrity, beaconTableTestNow)
|
||||
return text.LongestLineLen(tw.Render())
|
||||
}
|
||||
|
||||
func renderBeaconTableAtWidth(t *testing.T, beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, width int) string {
|
||||
t.Helper()
|
||||
tw := renderBeaconsAtWidth(beacons, filter, filterRegex, testBeaconConsole(), width, beaconTableTestNow)
|
||||
return text.StripEscape(tw.Render())
|
||||
}
|
||||
|
||||
func assertBeaconTableFits(t *testing.T, rendered string, width int) {
|
||||
t.Helper()
|
||||
if got := text.LongestLineLen(rendered); got > width {
|
||||
t.Fatalf("rendered table width %d exceeds terminal width %d:\n%s", got, width, rendered)
|
||||
}
|
||||
}
|
||||
|
||||
func assertHeaderPresence(t *testing.T, rendered string, header string, want bool) {
|
||||
t.Helper()
|
||||
headerLine := strings.SplitN(rendered, "\n", 2)[0]
|
||||
if got := strings.Contains(strings.ToLower(headerLine), strings.ToLower(header)); got != want {
|
||||
t.Fatalf("header %q presence = %v, want %v; header line: %s", header, got, want, headerLine)
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||
f.BoolP("kill-all", "K", false, "kill all beacons")
|
||||
f.StringP("interact", "i", "", "interact with a beacon")
|
||||
f.BoolP("force", "F", false, "force killing the beacon")
|
||||
|
||||
f.StringP("filter", "f", "", "filter beacons by substring")
|
||||
@@ -40,7 +41,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
flags.BindFlagCompletions(beaconsCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["kill"] = BeaconIDCompleter(con)
|
||||
(*comp)["interact"] = BeaconIDCompleter(con)
|
||||
})
|
||||
registerBeaconIDFlagCompletion(beaconsCmd, "interact", con)
|
||||
beaconsRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
Short: "Remove a beacon",
|
||||
@@ -81,22 +84,74 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
// BeaconIDCompleter completes beacon IDs
|
||||
func BeaconIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
callback := func(_ carapace.Context) carapace.Action {
|
||||
results := make([]string, 0)
|
||||
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err == nil {
|
||||
for _, b := range beacons.Beacons {
|
||||
link := fmt.Sprintf("[%s <- %s]", b.ActiveC2, b.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", b.Name, b.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", b.Username, b.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, b.ID[:8])
|
||||
results = append(results, desc)
|
||||
}
|
||||
}
|
||||
return carapace.ActionValuesDescribed(results...).Tag("beacons")
|
||||
return carapace.ActionValuesDescribed(beaconCompletionPairs(con)...).Tag("beacons")
|
||||
}
|
||||
|
||||
return carapace.ActionCallback(callback)
|
||||
}
|
||||
|
||||
func registerBeaconIDFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values := describedValuesToTabs(beaconCompletionPairs(con))
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func beaconCompletionPairs(con *console.SliverClient) []string {
|
||||
results := make([]string, 0)
|
||||
if con == nil || con.Rpc == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
beacons, err := con.Rpc.GetBeacons(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return results
|
||||
}
|
||||
|
||||
for _, b := range beacons.Beacons {
|
||||
link := fmt.Sprintf("[%s <- %s]", b.ActiveC2, b.RemoteAddress)
|
||||
id := fmt.Sprintf("%s (%d)", b.Name, b.PID)
|
||||
userHost := fmt.Sprintf("%s@%s", b.Username, b.Hostname)
|
||||
desc := strings.Join([]string{id, userHost, link}, " ")
|
||||
|
||||
results = append(results, b.ID[:8], desc)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func describedValuesToTabs(values []string) []string {
|
||||
tabbed := make([]string, 0, len(values)/2)
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
tabbed = append(tabbed, fmt.Sprintf("%s\t%s", values[i], values[i+1]))
|
||||
}
|
||||
return tabbed
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -26,9 +26,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -83,12 +82,8 @@ func SelectBeacon(con *console.SliverClient) (*clientpb.Beacon, error) {
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a beacon:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select a beacon:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package beacons
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -71,8 +71,7 @@ func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
con.Println()
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Prune these beacons?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Prune these beacons?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# client/command/builders
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'builders' command group for the Sliver client console.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `builders.go` – Fetches external builder metadata and formats it for console display.
|
||||
- `commands.go` – Registers the builders CLI command with its flags and completion helpers.
|
||||
@@ -0,0 +1,10 @@
|
||||
# client/command/c2profiles
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'c2profiles' command group for the Sliver client console. Handlers map Cobra invocations to c2profiles workflows such as C2 profiles.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `c2profiles.go` – Implements listing, import/export, and generation routines for HTTP C2 profiles, including protobuf/JSON conversion helpers.
|
||||
- `commands.go` – Binds the c2profiles command suite with flag handling and completions for profile management operations.
|
||||
@@ -21,28 +21,32 @@ package c2profiles
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSelection = errors.New("no selection")
|
||||
)
|
||||
|
||||
// C2ProfileCmd list available http profiles
|
||||
func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
profileName, _ := cmd.Flags().GetString("name")
|
||||
@@ -54,7 +58,11 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
return
|
||||
}
|
||||
if len(httpC2Profiles.Configs) != 1 {
|
||||
profileName = selectC2Profile(httpC2Profiles.Configs)
|
||||
profileName, err = selectC2Profile(httpC2Profiles.Configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +75,6 @@ func C2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
|
||||
func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
protocols := []string{constants.HttpStr, constants.HttpsStr}
|
||||
profileName, _ := cmd.Flags().GetString("name")
|
||||
if profileName == "" {
|
||||
con.PrintErrorf("Invalid c2 profile name\n")
|
||||
@@ -103,29 +110,6 @@ func ImportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Restart HTTP/S jobs?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
if confirm {
|
||||
var restartJobReq clientpb.RestartJobReq
|
||||
jobs, err := con.Rpc.GetJobs(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
// reload jobs to include new profile
|
||||
for _, job := range jobs.Active {
|
||||
if job != nil && slices.Contains(protocols, job.Name) {
|
||||
restartJobReq.JobIDs = append(restartJobReq.JobIDs, job.ID)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = con.Rpc.RestartJobs(context.Background(), &restartJobReq)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
@@ -149,7 +133,11 @@ func ExportC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
return
|
||||
}
|
||||
if len(httpC2Profiles.Configs) != 1 {
|
||||
profileName = selectC2Profile(httpC2Profiles.Configs)
|
||||
profileName, err = selectC2Profile(httpC2Profiles.Configs)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,23 +189,6 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
return
|
||||
}
|
||||
|
||||
c2Profiles, err := con.Rpc.GetHTTPC2Profiles(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
var extensions []string
|
||||
for _, c2profile := range c2Profiles.Configs {
|
||||
confProfile, err := con.Rpc.GetHTTPC2ProfileByName(context.Background(), &clientpb.C2ProfileReq{Name: c2profile.Name})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
extensions = append(extensions, confProfile.ImplantConfig.StagerFileExtension)
|
||||
extensions = append(extensions, confProfile.ImplantConfig.StartSessionFileExtension)
|
||||
}
|
||||
|
||||
config, err := C2ConfigToJSON(profileName, profile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
@@ -243,7 +214,7 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
}
|
||||
urls := strings.Split(string(fileContent), "\n")
|
||||
|
||||
jsonProfile, err := updateC2Profile(extensions, config, urls)
|
||||
jsonProfile, err := updateC2Profile(config, urls)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
@@ -264,26 +235,26 @@ func GenerateC2ProfileCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
}
|
||||
con.Println("C2 profile generated and saved as ", profileName)
|
||||
} else {
|
||||
PrintC2Profiles(profile, con)
|
||||
PrintC2Profiles(C2ConfigToProtobuf(profileName, jsonProfile), con)
|
||||
}
|
||||
}
|
||||
|
||||
// convert protobuf to json
|
||||
func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets.HTTPC2Config, error) {
|
||||
implantConfig := assets.HTTPC2ImplantConfig{
|
||||
UserAgent: profile.ImplantConfig.UserAgent,
|
||||
ChromeBaseVersion: int(profile.ImplantConfig.ChromeBaseVersion),
|
||||
MacOSVersion: profile.ImplantConfig.MacOSVersion,
|
||||
NonceQueryArgChars: profile.ImplantConfig.NonceQueryArgChars,
|
||||
MaxFiles: int(profile.ImplantConfig.MaxFiles),
|
||||
MinFiles: int(profile.ImplantConfig.MinFiles),
|
||||
MaxPaths: int(profile.ImplantConfig.MaxPaths),
|
||||
MinPaths: int(profile.ImplantConfig.MinFiles),
|
||||
StagerFileExt: profile.ImplantConfig.StagerFileExtension,
|
||||
PollFileExt: profile.ImplantConfig.PollFileExtension,
|
||||
StartSessionFileExt: profile.ImplantConfig.StartSessionFileExtension,
|
||||
SessionFileExt: profile.ImplantConfig.SessionFileExtension,
|
||||
CloseFileExt: profile.ImplantConfig.CloseFileExtension,
|
||||
UserAgent: profile.ImplantConfig.UserAgent,
|
||||
ChromeBaseVersion: int(profile.ImplantConfig.ChromeBaseVersion),
|
||||
MacOSVersion: profile.ImplantConfig.MacOSVersion,
|
||||
NonceQueryArgChars: profile.ImplantConfig.NonceQueryArgChars,
|
||||
NonceQueryLength: int(profile.ImplantConfig.NonceQueryLength),
|
||||
NonceMode: profile.ImplantConfig.NonceMode,
|
||||
MaxFileGen: int(profile.ImplantConfig.MaxFileGen),
|
||||
MinFileGen: int(profile.ImplantConfig.MinFileGen),
|
||||
MaxPathGen: int(profile.ImplantConfig.MaxPathGen),
|
||||
MinPathGen: int(profile.ImplantConfig.MinFileGen),
|
||||
MaxPathLength: int(profile.ImplantConfig.MaxPathLength),
|
||||
MinPathLength: int(profile.ImplantConfig.MinPathLength),
|
||||
Extensions: profile.ImplantConfig.Extensions,
|
||||
}
|
||||
|
||||
var headers []assets.NameValueProbability
|
||||
@@ -292,6 +263,7 @@ func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
Probability: int(header.Probability),
|
||||
Method: header.Method,
|
||||
})
|
||||
}
|
||||
implantConfig.Headers = headers
|
||||
@@ -307,50 +279,19 @@ func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets
|
||||
implantConfig.URLParameters = urlParameters
|
||||
|
||||
var (
|
||||
stagerFiles []string
|
||||
pollFiles []string
|
||||
sessionFiles []string
|
||||
closeFiles []string
|
||||
stagerPaths []string
|
||||
pollPaths []string
|
||||
sessionPaths []string
|
||||
closePaths []string
|
||||
files []string
|
||||
paths []string
|
||||
)
|
||||
|
||||
for _, pathSegment := range profile.ImplantConfig.PathSegments {
|
||||
if pathSegment.IsFile {
|
||||
switch pathSegment.SegmentType {
|
||||
case 0:
|
||||
pollFiles = append(pollFiles, pathSegment.Value)
|
||||
case 1:
|
||||
sessionFiles = append(sessionFiles, pathSegment.Value)
|
||||
case 2:
|
||||
closeFiles = append(closeFiles, pathSegment.Value)
|
||||
case 3:
|
||||
stagerFiles = append(stagerFiles, pathSegment.Value)
|
||||
}
|
||||
files = append(files, pathSegment.Value)
|
||||
} else {
|
||||
switch pathSegment.SegmentType {
|
||||
case 0:
|
||||
pollPaths = append(pollPaths, pathSegment.Value)
|
||||
case 1:
|
||||
sessionPaths = append(sessionPaths, pathSegment.Value)
|
||||
case 2:
|
||||
closePaths = append(closePaths, pathSegment.Value)
|
||||
case 3:
|
||||
stagerPaths = append(stagerPaths, pathSegment.Value)
|
||||
}
|
||||
paths = append(paths, pathSegment.Value)
|
||||
}
|
||||
}
|
||||
|
||||
implantConfig.PollFiles = pollFiles
|
||||
implantConfig.SessionFiles = sessionFiles
|
||||
implantConfig.CloseFiles = closeFiles
|
||||
implantConfig.StagerFiles = stagerFiles
|
||||
implantConfig.PollPaths = pollPaths
|
||||
implantConfig.SessionPaths = sessionPaths
|
||||
implantConfig.ClosePaths = closePaths
|
||||
implantConfig.StagerPaths = stagerPaths
|
||||
implantConfig.Files = files
|
||||
implantConfig.Paths = paths
|
||||
|
||||
var serverHeaders []assets.NameValueProbability
|
||||
for _, header := range profile.ServerConfig.Headers {
|
||||
@@ -358,6 +299,7 @@ func C2ConfigToJSON(profileName string, profile *clientpb.HTTPC2Config) (*assets
|
||||
Name: header.Name,
|
||||
Value: header.Value,
|
||||
Probability: int(header.Probability),
|
||||
Method: header.Method,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -388,101 +330,66 @@ func C2ConfigToProtobuf(profileName string, config *assets.HTTPC2Config) *client
|
||||
pathSegments := []*clientpb.HTTPC2PathSegment{}
|
||||
|
||||
// files
|
||||
for _, poll := range config.ImplantConfig.PollFiles {
|
||||
for _, file := range config.ImplantConfig.Files {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: true,
|
||||
SegmentType: 0,
|
||||
Value: poll,
|
||||
IsFile: true,
|
||||
Value: file,
|
||||
})
|
||||
}
|
||||
|
||||
for _, session := range config.ImplantConfig.SessionFiles {
|
||||
for _, path := range config.ImplantConfig.Paths {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: true,
|
||||
SegmentType: 1,
|
||||
Value: session,
|
||||
IsFile: false,
|
||||
Value: path,
|
||||
})
|
||||
}
|
||||
|
||||
for _, close := range config.ImplantConfig.CloseFiles {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: true,
|
||||
SegmentType: 2,
|
||||
Value: close,
|
||||
for _, clientHeader := range config.ImplantConfig.Headers {
|
||||
httpC2Headers = append(httpC2Headers, &clientpb.HTTPC2Header{
|
||||
Method: clientHeader.Method,
|
||||
Name: clientHeader.Name,
|
||||
Value: clientHeader.Value,
|
||||
Probability: int32(clientHeader.Probability),
|
||||
})
|
||||
}
|
||||
|
||||
for _, stager := range config.ImplantConfig.StagerFiles {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: true,
|
||||
SegmentType: 3,
|
||||
Value: stager,
|
||||
})
|
||||
}
|
||||
|
||||
// paths
|
||||
for _, poll := range config.ImplantConfig.PollPaths {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: false,
|
||||
SegmentType: 0,
|
||||
Value: poll,
|
||||
})
|
||||
}
|
||||
|
||||
for _, session := range config.ImplantConfig.SessionPaths {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: false,
|
||||
SegmentType: 1,
|
||||
Value: session,
|
||||
})
|
||||
}
|
||||
|
||||
for _, close := range config.ImplantConfig.ClosePaths {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: false,
|
||||
SegmentType: 2,
|
||||
Value: close,
|
||||
})
|
||||
}
|
||||
|
||||
for _, stager := range config.ImplantConfig.StagerPaths {
|
||||
pathSegments = append(pathSegments, &clientpb.HTTPC2PathSegment{
|
||||
IsFile: false,
|
||||
SegmentType: 3,
|
||||
Value: stager,
|
||||
for _, urlParameter := range config.ImplantConfig.URLParameters {
|
||||
httpC2UrlParameters = append(httpC2UrlParameters, &clientpb.HTTPC2URLParameter{
|
||||
Method: urlParameter.Method,
|
||||
Name: urlParameter.Name,
|
||||
Value: urlParameter.Value,
|
||||
Probability: int32(urlParameter.Probability),
|
||||
})
|
||||
}
|
||||
|
||||
implantConfig := &clientpb.HTTPC2ImplantConfig{
|
||||
UserAgent: config.ImplantConfig.UserAgent,
|
||||
ChromeBaseVersion: int32(config.ImplantConfig.ChromeBaseVersion),
|
||||
MacOSVersion: config.ImplantConfig.MacOSVersion,
|
||||
NonceQueryArgChars: config.ImplantConfig.NonceQueryArgChars,
|
||||
ExtraURLParameters: httpC2UrlParameters,
|
||||
Headers: httpC2Headers,
|
||||
MaxFiles: int32(config.ImplantConfig.MaxFiles),
|
||||
MinFiles: int32(config.ImplantConfig.MinFiles),
|
||||
MaxPaths: int32(config.ImplantConfig.MaxPaths),
|
||||
MinPaths: int32(config.ImplantConfig.MinFiles),
|
||||
StagerFileExtension: config.ImplantConfig.StagerFileExt,
|
||||
PollFileExtension: config.ImplantConfig.PollFileExt,
|
||||
StartSessionFileExtension: config.ImplantConfig.StartSessionFileExt,
|
||||
SessionFileExtension: config.ImplantConfig.SessionFileExt,
|
||||
CloseFileExtension: config.ImplantConfig.CloseFileExt,
|
||||
PathSegments: pathSegments,
|
||||
UserAgent: config.ImplantConfig.UserAgent,
|
||||
ChromeBaseVersion: int32(config.ImplantConfig.ChromeBaseVersion),
|
||||
MacOSVersion: config.ImplantConfig.MacOSVersion,
|
||||
NonceQueryArgChars: config.ImplantConfig.NonceQueryArgChars,
|
||||
NonceQueryLength: int32(config.ImplantConfig.NonceQueryLength),
|
||||
NonceMode: config.ImplantConfig.NonceMode,
|
||||
ExtraURLParameters: httpC2UrlParameters,
|
||||
Headers: httpC2Headers,
|
||||
MaxFileGen: int32(config.ImplantConfig.MaxFileGen),
|
||||
MinFileGen: int32(config.ImplantConfig.MinFileGen),
|
||||
MaxPathGen: int32(config.ImplantConfig.MaxPathGen),
|
||||
MinPathGen: int32(config.ImplantConfig.MinPathGen),
|
||||
MaxPathLength: int32(config.ImplantConfig.MaxPathLength),
|
||||
MinPathLength: int32(config.ImplantConfig.MinPathLength),
|
||||
Extensions: config.ImplantConfig.Extensions,
|
||||
PathSegments: pathSegments,
|
||||
}
|
||||
|
||||
// Server Config
|
||||
serverHeaders := []*clientpb.HTTPC2Header{}
|
||||
for _, serverHeader := range config.ServerConfig.Headers {
|
||||
for _, method := range serverHeader.Methods {
|
||||
serverHeaders = append(serverHeaders, &clientpb.HTTPC2Header{
|
||||
Method: method,
|
||||
Name: serverHeader.Name,
|
||||
Value: serverHeader.Value,
|
||||
Probability: int32(serverHeader.Probability),
|
||||
})
|
||||
}
|
||||
serverHeaders = append(serverHeaders, &clientpb.HTTPC2Header{
|
||||
Method: serverHeader.Method,
|
||||
Name: serverHeader.Name,
|
||||
Value: serverHeader.Value,
|
||||
Probability: int32(serverHeader.Probability),
|
||||
})
|
||||
}
|
||||
|
||||
serverCookies := []*clientpb.HTTPC2Cookie{}
|
||||
@@ -525,7 +432,7 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
|
||||
|
||||
var serverHeaders []string
|
||||
for _, header := range profile.ServerConfig.Headers {
|
||||
serverHeaders = append(serverHeaders, header.Value)
|
||||
serverHeaders = append(serverHeaders, header.Name)
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
"Server Headers",
|
||||
@@ -550,7 +457,7 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
|
||||
|
||||
var clientHeaders []string
|
||||
for _, header := range profile.ImplantConfig.Headers {
|
||||
clientHeaders = append(clientHeaders, header.Value)
|
||||
clientHeaders = append(clientHeaders, header.Name)
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
"Client Headers",
|
||||
@@ -581,126 +488,94 @@ func PrintC2Profiles(profile *clientpb.HTTPC2Config, con *console.SliverClient)
|
||||
"Nonce query arg chars",
|
||||
profile.ImplantConfig.NonceQueryArgChars,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Nonce query length",
|
||||
profile.ImplantConfig.NonceQueryLength,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Nonce mode",
|
||||
profile.ImplantConfig.NonceMode,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Max files",
|
||||
profile.ImplantConfig.MaxFiles,
|
||||
profile.ImplantConfig.MaxFileGen,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Min files",
|
||||
profile.ImplantConfig.MinFiles,
|
||||
profile.ImplantConfig.MinFileGen,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Max paths",
|
||||
profile.ImplantConfig.MaxPaths,
|
||||
profile.ImplantConfig.MaxPathGen,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Min paths",
|
||||
profile.ImplantConfig.MinPaths,
|
||||
profile.ImplantConfig.MinPathGen,
|
||||
})
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
"Stager file extension",
|
||||
profile.ImplantConfig.StagerFileExtension,
|
||||
"Max path length",
|
||||
profile.ImplantConfig.MaxPathLength,
|
||||
})
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
"Start session file extension",
|
||||
profile.ImplantConfig.StartSessionFileExtension,
|
||||
"Min path length",
|
||||
profile.ImplantConfig.MinPathLength,
|
||||
})
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
"Session file extension",
|
||||
profile.ImplantConfig.SessionFileExtension,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Poll file extension",
|
||||
profile.ImplantConfig.PollFileExtension,
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Close file extension",
|
||||
profile.ImplantConfig.CloseFileExtension,
|
||||
"File extensions",
|
||||
strings.Join(profile.ImplantConfig.Extensions, ","),
|
||||
})
|
||||
|
||||
var (
|
||||
pollPaths []string
|
||||
pollFiles []string
|
||||
sessionPaths []string
|
||||
sessionFiles []string
|
||||
closePaths []string
|
||||
closeFiles []string
|
||||
paths []string
|
||||
files []string
|
||||
)
|
||||
for _, segment := range profile.ImplantConfig.PathSegments {
|
||||
if segment.IsFile {
|
||||
switch segment.SegmentType {
|
||||
case 0:
|
||||
pollFiles = append(pollFiles, segment.Value)
|
||||
case 1:
|
||||
sessionFiles = append(sessionFiles, segment.Value)
|
||||
case 2:
|
||||
closeFiles = append(closeFiles, segment.Value)
|
||||
}
|
||||
files = append(files, segment.Value)
|
||||
} else {
|
||||
switch segment.SegmentType {
|
||||
case 0:
|
||||
pollPaths = append(pollPaths, segment.Value)
|
||||
case 1:
|
||||
sessionPaths = append(sessionPaths, segment.Value)
|
||||
case 2:
|
||||
closePaths = append(closePaths, segment.Value)
|
||||
}
|
||||
paths = append(paths, segment.Value)
|
||||
}
|
||||
}
|
||||
|
||||
tw.AppendRow(table.Row{
|
||||
"Poll paths",
|
||||
strings.Join(pollPaths[:], ","),
|
||||
"Paths",
|
||||
strings.Join(paths[:], ",")[:50] + "...",
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Poll files",
|
||||
strings.Join(pollFiles[:], ","),
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Session paths",
|
||||
strings.Join(sessionPaths[:], ","),
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Session files",
|
||||
strings.Join(sessionFiles[:], ","),
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Close paths",
|
||||
strings.Join(closePaths[:], ","),
|
||||
})
|
||||
tw.AppendRow(table.Row{
|
||||
"Close files",
|
||||
strings.Join(closeFiles[:], ","),
|
||||
"Files",
|
||||
strings.Join(files[:], ",")[:50] + "...",
|
||||
})
|
||||
|
||||
con.Println(tw.Render())
|
||||
con.Println("\n")
|
||||
}
|
||||
|
||||
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
|
||||
func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) (string, error) {
|
||||
c2profile := ""
|
||||
var choices []string
|
||||
for _, c2profile := range c2profiles {
|
||||
choices = append(choices, c2profile.Name)
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a c2 profile",
|
||||
Options: choices,
|
||||
}
|
||||
survey.AskOne(prompt, &c2profile, nil)
|
||||
_ = forms.Select("Select a c2 profile", choices, &c2profile)
|
||||
|
||||
return c2profile
|
||||
if c2profile == "" {
|
||||
return "", ErrNoSelection
|
||||
}
|
||||
|
||||
return c2profile, nil
|
||||
}
|
||||
|
||||
func updateC2Profile(usedExtensions []string, template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
|
||||
func updateC2Profile(template *assets.HTTPC2Config, urls []string) (*assets.HTTPC2Config, error) {
|
||||
// update the template with the urls
|
||||
|
||||
var (
|
||||
paths []string
|
||||
filenames []string
|
||||
extensions []string
|
||||
filteredExtensions []string
|
||||
paths []string
|
||||
filenames []string
|
||||
extensions []string
|
||||
)
|
||||
|
||||
for _, urlPath := range urls {
|
||||
@@ -729,23 +604,13 @@ func updateC2Profile(usedExtensions []string, template *assets.HTTPC2Config, url
|
||||
slices.Sort(extensions)
|
||||
extensions = slices.Compact(extensions)
|
||||
|
||||
for _, extension := range extensions {
|
||||
if !slices.Contains(usedExtensions, extension) {
|
||||
filteredExtensions = append(filteredExtensions, extension)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(paths)
|
||||
paths = slices.Compact(paths)
|
||||
|
||||
slices.Sort(filenames)
|
||||
filenames = slices.Compact(filenames)
|
||||
|
||||
// 5 is arbitrarily used as a minimum value, it only has to be 5 for the extensions, the others can be lower
|
||||
if len(filteredExtensions) < 5 {
|
||||
return nil, fmt.Errorf("got %d unused extensions, need at least 5", len(filteredExtensions))
|
||||
}
|
||||
|
||||
// 5 is arbitrarily used as a minimum value
|
||||
if len(paths) < 5 {
|
||||
return nil, fmt.Errorf("got %d paths need at least 5", len(paths))
|
||||
}
|
||||
@@ -754,53 +619,8 @@ func updateC2Profile(usedExtensions []string, template *assets.HTTPC2Config, url
|
||||
return nil, fmt.Errorf("got %d paths need at least 5", len(filenames))
|
||||
}
|
||||
|
||||
// shuffle extensions
|
||||
for i := len(extensions) - 1; i > 0; i-- {
|
||||
j := rand.Intn(i + 1)
|
||||
extensions[i], extensions[j] = extensions[j], extensions[i]
|
||||
}
|
||||
|
||||
template.ImplantConfig.PollFileExt = extensions[0]
|
||||
template.ImplantConfig.StagerFileExt = extensions[1]
|
||||
template.ImplantConfig.StartSessionFileExt = extensions[2]
|
||||
template.ImplantConfig.SessionFileExt = extensions[3]
|
||||
template.ImplantConfig.CloseFileExt = extensions[4]
|
||||
|
||||
// randomly distribute the paths and filenames into the different segment types
|
||||
template.ImplantConfig.CloseFiles = []string{}
|
||||
template.ImplantConfig.SessionFiles = []string{}
|
||||
template.ImplantConfig.PollFiles = []string{}
|
||||
template.ImplantConfig.StagerFiles = []string{}
|
||||
template.ImplantConfig.ClosePaths = []string{}
|
||||
template.ImplantConfig.SessionPaths = []string{}
|
||||
template.ImplantConfig.PollPaths = []string{}
|
||||
template.ImplantConfig.StagerPaths = []string{}
|
||||
|
||||
for _, path := range paths {
|
||||
switch rand.Intn(4) {
|
||||
case 0:
|
||||
template.ImplantConfig.PollPaths = append(template.ImplantConfig.PollPaths, path)
|
||||
case 1:
|
||||
template.ImplantConfig.SessionPaths = append(template.ImplantConfig.SessionPaths, path)
|
||||
case 2:
|
||||
template.ImplantConfig.ClosePaths = append(template.ImplantConfig.ClosePaths, path)
|
||||
case 3:
|
||||
template.ImplantConfig.StagerPaths = append(template.ImplantConfig.StagerPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, filename := range filenames {
|
||||
switch rand.Intn(4) {
|
||||
case 0:
|
||||
template.ImplantConfig.PollFiles = append(template.ImplantConfig.PollFiles, filename)
|
||||
case 1:
|
||||
template.ImplantConfig.SessionFiles = append(template.ImplantConfig.SessionFiles, filename)
|
||||
case 2:
|
||||
template.ImplantConfig.CloseFiles = append(template.ImplantConfig.CloseFiles, filename)
|
||||
case 3:
|
||||
template.ImplantConfig.StagerFiles = append(template.ImplantConfig.StagerFiles, filename)
|
||||
}
|
||||
}
|
||||
|
||||
template.ImplantConfig.Extensions = extensions
|
||||
template.ImplantConfig.Paths = paths
|
||||
template.ImplantConfig.Files = filenames
|
||||
return template, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package c2profiles
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("file", "f", "", "Path to C2 configuration file to import")
|
||||
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
|
||||
|
||||
exportC2ProfileCmd := &cobra.Command{
|
||||
Use: consts.ExportC2ProfileStr,
|
||||
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
|
||||
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
|
||||
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["name"] = generate.HTTPC2Completer(con)
|
||||
})
|
||||
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
|
||||
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
|
||||
|
||||
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["template"] = generate.HTTPC2Completer(con)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# client/command/certificates
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'certificates' command group for the Sliver client console.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `authorities.go` – Retrieves certificate authority metadata from the server and prints it in a table.
|
||||
- `completions.go` – Provides tab completion helpers for certificate common names.
|
||||
- `certificates.go` – Retrieves certificate metadata from the server, formats expiration status, and prints results to the console.
|
||||
- `commands.go` – Registers the certificates command and binds the flags needed to query certificate inventories.
|
||||
@@ -0,0 +1,89 @@
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func CertificateAuthoritiesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
authorityInfo, err := con.Rpc.GetCertificateAuthorityInfo(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not get certificate authority information from database: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
printCertificateAuthorityInfo(con, authorityInfo.Info)
|
||||
}
|
||||
|
||||
func printCertificateAuthorityInfo(con *console.SliverClient, authData []*clientpb.CertificateAuthorityData) {
|
||||
width, _, err := term.GetSize(0)
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
|
||||
if len(authData) == 0 {
|
||||
con.PrintWarnf("There are no certificate authorities in the database.\n")
|
||||
return
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
wideTermWidth := con.Settings.SmallTermWidth < width
|
||||
|
||||
if wideTermWidth {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Common Name",
|
||||
"Creation Time",
|
||||
"Authority Type",
|
||||
"Key Algorithm",
|
||||
"Validity Start",
|
||||
"Expires",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Common Name",
|
||||
"Expires",
|
||||
})
|
||||
}
|
||||
|
||||
for _, authority := range authData {
|
||||
rowStyle := console.StyleNormal
|
||||
|
||||
expiry, err := time.Parse(timeFormat, authority.ValidityExpiry)
|
||||
if err == nil {
|
||||
rowStyle = checkCertExpiry(expiry)
|
||||
}
|
||||
|
||||
if wideTermWidth {
|
||||
tw.AppendRow(table.Row{
|
||||
rowStyle.Render(authority.ID),
|
||||
rowStyle.Render(authority.CN),
|
||||
rowStyle.Render(authority.CreationTime),
|
||||
rowStyle.Render(authority.Type),
|
||||
rowStyle.Render(authority.KeyAlgorithm),
|
||||
rowStyle.Render(authority.ValidityStart),
|
||||
rowStyle.Render(authority.ValidityExpiry),
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{
|
||||
rowStyle.Render(authority.ID),
|
||||
rowStyle.Render(authority.CN),
|
||||
rowStyle.Render(authority.ValidityExpiry),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tw.SortBy([]table.SortBy{{Name: "Expires", Mode: table.Dsc}})
|
||||
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
@@ -95,22 +94,22 @@ func CertificateInfoCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
printCertificateInfo(con, certificateInfo.Info)
|
||||
}
|
||||
|
||||
func checkCertExpiry(expiryTime time.Time) string {
|
||||
func checkCertExpiry(expiryTime time.Time) console.TextStyle {
|
||||
if expiryTime.Before(time.Now()) || expiryTime.Equal(time.Now()) {
|
||||
return console.Bold + console.Red
|
||||
return console.StyleBoldRed
|
||||
}
|
||||
|
||||
// One week is 168 hours - this is bad
|
||||
if expiryTime.Before(time.Now().Add(168 * time.Hour)) {
|
||||
return console.Bold + console.Red
|
||||
return console.StyleBoldRed
|
||||
}
|
||||
|
||||
// One month is approximately 730 hours - this is a warning
|
||||
if expiryTime.Before(time.Now().Add(730 * time.Hour)) {
|
||||
return console.Bold + console.Orange
|
||||
return console.StyleBoldOrange
|
||||
}
|
||||
|
||||
return console.Normal
|
||||
return console.StyleNormal
|
||||
}
|
||||
|
||||
func printCertificateInfo(con *console.SliverClient, certData []*clientpb.CertificateData) {
|
||||
@@ -148,28 +147,28 @@ func printCertificateInfo(con *console.SliverClient, certData []*clientpb.Certif
|
||||
}
|
||||
|
||||
for _, cert := range certData {
|
||||
rowColor := console.Normal
|
||||
rowStyle := console.StyleNormal
|
||||
|
||||
expiry, err := time.Parse(timeFormat, cert.ValidityExpiry)
|
||||
// This should not error out, but if it does, the row will not be colored
|
||||
if err == nil {
|
||||
rowColor = checkCertExpiry(expiry)
|
||||
rowStyle = checkCertExpiry(expiry)
|
||||
}
|
||||
if wideTermWidth {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CreationTime),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.Type),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.KeyAlgorithm),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityStart),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
||||
rowStyle.Render(cert.ID),
|
||||
rowStyle.Render(cert.CN),
|
||||
rowStyle.Render(cert.CreationTime),
|
||||
rowStyle.Render(cert.Type),
|
||||
rowStyle.Render(cert.KeyAlgorithm),
|
||||
rowStyle.Render(cert.ValidityStart),
|
||||
rowStyle.Render(cert.ValidityExpiry),
|
||||
})
|
||||
} else {
|
||||
tw.AppendRow(table.Row{
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ID),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.CN),
|
||||
fmt.Sprintf(rowColor+"%s"+console.Normal, cert.ValidityExpiry),
|
||||
rowStyle.Render(cert.ID),
|
||||
rowStyle.Render(cert.CN),
|
||||
rowStyle.Render(cert.ValidityExpiry),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -44,6 +45,19 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.BoolP("server", "s", false, "Show server certificates")
|
||||
f.StringP("cn", "c", "", "Show certificate information for a provided common name")
|
||||
})
|
||||
flags.BindFlagCompletions(certificatesCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["cn"] = CertificateCommonNameCompleter(con)
|
||||
})
|
||||
registerCertificateCNFlagCompletion(certificatesCmd, "cn", con)
|
||||
|
||||
authoritiesCmd := &cobra.Command{
|
||||
Use: "authorities",
|
||||
Short: "List certificate authorities",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CertificateAuthoritiesCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
certificatesCmd.AddCommand(authoritiesCmd)
|
||||
|
||||
return []*cobra.Command{
|
||||
certificatesCmd,
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package certificates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CertificateCommonNameCompleter completes common names for stored certificates.
|
||||
func CertificateCommonNameCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return carapace.ActionMessage("not connected")
|
||||
}
|
||||
|
||||
values, err := certificateCommonNameValues(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to list certificates %s", err)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return carapace.ActionMessage("no certificates")
|
||||
}
|
||||
|
||||
return carapace.ActionValues(values...).Tag("common name").Usage("certificate common name")
|
||||
})
|
||||
}
|
||||
|
||||
func registerCertificateCNFlagCompletion(cmd *cobra.Command, name string, con *console.SliverClient) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
values, err := certificateCommonNameValues(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return filterCompletionValues(values, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
|
||||
func certificateCommonNameValues(con *console.SliverClient) ([]string, error) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
certInfo, err := con.Rpc.GetCertificateInfo(context.Background(), &clientpb.CertificatesReq{
|
||||
CategoryFilters: AllTransports | AllRoles,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unique := map[string]struct{}{}
|
||||
for _, cert := range certInfo.Info {
|
||||
if cert.CN == "" {
|
||||
continue
|
||||
}
|
||||
unique[cert.CN] = struct{}{}
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(unique))
|
||||
for name := range unique {
|
||||
values = append(values, name)
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func filterCompletionValues(values []string, prefix string) []string {
|
||||
if prefix == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# client/command/clean
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'clean' command group for the Sliver client console.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `clean.go` – Defines the clean command and its helpers that purge local build artifacts, cached profiles, and stale sessions.
|
||||
@@ -21,10 +21,10 @@ package clean
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
@@ -35,8 +35,7 @@ import (
|
||||
func CleanCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.Printf("This command will kill and remove all sessions, beacons and profiles \n")
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Are you sure you want to destroy everything?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Are you sure you want to destroy everything?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
Completers
|
||||
===========
|
||||
# client/command/completers
|
||||
|
||||
This package contains general purpose completers for the console, note that some package also contain their own package-specific completers.
|
||||
## Overview
|
||||
|
||||
Implements the 'completers' command group for the Sliver client console.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `completers.go` – Provides reusable carapace completion helpers for selecting client interfaces and proxy endpoints.
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ClientInterfacesCompleter completes interface addresses on the client host.
|
||||
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
|
||||
}).Invoke(c).Prefix(prefix).ToA()
|
||||
})
|
||||
}
|
||||
|
||||
// LocalFilePathCompleter completes local file and directory paths.
|
||||
func LocalFilePathCompleter() carapace.Action {
|
||||
return carapace.ActionFiles().Tag("local file path")
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
|
||||
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
|
||||
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
|
||||
for _, name := range names {
|
||||
RegisterLocalFilePathFlagCompletion(cmd, name)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
|
||||
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
|
||||
if cmd == nil || index < 0 {
|
||||
return
|
||||
}
|
||||
if cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == index {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
|
||||
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# client/command/crack
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as cached benchmark inspection, durable jobs, real-time monitoring, and crack files.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Defines the crack command hierarchy, attaching operations for stations and file management.
|
||||
- `crack-benchmarks.go` – Displays bounded cached crackstation benchmark summaries, freshness metadata, and representative or complete hash-mode rates.
|
||||
- `crack-files.go` – Manages cracking asset uploads, listings, and removals, handling streaming transfers and chunking.
|
||||
- `crack-jobs.go` – Lists durable cracking jobs, renders detailed task progress, manages pause/resume/cancel lifecycle actions, and removes terminal jobs after confirmation.
|
||||
- `top.go` – Preserves the crack command's compatibility entry point and delegates to the dedicated monitor package.
|
||||
- `top/` – Owns the full-screen Bubble Tea monitor, its lean real-time snapshot loader, live telemetry, history, charts, actions, and tests.
|
||||
- `internal/hashcatdisplay/` – Shares Hashcat mode names and rate formatting between the command handlers and monitor without coupling the packages.
|
||||
- `crack.go` – Implements station status reporting, health checks, and list rendering for distributed cracking nodes.
|
||||
- `helpers.go` – Supplies completion helpers and byte formatting utilities for crack subcommands.
|
||||
@@ -1,6 +1,9 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -25,6 +28,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", true, crackCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("", false, crackCmd, func(f *pflag.FlagSet) {
|
||||
bindCrackFlags(f)
|
||||
})
|
||||
flags.BindFlagCompletions(crackCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["input"] = CrackWordlistCompleter(con)
|
||||
(*comp)["rules-file"] = CrackRulesCompleter(con)
|
||||
(*comp)["markov-hcstat2"] = CrackHcstat2Completer(con)
|
||||
})
|
||||
|
||||
crackStationsCmd := &cobra.Command{
|
||||
Use: consts.StationsStr,
|
||||
@@ -34,8 +45,117 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
CrackStationsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, crackStationsCmd, func(f *pflag.FlagSet) {
|
||||
f.Bool("show-benchmarks", false, "show benchmark rates for crackstations")
|
||||
})
|
||||
crackCmd.AddCommand(crackStationsCmd)
|
||||
|
||||
crackBenchmarksCmd := &cobra.Command{
|
||||
Use: consts.BenchmarksStr,
|
||||
Short: "Show cached crackstation benchmark results",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
BenchmarksCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, crackBenchmarksCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("all", "a", false, "show every cached hash mode")
|
||||
})
|
||||
crackCmd.AddCommand(crackBenchmarksCmd)
|
||||
|
||||
crackJobsCmd := &cobra.Command{
|
||||
Use: "jobs",
|
||||
Short: "List hash cracking jobs",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", true, crackJobsCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
crackCmd.AddCommand(crackJobsCmd)
|
||||
|
||||
crackTopCmd := &cobra.Command{
|
||||
Use: "top",
|
||||
Short: "Monitor crack jobs and crackstations in real time",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackTopCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", false, crackTopCmd, func(f *pflag.FlagSet) {
|
||||
f.Duration("poll-interval", time.Second, "crack job and crackstation refresh interval")
|
||||
})
|
||||
crackCmd.AddCommand(crackTopCmd)
|
||||
|
||||
crackJobCmd := &cobra.Command{
|
||||
Use: "job [id]",
|
||||
Short: "Show a hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
flags.Bind("", true, crackJobCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("", false, crackJobCmd, func(f *pflag.FlagSet) {
|
||||
f.BoolP("watch", "w", false, "watch until the job reaches a terminal state")
|
||||
f.Duration("poll-interval", time.Second, "job status polling interval")
|
||||
})
|
||||
carapace.Gen(crackJobCmd).PositionalCompletion(CrackJobIDCompleter(con).Usage("crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletion(crackJobCmd, con)
|
||||
|
||||
crackJobCancelCmd := &cobra.Command{
|
||||
Use: consts.CancelStr + " [id]",
|
||||
Short: "Cancel an active or paused hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobCancelCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobCancelCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobCancellable).Usage("active or paused crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobCancelCmd, con, crackJobCancellable)
|
||||
crackJobCmd.AddCommand(crackJobCancelCmd)
|
||||
|
||||
crackJobPauseCmd := &cobra.Command{
|
||||
Use: "pause [id]",
|
||||
Short: "Pause an active hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobPauseCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobPauseCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobPausable).Usage("active crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobPauseCmd, con, crackJobPausable)
|
||||
crackJobCmd.AddCommand(crackJobPauseCmd)
|
||||
|
||||
crackJobResumeCmd := &cobra.Command{
|
||||
Use: "resume [id]",
|
||||
Short: "Resume a paused hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobResumeCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobResumeCmd).PositionalCompletion(crackJobIDCompleter(con, crackJobResumable).Usage("paused crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletionFiltered(crackJobResumeCmd, con, crackJobResumable)
|
||||
crackJobCmd.AddCommand(crackJobResumeCmd)
|
||||
|
||||
crackJobRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr + " [id]",
|
||||
Short: "Remove a completed, failed, or cancelled hash cracking job",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
CrackJobRmCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
carapace.Gen(crackJobRmCmd).PositionalCompletion(crackTerminalJobIDCompleter(con).Usage("terminal crack job ID (leave empty to select)"))
|
||||
registerCrackTerminalJobIDCompletion(crackJobRmCmd, con)
|
||||
crackJobCmd.AddCommand(crackJobRmCmd)
|
||||
crackCmd.AddCommand(crackJobCmd)
|
||||
|
||||
wordlistsCmd := &cobra.Command{
|
||||
Use: consts.WordlistsStr,
|
||||
Short: "Manage wordlists",
|
||||
@@ -57,6 +177,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "wordlist name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
|
||||
wordlistsCmd.AddCommand(wordlistsAddCmd)
|
||||
|
||||
wordlistsRmCmd := &cobra.Command{
|
||||
@@ -91,6 +212,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "rules name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
|
||||
rulesCmd.AddCommand(rulesAddCmd)
|
||||
|
||||
rulesRmCmd := &cobra.Command{
|
||||
@@ -126,6 +248,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
|
||||
hcstat2Cmd.AddCommand(hcstat2AddCmd)
|
||||
|
||||
hcstat2RmCmd := &cobra.Command{
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
type backendDeviceDetails struct {
|
||||
deviceType string
|
||||
vendor string
|
||||
vendorID int32
|
||||
version string
|
||||
runtimeVersion string
|
||||
deviceID uint32
|
||||
deviceAlias *uint32
|
||||
preferredThreadSize *uint32
|
||||
memoryUnified *bool
|
||||
localMemory string
|
||||
cacheSize string
|
||||
memoryAllocPerBlock string
|
||||
pciAddress string
|
||||
}
|
||||
|
||||
func hasReportedBackendDevice(cracker *clientpb.Crackstation) bool {
|
||||
for _, device := range cracker.CUDA {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.HIP {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.Metal {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, device := range cracker.OpenCL {
|
||||
if device != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendCrackstationDetailRows(tw table.Writer, cracker *clientpb.Crackstation) {
|
||||
if cracker.HostUUID != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), safeCrackCell(cracker.HostUUID)})
|
||||
}
|
||||
if cracker.Version != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Crackstation Version"), safeCrackCell(cracker.Version)})
|
||||
}
|
||||
}
|
||||
|
||||
func appendCUDABackendRows(tw table.Writer, device *clientpb.CUDABackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "CUDA Device", formatCUDADevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.CUDAVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
}
|
||||
|
||||
func appendHIPBackendRows(tw table.Writer, device *clientpb.HIPBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "HIP Device", formatHIPDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.HIPVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
}
|
||||
|
||||
func appendMetalBackendRows(tw table.Writer, device *clientpb.MetalBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "Metal Device", formatMetalDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.MetalVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
cacheSize: device.CacheSize,
|
||||
memoryAllocPerBlock: device.MemoryAllocPerBlock,
|
||||
})
|
||||
appendStringBackendDetailRow(tw, "Physical Location", device.PhysicalLocation)
|
||||
appendUint32BackendDetailRow(tw, "Registry ID", device.RegistryID)
|
||||
appendStringBackendDetailRow(tw, "Max TX Rate", device.MaxTXRate)
|
||||
appendBoolBackendDetailRow(tw, "Headless", device.GPUHeadless)
|
||||
appendBoolBackendDetailRow(tw, "Low Power", device.GPULowPower)
|
||||
appendBoolBackendDetailRow(tw, "Removable", device.GPURemovable)
|
||||
}
|
||||
|
||||
func appendOpenCLBackendRows(tw table.Writer, device *clientpb.OpenCLBackendInfo, level uint32) {
|
||||
if device == nil {
|
||||
return
|
||||
}
|
||||
appendBackendSummaryRows(tw, "OpenCL Device", formatOpenCLDevice(device), device.MemoryFree, device.MemoryTotal, device.Clock, device.Processors)
|
||||
if level < 2 {
|
||||
return
|
||||
}
|
||||
appendBackendDeviceDetailRows(tw, backendDeviceDetails{
|
||||
deviceType: device.Type,
|
||||
vendor: device.Vendor,
|
||||
vendorID: device.VendorID,
|
||||
version: device.Version,
|
||||
runtimeVersion: device.OpenCLVersion,
|
||||
deviceID: device.BackendDeviceID,
|
||||
deviceAlias: device.BackendDeviceAlias,
|
||||
preferredThreadSize: device.PreferredThreadSize,
|
||||
memoryUnified: device.MemoryUnified,
|
||||
localMemory: device.LocalMemory,
|
||||
memoryAllocPerBlock: device.MemoryAllocPerBlock,
|
||||
pciAddress: device.PCIAddress,
|
||||
})
|
||||
appendStringBackendDetailRow(tw, "OpenCL Driver", device.OpenCLDriverVersion)
|
||||
if device.PlatformID != 0 || device.PlatformVendor != "" || device.PlatformName != "" || device.PlatformVersion != "" {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("OpenCL Platform ID"), strconv.FormatUint(uint64(device.PlatformID), 10)})
|
||||
}
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform Vendor", device.PlatformVendor)
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform", device.PlatformName)
|
||||
appendStringBackendDetailRow(tw, "OpenCL Platform Version", device.PlatformVersion)
|
||||
}
|
||||
|
||||
func appendBackendSummaryRows(tw table.Writer, label, name, memoryFree, memoryTotal string, clock, processors int32) {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), console.StyleBoldGreen.Render(safeCrackCell(name))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Memory"), fmt.Sprintf("%s free of %s", valueOrDash(memoryFree), valueOrDash(memoryTotal))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Clock"), strconv.FormatInt(int64(clock), 10)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Processors"), strconv.FormatInt(int64(processors), 10)})
|
||||
}
|
||||
|
||||
func appendBackendDeviceDetailRows(tw table.Writer, details backendDeviceDetails) {
|
||||
appendStringBackendDetailRow(tw, "Device Type", details.deviceType)
|
||||
appendStringBackendDetailRow(tw, "Vendor", details.vendor)
|
||||
if details.vendorID != 0 {
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Vendor ID"), strconv.FormatInt(int64(details.vendorID), 10)})
|
||||
}
|
||||
if details.version != "" && details.version != details.runtimeVersion {
|
||||
appendStringBackendDetailRow(tw, "Device Version", details.version)
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Device ID"), strconv.FormatUint(uint64(details.deviceID), 10)})
|
||||
appendUint32BackendDetailRow(tw, "Backend Device Alias", details.deviceAlias)
|
||||
appendUint32BackendDetailRow(tw, "Preferred Thread Size", details.preferredThreadSize)
|
||||
appendBoolBackendDetailRow(tw, "Unified Memory", details.memoryUnified)
|
||||
appendStringBackendDetailRow(tw, "Local Memory", details.localMemory)
|
||||
appendStringBackendDetailRow(tw, "Cache Size", details.cacheSize)
|
||||
appendStringBackendDetailRow(tw, "Memory Allocation per Block", details.memoryAllocPerBlock)
|
||||
appendStringBackendDetailRow(tw, "PCI Address", details.pciAddress)
|
||||
}
|
||||
|
||||
func appendStringBackendDetailRow(tw table.Writer, label, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), safeCrackCell(value)})
|
||||
}
|
||||
|
||||
func appendUint32BackendDetailRow(tw table.Writer, label string, value *uint32) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatUint(uint64(*value), 10)})
|
||||
}
|
||||
|
||||
func appendBoolBackendDetailRow(tw table.Writer, label string, value *bool) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render(label), strconv.FormatBool(*value)})
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
|
||||
var representativeCrackBenchmarkModes = map[int32]struct{}{
|
||||
0: {}, // MD5
|
||||
100: {}, // SHA1
|
||||
1000: {}, // NTLM
|
||||
1400: {}, // SHA2-256
|
||||
3200: {}, // bcrypt
|
||||
22000: {}, // WPA-PBKDF2
|
||||
}
|
||||
|
||||
const (
|
||||
crackBenchmarkOutputWidth = 80
|
||||
crackBenchmarkHashTypeWidth = 48
|
||||
crackBenchmarkMetadataValueWidth = 48
|
||||
)
|
||||
|
||||
// BenchmarksCmd displays benchmark results persisted by the server.
|
||||
func BenchmarksCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
|
||||
response, err := con.Rpc.CrackstationBenchmarks(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
showAll, _ := cmd.Flags().GetBool("all")
|
||||
benchmarks := response.GetSnapshots()
|
||||
rendered := renderCrackBenchmarks(benchmarks, settings.GetTableStyle(con), showAll)
|
||||
if rendered == "" {
|
||||
con.PrintInfof("No cached crackstation benchmarks\n")
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", rendered)
|
||||
}
|
||||
|
||||
func renderCrackBenchmarks(benchmarks []*clientpb.CrackBenchmarkSnapshot, style table.Style, showAll bool) string {
|
||||
ordered := make([]*clientpb.CrackBenchmarkSnapshot, 0, len(benchmarks))
|
||||
for _, benchmark := range benchmarks {
|
||||
if benchmark != nil {
|
||||
ordered = append(ordered, benchmark)
|
||||
}
|
||||
}
|
||||
if len(ordered) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
leftName := safeCrackCell(ordered[i].GetName())
|
||||
rightName := safeCrackCell(ordered[j].GetName())
|
||||
if leftName == rightName {
|
||||
return safeCrackCell(ordered[i].GetHostUUID()) < safeCrackCell(ordered[j].GetHostUUID())
|
||||
}
|
||||
return leftName < rightName
|
||||
})
|
||||
|
||||
sections := make([]string, 0, len(ordered)*2+1)
|
||||
for index, benchmark := range ordered {
|
||||
identity := cachedBenchmarkIdentity(benchmark)
|
||||
title := renderCachedBenchmarkTitle(index, identity)
|
||||
metadata := table.NewWriter()
|
||||
metadata.SetStyle(boundedCrackBenchmarkStyle(style))
|
||||
metadata.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 2, WidthMax: crackBenchmarkMetadataValueWidth},
|
||||
})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Name"), valueOrDash(benchmark.GetName())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Host UUID"), console.StyleBoldPrimary.Render(valueOrDash(benchmark.GetHostUUID()))})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Operator"), valueOrDash(benchmark.GetOperatorName())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Connection"), renderCachedBenchmarkConnection(benchmark.GetOnline())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Cache"), renderCachedBenchmarkFreshness(benchmark.GetFresh())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Current Hashcat Version"), valueOrDash(benchmark.GetCurrentHashcatVersion())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Hashcat Version"), valueOrDash(benchmark.GetBenchmarkHashcatVersion())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmark Schema"), strconv.FormatUint(uint64(benchmark.GetBenchmarkSchemaVersion()), 10)})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Benchmarked At"), formatCachedBenchmarkTime(benchmark.GetBenchmarkedAt())})
|
||||
metadata.AppendRow(table.Row{console.StyleBold.Render("Cached Modes"), strconv.Itoa(len(benchmark.GetBenchmarks()))})
|
||||
sections = append(sections, title+"\n"+metadata.Render())
|
||||
|
||||
rates := table.NewWriter()
|
||||
rates.SetStyle(boundedCrackBenchmarkStyle(style))
|
||||
rates.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Name: "Hash Type", WidthMax: crackBenchmarkHashTypeWidth},
|
||||
})
|
||||
if showAll {
|
||||
rates.SetTitle(console.StyleBold.Render("All Cached Rates"))
|
||||
} else {
|
||||
rates.SetTitle(console.StyleBold.Render("Representative Rates"))
|
||||
}
|
||||
rates.AppendHeader(table.Row{"Mode", "Hash Type", "Rate"})
|
||||
modes := cachedBenchmarkModes(benchmark.GetBenchmarks(), showAll)
|
||||
if len(modes) == 0 {
|
||||
rates.AppendRow(table.Row{"-", "No benchmark rates cached", "-"})
|
||||
} else {
|
||||
for _, mode := range modes {
|
||||
rates.AppendRow(table.Row{
|
||||
strconv.FormatInt(int64(mode), 10),
|
||||
crackBenchmarkModeName(mode),
|
||||
humanizeHashRate(benchmark.GetBenchmarks()[mode]),
|
||||
})
|
||||
}
|
||||
}
|
||||
sections = append(sections, rates.Render())
|
||||
}
|
||||
if !showAll {
|
||||
sections = append(sections, console.StyleGray.Render("Showing representative rates; use --all for every cached mode."))
|
||||
}
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func boundedCrackBenchmarkStyle(style table.Style) table.Style {
|
||||
style.Size.WidthMax = crackBenchmarkOutputWidth
|
||||
return style
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkTitle(index int, identity string) string {
|
||||
prefix := fmt.Sprintf(">>> Cached Crackstation %02d - ", index+1)
|
||||
maximumIdentityWidth := crackBenchmarkOutputWidth - ansi.StringWidth(prefix)
|
||||
if maximumIdentityWidth < 1 {
|
||||
return console.StyleBoldPrimary.Render(ansi.Truncate(prefix, crackBenchmarkOutputWidth, "…"))
|
||||
}
|
||||
identity = ansi.Truncate(identity, maximumIdentityWidth, "…")
|
||||
return console.StyleBoldPrimary.Render(prefix + identity)
|
||||
}
|
||||
|
||||
func cachedBenchmarkIdentity(benchmark *clientpb.CrackBenchmarkSnapshot) string {
|
||||
if benchmark == nil {
|
||||
return "Unknown crackstation"
|
||||
}
|
||||
if name := safeCrackCell(benchmark.GetName()); name != "" {
|
||||
return name
|
||||
}
|
||||
if hostUUID := safeCrackCell(benchmark.GetHostUUID()); hostUUID != "" {
|
||||
return hostUUID
|
||||
}
|
||||
return "Unknown crackstation"
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkConnection(online bool) string {
|
||||
if online {
|
||||
return console.StyleBoldSuccess.Render("Online")
|
||||
}
|
||||
return console.StyleBoldGray.Render("Offline")
|
||||
}
|
||||
|
||||
func renderCachedBenchmarkFreshness(fresh bool) string {
|
||||
if fresh {
|
||||
return console.StyleBoldSuccess.Render("Fresh")
|
||||
}
|
||||
return console.StyleBoldWarning.Render("Stale")
|
||||
}
|
||||
|
||||
func formatCachedBenchmarkTime(unix int64) string {
|
||||
if unix == 0 {
|
||||
return "-"
|
||||
}
|
||||
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func cachedBenchmarkModes(benchmarks map[int32]uint64, showAll bool) []int32 {
|
||||
modes := make([]int32, 0, len(benchmarks))
|
||||
for mode := range benchmarks {
|
||||
if showAll {
|
||||
modes = append(modes, mode)
|
||||
continue
|
||||
}
|
||||
if _, representative := representativeCrackBenchmarkModes[mode]; representative {
|
||||
modes = append(modes, mode)
|
||||
}
|
||||
}
|
||||
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
|
||||
if showAll || len(modes) != 0 {
|
||||
return modes
|
||||
}
|
||||
|
||||
for mode := range benchmarks {
|
||||
modes = append(modes, mode)
|
||||
}
|
||||
sort.Slice(modes, func(i, j int) bool { return modes[i] < modes[j] })
|
||||
if len(modes) > 6 {
|
||||
modes = modes[:6]
|
||||
}
|
||||
return modes
|
||||
}
|
||||
|
||||
func crackBenchmarkModeName(mode int32) string {
|
||||
if name, ok := hashcatHashTypeName(mode); ok {
|
||||
return name
|
||||
}
|
||||
if enumName := clientpb.HashType(mode).String(); enumName != strconv.FormatInt(int64(mode), 10) {
|
||||
return enumName
|
||||
}
|
||||
return "Unknown hash mode"
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestCrackBenchmarksCommandIsRegistered(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
benchmarks, _, err := root.Find([]string{"benchmarks"})
|
||||
if err != nil || benchmarks == root {
|
||||
t.Fatalf("find crack benchmarks: command=%v error=%v", benchmarks, err)
|
||||
}
|
||||
if benchmarks.Use != "benchmarks" {
|
||||
t.Fatalf("crack benchmarks use = %q, want benchmarks", benchmarks.Use)
|
||||
}
|
||||
if err := benchmarks.Args(benchmarks, nil); err != nil {
|
||||
t.Fatalf("crack benchmarks rejected no arguments: %v", err)
|
||||
}
|
||||
if err := benchmarks.Args(benchmarks, []string{"unexpected"}); err == nil {
|
||||
t.Fatal("crack benchmarks accepted a positional argument")
|
||||
}
|
||||
|
||||
allFlag := benchmarks.Flags().Lookup("all")
|
||||
if allFlag == nil {
|
||||
t.Fatal("crack benchmarks is missing --all")
|
||||
}
|
||||
if allFlag.Shorthand != "a" {
|
||||
t.Fatalf("--all shorthand = %q, want %q", allFlag.Shorthand, "a")
|
||||
}
|
||||
if benchmarks.PersistentFlags().Lookup("all") != nil || root.PersistentFlags().Lookup("all") != nil {
|
||||
t.Fatal("--all must remain local to crack benchmarks")
|
||||
}
|
||||
if benchmarks.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack benchmarks must inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksSortsStationsAndRepresentativeModes(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{
|
||||
nil,
|
||||
{
|
||||
Name: "zeta",
|
||||
HostUUID: "host-z",
|
||||
OperatorName: "operator-z",
|
||||
CurrentHashcatVersion: "v7.1.3",
|
||||
BenchmarkHashcatVersion: "v7.1.2",
|
||||
BenchmarkSchemaVersion: 1,
|
||||
BenchmarkedAt: 1_700_000_000,
|
||||
Online: true,
|
||||
Fresh: true,
|
||||
Benchmarks: map[int32]uint64{
|
||||
22000: 6_000_000_000_000,
|
||||
0: 999,
|
||||
3200: 5_000_000_000,
|
||||
100: 1_000,
|
||||
1400: 3_000_000,
|
||||
1000: 2_000_000,
|
||||
99999: 7_000_000,
|
||||
},
|
||||
},
|
||||
{Name: "alpha", HostUUID: "host-b", Benchmarks: map[int32]uint64{0: 1}},
|
||||
{Name: "alpha", HostUUID: "host-a", Benchmarks: map[int32]uint64{0: 2}},
|
||||
}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
requireOrderedCrackBenchmarkText(t, output, "host-a", "host-b", "host-z")
|
||||
zetaStart := strings.Index(output, ">>> Cached Crackstation 03 - zeta")
|
||||
if zetaStart < 0 {
|
||||
t.Fatalf("rendered benchmarks are missing sorted zeta section:\n%s", output)
|
||||
}
|
||||
requireOrderedCrackBenchmarkText(t, output[zetaStart:],
|
||||
"MD5", "SHA1", "NTLM", "SHA2-256", "bcrypt $2\\*$, Blowfish (Unix)", "WPA-PBKDF2-PMKID+EAPOL",
|
||||
)
|
||||
|
||||
for _, expected := range []string{
|
||||
"Name", "Host UUID", "Operator", "Connection", "Online", "Cache", "Fresh",
|
||||
"Current Hashcat Version", "v7.1.3", "Benchmark Hashcat Version", "v7.1.2",
|
||||
"Benchmark Schema", "Benchmarked At", "2023-11-14T22:13:20Z", "Cached Modes",
|
||||
"operator-z", "Representative Rates", "Showing representative rates; use --all for every cached mode.",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered benchmarks do not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
requireCrackBenchmarkRow(t, output, 0, "MD5", "999 H/s")
|
||||
requireCrackBenchmarkRow(t, output, 100, "SHA1", "1.00 kH/s")
|
||||
requireCrackBenchmarkRow(t, output, 1000, "NTLM", "2.00 MH/s")
|
||||
requireCrackBenchmarkRow(t, output, 1400, "SHA2-256", "3.00 MH/s")
|
||||
requireCrackBenchmarkRow(t, output, 3200, "bcrypt $2\\*$, Blowfish (Unix)", "5.00 GH/s")
|
||||
requireCrackBenchmarkRow(t, output, 22000, "WPA-PBKDF2-PMKID+EAPOL", "6.00 TH/s")
|
||||
if hasCrackBenchmarkRow(output, 99999, "Plaintext", "7.00 MH/s") {
|
||||
t.Fatalf("default benchmark view unexpectedly contains non-representative mode 99999:\n%s", output)
|
||||
}
|
||||
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldPrimary.Render(">>> Cached Crackstation 01 - alpha"),
|
||||
console.StyleBoldPrimary.Render("host-a"),
|
||||
console.StyleBoldSuccess.Render("Online"),
|
||||
console.StyleBoldSuccess.Render("Fresh"),
|
||||
console.StyleBold.Render("Representative Rates"),
|
||||
console.StyleGray.Render("Showing representative rates; use --all for every cached mode."),
|
||||
} {
|
||||
if !strings.Contains(raw, styled) {
|
||||
t.Errorf("rendered benchmarks are missing shared Lip Gloss styling %q:\n%s", styled, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksFallsBackToFirstSixModes(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "fallback",
|
||||
Benchmarks: map[int32]uint64{
|
||||
23: 23,
|
||||
10: 10,
|
||||
22: 22,
|
||||
12: 12,
|
||||
20: 20,
|
||||
11: 11,
|
||||
21: 21,
|
||||
},
|
||||
}}
|
||||
|
||||
output := ansi.Strip(renderCrackBenchmarks(benchmarks, settings.SliverDefault, false))
|
||||
requireOrderedCrackBenchmarkText(t, output,
|
||||
"md5($pass.$salt)",
|
||||
"Joomla < 2.5.18",
|
||||
"PostgreSQL",
|
||||
"md5($salt.$pass)",
|
||||
"osCommerce, xt:Commerce",
|
||||
"Juniper NetScreen/SSG (ScreenOS)",
|
||||
)
|
||||
if hasCrackBenchmarkRow(output, 23, "Skype", "23 H/s") {
|
||||
t.Fatalf("fallback benchmark view contains more than six modes:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksAllShowsEveryModeAndUnknownFallback(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "all-modes",
|
||||
Benchmarks: map[int32]uint64{
|
||||
424242: 1_234,
|
||||
36100: 3_600,
|
||||
99999: 7_000_000,
|
||||
0: 500,
|
||||
},
|
||||
}}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, true)
|
||||
output := ansi.Strip(raw)
|
||||
for _, row := range []struct {
|
||||
mode int32
|
||||
hashType string
|
||||
rate string
|
||||
}{
|
||||
{mode: 0, hashType: "MD5", rate: "500 H/s"},
|
||||
{mode: 36100, hashType: "YESCRYPT", rate: "3.60 kH/s"},
|
||||
{mode: 99999, hashType: "Plaintext", rate: "7.00 MH/s"},
|
||||
{mode: 424242, hashType: "Unknown hash mode", rate: "1.23 kH/s"},
|
||||
} {
|
||||
requireCrackBenchmarkRow(t, output, row.mode, row.hashType, row.rate)
|
||||
}
|
||||
if !strings.Contains(raw, console.StyleBold.Render("All Cached Rates")) {
|
||||
t.Fatalf("all-modes view is missing styled title:\n%s", raw)
|
||||
}
|
||||
if strings.Contains(output, "Showing representative rates") {
|
||||
t.Fatalf("all-modes view unexpectedly contains representative-rate hint:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksSanitizesMetadata(t *testing.T) {
|
||||
benchmarks := []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "alpha\nbeta\x1b]52;c;payload\a",
|
||||
HostUUID: "host\r\nuuid",
|
||||
OperatorName: "op\tname",
|
||||
CurrentHashcatVersion: "current-v7\x00.2",
|
||||
BenchmarkHashcatVersion: "benchmark-v7\x00.1",
|
||||
Benchmarks: map[int32]uint64{0: 1},
|
||||
}}
|
||||
|
||||
raw := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
for _, expected := range []string{"alphabeta]52;c;payload", "hostuuid", "opname", "current-v7.2", "benchmark-v7.1"} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("sanitized benchmark metadata does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
for _, control := range []string{"\x1b]52;", "\a", "\r", "\x00", "\t"} {
|
||||
if strings.Contains(raw, control) {
|
||||
t.Errorf("rendered benchmark metadata retained control text %q:\n%q", control, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksStylesConnectionAndFreshness(t *testing.T) {
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{
|
||||
{Name: "fresh", Online: true, Fresh: true, Benchmarks: map[int32]uint64{0: 1}},
|
||||
{Name: "stale", Online: false, Fresh: false, Benchmarks: map[int32]uint64{0: 1}},
|
||||
}, settings.SliverDefault, false)
|
||||
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldSuccess.Render("Online"),
|
||||
console.StyleBoldSuccess.Render("Fresh"),
|
||||
console.StyleBoldGray.Render("Offline"),
|
||||
console.StyleBoldWarning.Render("Stale"),
|
||||
} {
|
||||
if !strings.Contains(raw, styled) {
|
||||
t.Errorf("rendered benchmarks are missing status styling %q:\n%s", styled, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksBoundsEveryLine(t *testing.T) {
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: strings.Repeat("very-long-crackstation-name-", 8),
|
||||
HostUUID: "12345678-1234-1234-1234-123456789abc",
|
||||
OperatorName: strings.Repeat("operator-", 16),
|
||||
CurrentHashcatVersion: strings.Repeat("current-version-", 10),
|
||||
BenchmarkHashcatVersion: strings.Repeat("benchmark-version-", 10),
|
||||
BenchmarkedAt: 1_700_000_000,
|
||||
Benchmarks: map[int32]uint64{6211: 1_000_000},
|
||||
}}, settings.SliverDefault, true)
|
||||
|
||||
for lineNumber, line := range strings.Split(raw, "\n") {
|
||||
if width := ansi.StringWidth(line); width > crackBenchmarkOutputWidth {
|
||||
t.Errorf("rendered line %d width = %d, want <= %d:\n%s", lineNumber+1, width, crackBenchmarkOutputWidth, line)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(ansi.Strip(raw), "…") {
|
||||
t.Fatalf("long benchmark heading was not truncated:\n%s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCachedBenchmarkTime(t *testing.T) {
|
||||
if got := formatCachedBenchmarkTime(0); got != "-" {
|
||||
t.Fatalf("zero benchmark time = %q, want -", got)
|
||||
}
|
||||
if got := formatCachedBenchmarkTime(1_700_000_000); got != "2023-11-14T22:13:20Z" {
|
||||
t.Fatalf("benchmark time = %q, want UTC RFC3339", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackBenchmarksHandlesNilAndEmptyData(t *testing.T) {
|
||||
for _, benchmarks := range [][]*clientpb.CrackBenchmarkSnapshot{nil, {nil, nil}} {
|
||||
if got := renderCrackBenchmarks(benchmarks, settings.SliverDefault, false); got != "" {
|
||||
t.Fatalf("renderCrackBenchmarks(%#v) = %q, want empty output", benchmarks, got)
|
||||
}
|
||||
}
|
||||
|
||||
raw := renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{
|
||||
HostUUID: "host-only",
|
||||
}}, settings.SliverDefault, false)
|
||||
output := ansi.Strip(raw)
|
||||
for _, expected := range []string{
|
||||
">>> Cached Crackstation 01 - host-only",
|
||||
"No benchmark rates cached",
|
||||
"Representative Rates",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("empty benchmark snapshot does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
unknown := ansi.Strip(renderCrackBenchmarks([]*clientpb.CrackBenchmarkSnapshot{{}}, settings.SliverDefault, false))
|
||||
if !strings.Contains(unknown, ">>> Cached Crackstation 01 - Unknown crackstation") {
|
||||
t.Fatalf("missing unknown-station identity fallback:\n%s", unknown)
|
||||
}
|
||||
}
|
||||
|
||||
type crackBenchmarksRPCCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
response *clientpb.CrackBenchmarkSnapshots
|
||||
err error
|
||||
benchmarkCalls int
|
||||
crackstationsCalls int
|
||||
hadDeadline bool
|
||||
}
|
||||
|
||||
func (capture *crackBenchmarksRPCCapture) CrackstationBenchmarks(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackBenchmarkSnapshots, error) {
|
||||
capture.benchmarkCalls++
|
||||
_, capture.hadDeadline = ctx.Deadline()
|
||||
return capture.response, capture.err
|
||||
}
|
||||
|
||||
func (capture *crackBenchmarksRPCCapture) Crackstations(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
|
||||
capture.crackstationsCalls++
|
||||
return &clientpb.Crackstations{}, nil
|
||||
}
|
||||
|
||||
func TestCrackBenchmarksCommandUsesDedicatedRPCWithDeadline(t *testing.T) {
|
||||
capture := &crackBenchmarksRPCCapture{response: &clientpb.CrackBenchmarkSnapshots{Snapshots: []*clientpb.CrackBenchmarkSnapshot{{
|
||||
Name: "station",
|
||||
HostUUID: "host-id",
|
||||
Benchmarks: map[int32]uint64{0: 1},
|
||||
}}}}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := Commands(con)[0]
|
||||
root.SetArgs([]string{"benchmarks", "--timeout=1", "-a"})
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute crack benchmarks: %v", err)
|
||||
}
|
||||
if capture.benchmarkCalls != 1 {
|
||||
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
|
||||
}
|
||||
if capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("Crackstations calls = %d, want 0", capture.crackstationsCalls)
|
||||
}
|
||||
if !capture.hadDeadline {
|
||||
t.Fatal("CrackstationBenchmarks context did not inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackBenchmarksCommandHandlesNilResponse(t *testing.T) {
|
||||
capture := &crackBenchmarksRPCCapture{}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
root := Commands(con)[0]
|
||||
root.SetArgs([]string{"benchmarks", "--timeout=1"})
|
||||
root.SilenceErrors = true
|
||||
root.SilenceUsage = true
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute crack benchmarks with nil response: %v", err)
|
||||
}
|
||||
if capture.benchmarkCalls != 1 {
|
||||
t.Fatalf("CrackstationBenchmarks calls = %d, want 1", capture.benchmarkCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func requireOrderedCrackBenchmarkText(t *testing.T, output string, values ...string) {
|
||||
t.Helper()
|
||||
remaining := output
|
||||
for _, value := range values {
|
||||
index := strings.Index(remaining, value)
|
||||
if index < 0 {
|
||||
t.Fatalf("rendered benchmarks do not contain %q in order:\n%s", value, output)
|
||||
}
|
||||
remaining = remaining[index+len(value):]
|
||||
}
|
||||
}
|
||||
|
||||
func requireCrackBenchmarkRow(t *testing.T, output string, mode int32, hashType string, rate string) {
|
||||
t.Helper()
|
||||
if !hasCrackBenchmarkRow(output, mode, hashType, rate) {
|
||||
t.Fatalf("rendered benchmarks are missing mode %d, hash type %q, rate %q:\n%s", mode, hashType, rate, output)
|
||||
}
|
||||
}
|
||||
|
||||
func hasCrackBenchmarkRow(output string, mode int32, hashType string, rate string) bool {
|
||||
pattern := `(?m)^[ \t]*` + regexp.QuoteMeta(strconv.FormatInt(int64(mode), 10)) + `[ \t]+` +
|
||||
regexp.QuoteMeta(hashType) + `[ \t]+` + regexp.QuoteMeta(rate) + `[ \t]*$`
|
||||
return regexp.MustCompile(pattern).MatchString(output)
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const maxKeyboardLayoutMappingBytes int64 = 1 << 20
|
||||
|
||||
func readKeyboardLayoutMapping(path string) ([]byte, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q is not a regular file", path)
|
||||
}
|
||||
if info.Size() > maxKeyboardLayoutMappingBytes {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, maxKeyboardLayoutMappingBytes+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read keyboard layout mapping %q: %w", path, err)
|
||||
}
|
||||
if int64(len(data)) > maxKeyboardLayoutMappingBytes {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q exceeds %d bytes", path, maxKeyboardLayoutMappingBytes)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("keyboard layout mapping %q is empty", path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func shouldRunCrack(cmd *cobra.Command, args []string) bool {
|
||||
if len(args) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
hasFlag := false
|
||||
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
if !flag.Changed || flag.Name == "timeout" {
|
||||
return
|
||||
}
|
||||
// An explicitly disabled standalone mode is a no-op, not a crack job.
|
||||
switch flag.Name {
|
||||
case "backend-info", "keyspace", "total-candidates", "identify-mode", "hash-info":
|
||||
enabled, _ := cmd.Flags().GetBool(flag.Name)
|
||||
if !enabled {
|
||||
return
|
||||
}
|
||||
}
|
||||
hasFlag = true
|
||||
})
|
||||
return hasFlag
|
||||
}
|
||||
|
||||
func buildCrackCommand(cmd *cobra.Command, args []string) (*clientpb.CrackCommand, error) {
|
||||
flags := cmd.Flags()
|
||||
req := &clientpb.CrackCommand{HashType: clientpb.HashType_INVALID}
|
||||
|
||||
if flags.Changed("attack-mode") {
|
||||
raw, _ := flags.GetString("attack-mode")
|
||||
attackMode, err := parseCrackAttackMode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.AttackMode = attackMode
|
||||
}
|
||||
|
||||
if flags.Changed("hash-type") {
|
||||
raw, _ := flags.GetString("hash-type")
|
||||
hashType, err := parseHashType(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.HashType = hashType
|
||||
}
|
||||
if flags.Changed("hash-mode") {
|
||||
hashMode, _ := flags.GetUint32("hash-mode")
|
||||
req.HashMode = &hashMode
|
||||
}
|
||||
|
||||
hashes, _ := flags.GetStringArray("hash")
|
||||
if len(args) > 0 {
|
||||
hashes = append(hashes, args...)
|
||||
}
|
||||
req.Hashes = hashes
|
||||
req.CredentialIDs, _ = flags.GetStringArray("credential")
|
||||
for index, credentialID := range req.CredentialIDs {
|
||||
req.CredentialIDs[index] = strings.TrimSpace(credentialID)
|
||||
if req.CredentialIDs[index] == "" {
|
||||
return nil, fmt.Errorf("--credential cannot be empty")
|
||||
}
|
||||
}
|
||||
req.CredentialCollection, _ = flags.GetString("credential-collection")
|
||||
req.CredentialCollection = strings.TrimSpace(req.CredentialCollection)
|
||||
req.IncludeCrackedCredentials, _ = flags.GetBool("include-cracked-credentials")
|
||||
if req.IncludeCrackedCredentials && len(req.CredentialIDs) == 0 && req.CredentialCollection == "" {
|
||||
return nil, fmt.Errorf("--include-cracked-credentials requires --credential or --credential-collection")
|
||||
}
|
||||
req.PositionalArguments, _ = flags.GetStringArray("input")
|
||||
req.Crackstation, _ = flags.GetString("crackstation")
|
||||
req.Crackstation = strings.TrimSpace(req.Crackstation)
|
||||
if stdin, _ := flags.GetString("stdin"); stdin != "" {
|
||||
req.Stdin = []byte(stdin)
|
||||
}
|
||||
|
||||
if flags.Changed("segment-size") {
|
||||
return nil, fmt.Errorf("--segment-size was removed in Hashcat v7")
|
||||
}
|
||||
|
||||
req.Quiet, _ = flags.GetBool("quiet")
|
||||
req.HexCharset, _ = flags.GetBool("hex-charset")
|
||||
req.HexSalt, _ = flags.GetBool("hex-salt")
|
||||
req.HexWordlist, _ = flags.GetBool("hex-wordlist")
|
||||
req.Force, _ = flags.GetBool("force")
|
||||
req.DeprecatedCheckDisable, _ = flags.GetBool("deprecated-check-disable")
|
||||
req.AdviceDisable, _ = flags.GetBool("advice-disable")
|
||||
req.Status, _ = flags.GetBool("status")
|
||||
req.StatusJSON, _ = flags.GetBool("status-json")
|
||||
req.StatusTimer, _ = flags.GetUint32("status-timer")
|
||||
if flags.Changed("status-timer") {
|
||||
value := req.StatusTimer
|
||||
req.StatusTimerV7 = &value
|
||||
}
|
||||
req.StdinTimeoutAbort, _ = flags.GetUint32("stdin-timeout-abort")
|
||||
if flags.Changed("stdin-timeout-abort") {
|
||||
value := req.StdinTimeoutAbort
|
||||
req.StdinTimeoutAbortV7 = &value
|
||||
}
|
||||
req.MachineReadable, _ = flags.GetBool("machine-readable")
|
||||
req.KeepGuessing, _ = flags.GetBool("keep-guessing")
|
||||
req.SelfTestDisable, _ = flags.GetBool("self-test-disable")
|
||||
req.Loopback, _ = flags.GetBool("loopback")
|
||||
|
||||
if raw, _ := flags.GetString("markov-hcstat2"); raw != "" {
|
||||
req.MarkovHcstat2 = []byte(raw)
|
||||
}
|
||||
req.MarkovDisable, _ = flags.GetBool("markov-disable")
|
||||
req.MarkovClassic, _ = flags.GetBool("markov-classic")
|
||||
req.MarkovInverse, _ = flags.GetBool("markov-inverse")
|
||||
req.MarkovThreshold, _ = flags.GetUint32("markov-threshold")
|
||||
req.Runtime, _ = flags.GetUint32("runtime")
|
||||
req.Session, _ = flags.GetString("session")
|
||||
req.Restore, _ = flags.GetBool("restore")
|
||||
req.RestoreDisable, _ = flags.GetBool("restore-disable")
|
||||
if raw, _ := flags.GetString("restore-file"); raw != "" {
|
||||
req.RestoreFile = []byte(raw)
|
||||
}
|
||||
req.RestorePosition, _ = flags.GetBool("restore-position")
|
||||
req.RestoreShowCommand, _ = flags.GetBool("restore-show-command")
|
||||
if req.RestoreShowCommand && (req.Restore || req.RestorePosition) { //nolint:staticcheck // Restore is required for legacy command compatibility.
|
||||
return nil, fmt.Errorf("--restore-show-command and restore-position modes are mutually exclusive")
|
||||
}
|
||||
|
||||
req.Outfile, _ = flags.GetString("outfile")
|
||||
if formats, _ := flags.GetStringSlice("outfile-format"); len(formats) > 0 {
|
||||
for _, raw := range formats {
|
||||
format, err := parseCrackOutfileFormat(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.OutfileFormat = append(req.OutfileFormat, format)
|
||||
}
|
||||
}
|
||||
req.OutfileJSON, _ = flags.GetBool("outfile-json")
|
||||
req.OutfileAutohexDisable, _ = flags.GetBool("outfile-autohex-disable")
|
||||
req.OutfileCheckTimer, _ = flags.GetUint32("outfile-check-timer")
|
||||
if flags.Changed("outfile-check-timer") {
|
||||
value := req.OutfileCheckTimer
|
||||
req.OutfileCheckTimerV7 = &value
|
||||
}
|
||||
req.WordlistAutohexDisable, _ = flags.GetBool("wordlist-autohex-disable")
|
||||
req.Separator, _ = flags.GetString("separator")
|
||||
req.Show, _ = flags.GetBool("show")
|
||||
req.Left, _ = flags.GetBool("left")
|
||||
req.Username, _ = flags.GetBool("username")
|
||||
req.Remove, _ = flags.GetBool("remove")
|
||||
req.RemoveTimer, _ = flags.GetUint32("remove-timer")
|
||||
req.PotfileDisable, _ = flags.GetBool("potfile-disable")
|
||||
if raw, _ := flags.GetString("potfile"); raw != "" {
|
||||
req.Potfile = []byte(raw)
|
||||
}
|
||||
|
||||
if flags.Changed("encoding-from") {
|
||||
raw, _ := flags.GetString("encoding-from")
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingFrom = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingFromName = raw
|
||||
}
|
||||
}
|
||||
if flags.Changed("encoding-to") {
|
||||
raw, _ := flags.GetString("encoding-to")
|
||||
if enc, err := parseCrackEncoding(raw); err == nil {
|
||||
req.EncodingTo = enc
|
||||
}
|
||||
if _, err := strconv.ParseInt(raw, 10, 32); err != nil {
|
||||
req.EncodingToName = raw
|
||||
}
|
||||
}
|
||||
|
||||
req.DebugMode, _ = flags.GetUint32("debug-mode")
|
||||
req.DebugFile, _ = flags.GetString("debug-file")
|
||||
req.InductionDir, _ = flags.GetString("induction-dir")
|
||||
req.OutfileCheckDir, _ = flags.GetString("outfile-check-dir")
|
||||
req.LogfileDisable, _ = flags.GetBool("logfile-disable")
|
||||
req.HccapxMessagePair, _ = flags.GetUint32("hccapx-message-pair")
|
||||
if flags.Changed("hccapx-message-pair") {
|
||||
value := req.HccapxMessagePair
|
||||
req.HccapxMessagePairV7 = &value
|
||||
}
|
||||
req.NonceErrorCorrections, _ = flags.GetUint32("nonce-error-corrections")
|
||||
if flags.Changed("nonce-error-corrections") {
|
||||
value := req.NonceErrorCorrections
|
||||
req.NonceErrorCorrectionsV7 = &value
|
||||
}
|
||||
if raw, _ := flags.GetString("keyboard-layout-mapping"); raw != "" {
|
||||
mapping, err := readKeyboardLayoutMapping(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.KeyboardLayoutMapping = mapping
|
||||
}
|
||||
req.TruecryptKeyfiles, _ = flags.GetString("truecrypt-keyfiles")
|
||||
req.VeracryptKeyfiles, _ = flags.GetString("veracrypt-keyfiles")
|
||||
if flags.Changed("veracrypt-pim-start") {
|
||||
value, _ := flags.GetUint32("veracrypt-pim-start")
|
||||
req.VeracryptPimStart = &value
|
||||
}
|
||||
if flags.Changed("veracrypt-pim-stop") {
|
||||
value, _ := flags.GetUint32("veracrypt-pim-stop")
|
||||
req.VeracryptPimStop = &value
|
||||
}
|
||||
|
||||
req.Benchmark, _ = flags.GetBool("benchmark")
|
||||
req.BenchmarkAll, _ = flags.GetBool("benchmark-all")
|
||||
req.BenchmarkMin, _ = flags.GetUint32("benchmark-min")
|
||||
if flags.Changed("benchmark-max") {
|
||||
value, _ := flags.GetUint32("benchmark-max")
|
||||
req.BenchmarkMax = &value
|
||||
}
|
||||
req.SpeedOnly, _ = flags.GetBool("speed-only")
|
||||
req.ProgressOnly, _ = flags.GetBool("progress-only")
|
||||
req.PipelineStats, _ = flags.GetBool("pipeline-stats")
|
||||
req.TaskTimeBreakdown, _ = flags.GetBool("task-time-breakdown")
|
||||
req.BitmapMin, _ = flags.GetUint32("bitmap-min")
|
||||
if flags.Changed("bitmap-min") {
|
||||
value := req.BitmapMin
|
||||
req.BitmapMinV7 = &value
|
||||
}
|
||||
req.BitmapMax, _ = flags.GetUint32("bitmap-max")
|
||||
if flags.Changed("bitmap-max") {
|
||||
value := req.BitmapMax
|
||||
req.BitmapMaxV7 = &value
|
||||
}
|
||||
req.CPUAffinity = uintSliceToUint32(flags, "cpu-affinity")
|
||||
req.HookThreads, _ = flags.GetUint32("hook-threads")
|
||||
req.HashInfo, _ = flags.GetBool("hash-info")
|
||||
if req.HashInfo {
|
||||
req.HashInfoLevel = 1
|
||||
}
|
||||
if flags.Changed("hash-info-level") {
|
||||
req.HashInfoLevel, _ = flags.GetUint32("hash-info-level")
|
||||
}
|
||||
req.BackendIgnoreCUDA, _ = flags.GetBool("backend-ignore-cuda")
|
||||
req.BackendIgnoreHip, _ = flags.GetBool("backend-ignore-hip")
|
||||
req.BackendIgnoreMetal, _ = flags.GetBool("backend-ignore-metal")
|
||||
req.BackendIgnoreOpenCL, _ = flags.GetBool("backend-ignore-opencl")
|
||||
req.BackendInfo, _ = flags.GetBool("backend-info")
|
||||
if req.BackendInfo {
|
||||
req.BackendInfoLevel = 1
|
||||
}
|
||||
if flags.Changed("backend-info-level") {
|
||||
req.BackendInfoLevel, _ = flags.GetUint32("backend-info-level")
|
||||
}
|
||||
req.BackendDevices = uintSliceToUint32(flags, "backend-devices")
|
||||
req.BackendDevicesVirtMulti, _ = flags.GetUint32("backend-devices-virtmulti")
|
||||
req.BackendDevicesVirtHost, _ = flags.GetUint32("backend-devices-virthost")
|
||||
req.OpenCLDeviceTypes = uintSliceToUint32(flags, "opencl-device-types")
|
||||
req.MetalCompilerRuntime, _ = flags.GetUint32("metal-compiler-runtime")
|
||||
req.OptimizedKernelEnable, _ = flags.GetBool("optimized-kernel-enable")
|
||||
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disable")
|
||||
if !req.MultiplyAccelDisabled {
|
||||
req.MultiplyAccelDisabled, _ = flags.GetBool("multiply-accel-disabled")
|
||||
}
|
||||
|
||||
if flags.Changed("workload-profile") {
|
||||
raw, _ := flags.GetString("workload-profile")
|
||||
profile, err := parseCrackWorkloadProfile(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.WorkloadProfile = profile
|
||||
}
|
||||
|
||||
req.KernelAccel, _ = flags.GetUint32("kernel-accel")
|
||||
req.KernelLoops, _ = flags.GetUint32("kernel-loops")
|
||||
req.KernelThreads, _ = flags.GetUint32("kernel-threads")
|
||||
req.BackendVectorWidth, _ = flags.GetUint32("backend-vector-width")
|
||||
req.SpinDamp, _ = flags.GetUint32("spin-damp")
|
||||
req.HwmonDisable, _ = flags.GetBool("hwmon-disable")
|
||||
req.HwmonTempAbort, _ = flags.GetUint32("hwmon-temp-abort")
|
||||
if flags.Changed("hwmon-temp-abort") {
|
||||
value := req.HwmonTempAbort
|
||||
req.HwmonTempAbortV7 = &value
|
||||
}
|
||||
req.ScryptTMTO, _ = flags.GetUint32("scrypt-tmto")
|
||||
if flags.Changed("scrypt-tmto") {
|
||||
value := req.ScryptTMTO
|
||||
req.ScryptTMTOV7 = &value
|
||||
}
|
||||
req.Skip, _ = flags.GetUint64("skip")
|
||||
req.Limit, _ = flags.GetUint64("limit")
|
||||
req.Keyspace, _ = flags.GetBool("keyspace")
|
||||
req.TotalCandidates, _ = flags.GetBool("total-candidates")
|
||||
req.Lookup, _ = flags.GetString("lookup")
|
||||
req.DynamicX, _ = flags.GetBool("dynamic-x")
|
||||
req.SeekDBPath, _ = flags.GetString("seekdb-path")
|
||||
|
||||
req.RuleLeft, _ = flags.GetString("rule-left")
|
||||
req.RuleRight, _ = flags.GetString("rule-right")
|
||||
if values, _ := flags.GetStringArray("rules-file"); len(values) > 0 {
|
||||
req.RulesFile = []byte(values[0])
|
||||
for _, value := range values {
|
||||
req.RulesFilesV7 = append(req.RulesFilesV7, []byte(value))
|
||||
}
|
||||
}
|
||||
req.GenerateRules, _ = flags.GetUint32("generate-rules")
|
||||
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-func-min")
|
||||
if !flags.Changed("generate-rules-func-min") {
|
||||
req.GenerateRulesFunMin, _ = flags.GetUint32("generate-rules-fun-min")
|
||||
}
|
||||
if flags.Changed("generate-rules-func-min") || flags.Changed("generate-rules-fun-min") {
|
||||
value := req.GenerateRulesFunMin
|
||||
req.GenerateRulesFuncMinV7 = &value
|
||||
}
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-func-max")
|
||||
if !flags.Changed("generate-rules-func-max") {
|
||||
req.GenerateRulesFunMax, _ = flags.GetUint32("generate-rules-fun-max")
|
||||
}
|
||||
if flags.Changed("generate-rules-func-max") || flags.Changed("generate-rules-fun-max") {
|
||||
value := req.GenerateRulesFunMax
|
||||
req.GenerateRulesFuncMaxV7 = &value
|
||||
}
|
||||
req.GenerateRulesFuncSel, _ = flags.GetString("generate-rules-func-sel")
|
||||
req.GenerateRulesSeed, _ = flags.GetInt32("generate-rules-seed")
|
||||
if flags.Changed("generate-rules-seed") && req.GenerateRulesSeed >= 0 {
|
||||
value := uint32(req.GenerateRulesSeed)
|
||||
req.GenerateRulesSeedV7 = &value
|
||||
}
|
||||
req.CustomCharset1, _ = flags.GetString("custom-charset1")
|
||||
req.CustomCharset2, _ = flags.GetString("custom-charset2")
|
||||
req.CustomCharset3, _ = flags.GetString("custom-charset3")
|
||||
req.CustomCharset4, _ = flags.GetString("custom-charset4")
|
||||
req.CustomCharset5, _ = flags.GetString("custom-charset5")
|
||||
req.CustomCharset6, _ = flags.GetString("custom-charset6")
|
||||
req.CustomCharset7, _ = flags.GetString("custom-charset7")
|
||||
req.CustomCharset8, _ = flags.GetString("custom-charset8")
|
||||
req.Identify, _ = flags.GetString("identify")
|
||||
req.IdentifyMode, _ = flags.GetBool("identify-mode")
|
||||
if req.IdentifyMode && (flags.Changed("hash-mode") || flags.Changed("hash-type")) {
|
||||
return nil, fmt.Errorf("--identify-mode and hash mode/type are mutually exclusive")
|
||||
}
|
||||
req.Increment, _ = flags.GetBool("increment")
|
||||
req.IncrementInverse, _ = flags.GetBool("increment-inverse")
|
||||
req.IncrementMin, _ = flags.GetUint32("increment-min")
|
||||
req.IncrementMax, _ = flags.GetUint32("increment-max")
|
||||
if flags.Changed("bypass-delay") {
|
||||
value, _ := flags.GetUint32("bypass-delay")
|
||||
req.BypassDelay = &value
|
||||
}
|
||||
if flags.Changed("bypass-threshold") {
|
||||
value, _ := flags.GetUint32("bypass-threshold")
|
||||
req.BypassThreshold = &value
|
||||
}
|
||||
req.SlowCandidates, _ = flags.GetBool("slow-candidates")
|
||||
req.BridgeParameter1, _ = flags.GetString("bridge-parameter1")
|
||||
req.BridgeParameter2, _ = flags.GetString("bridge-parameter2")
|
||||
req.BridgeParameter3, _ = flags.GetString("bridge-parameter3")
|
||||
req.BridgeParameter4, _ = flags.GetString("bridge-parameter4")
|
||||
req.BrainServer, _ = flags.GetBool("brain-server")
|
||||
req.BrainServerTimer, _ = flags.GetUint32("brain-server-timer")
|
||||
if flags.Changed("brain-server-timer") {
|
||||
value := req.BrainServerTimer
|
||||
req.BrainServerTimerV7 = &value
|
||||
}
|
||||
req.BrainClient, _ = flags.GetBool("brain-client")
|
||||
req.BrainClientFeatures, _ = flags.GetString("brain-client-features")
|
||||
if value, ok := parseUint32(req.BrainClientFeatures); ok {
|
||||
req.BrainClientFeaturesV7 = value
|
||||
}
|
||||
req.BrainHost, _ = flags.GetString("brain-host")
|
||||
req.BrainPort, _ = flags.GetUint32("brain-port")
|
||||
req.BrainPassword, _ = flags.GetString("brain-password")
|
||||
if flags.Changed("brain-password") {
|
||||
value := req.BrainPassword
|
||||
req.BrainPasswordV7 = &value
|
||||
}
|
||||
req.BrainSession, _ = flags.GetString("brain-session")
|
||||
if value, ok := parseHexUint32(req.BrainSession); ok {
|
||||
req.BrainSessionV7 = &value
|
||||
}
|
||||
req.BrainSessionWhitelist, _ = flags.GetString("brain-session-whitelist")
|
||||
if values, ok := parseHexUint32List(req.BrainSessionWhitelist); ok {
|
||||
req.BrainSessionWhitelistV7 = values
|
||||
}
|
||||
req.BrainFeed, _ = flags.GetBool("brain-feed")
|
||||
req.ColorCracked, _ = flags.GetBool("color-cracked")
|
||||
req.HashCopy, _ = flags.GetBool("hash-copy")
|
||||
req.EncryptWithPubkey, _ = flags.GetString("encrypt-with-pubkey")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func uintSliceToUint32(flags *pflag.FlagSet, name string) []uint32 {
|
||||
values, _ := flags.GetUintSlice(name)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]uint32, len(values))
|
||||
for i, value := range values {
|
||||
out[i] = uint32(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseUint32(raw string) (uint32, bool) {
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 10, 32)
|
||||
return uint32(value), err == nil
|
||||
}
|
||||
|
||||
func parseHexUint32(raw string) (uint32, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if strings.HasPrefix(raw, "0x") || strings.HasPrefix(raw, "0X") {
|
||||
raw = raw[2:]
|
||||
}
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 16, 32)
|
||||
return uint32(value), err == nil
|
||||
}
|
||||
|
||||
func parseHexUint32List(raw string) ([]uint32, bool) {
|
||||
if raw == "" {
|
||||
return nil, false
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
values := make([]uint32, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
value, ok := parseHexUint32(part)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, true
|
||||
}
|
||||
|
||||
func parseCrackAttackMode(raw string) (clientpb.CrackAttackMode, error) {
|
||||
if raw == "" {
|
||||
return clientpb.CrackAttackMode_STRAIGHT, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
||||
return clientpb.CrackAttackMode(numeric), nil
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
normalized = strings.NewReplacer("-", "_", " ", "_").Replace(normalized)
|
||||
switch normalized {
|
||||
case "straight":
|
||||
return clientpb.CrackAttackMode_STRAIGHT, nil
|
||||
case "combination":
|
||||
return clientpb.CrackAttackMode_COMBINATION, nil
|
||||
case "bruteforce", "brute_force", "brute-force":
|
||||
return clientpb.CrackAttackMode_BRUTEFORCE, nil
|
||||
case "pcfg":
|
||||
return clientpb.CrackAttackMode_PCFG, nil
|
||||
case "hybrid_wordlist_mask":
|
||||
return clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK, nil
|
||||
case "hybrid_mask_wordlist":
|
||||
return clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST, nil
|
||||
case "generic":
|
||||
return clientpb.CrackAttackMode_GENERIC, nil
|
||||
case "association":
|
||||
return clientpb.CrackAttackMode_ASSOCIATION, nil
|
||||
case "no_attack", "none":
|
||||
return clientpb.CrackAttackMode_NO_ATTACK, nil
|
||||
case "hybrid":
|
||||
return clientpb.CrackAttackMode_HYBRID, nil
|
||||
default:
|
||||
return clientpb.CrackAttackMode_STRAIGHT, fmt.Errorf("unknown attack mode: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func parseHashType(raw string) (clientpb.HashType, error) {
|
||||
if raw == "" {
|
||||
return clientpb.HashType_MD5, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
||||
return clientpb.HashType(numeric), nil
|
||||
}
|
||||
key := normalizeEnumKey(raw)
|
||||
if value, ok := clientpb.HashType_value[key]; ok {
|
||||
return clientpb.HashType(value), nil
|
||||
}
|
||||
return clientpb.HashType_MD5, fmt.Errorf("unknown hash type: %s", raw)
|
||||
}
|
||||
|
||||
func parseCrackEncoding(raw string) (clientpb.CrackEncoding, error) {
|
||||
if raw == "" {
|
||||
return clientpb.CrackEncoding_INVALID_ENCODING, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
||||
return clientpb.CrackEncoding(numeric), nil
|
||||
}
|
||||
key := normalizeEnumKey(raw)
|
||||
if value, ok := clientpb.CrackEncoding_value[key]; ok {
|
||||
return clientpb.CrackEncoding(value), nil
|
||||
}
|
||||
return clientpb.CrackEncoding_INVALID_ENCODING, fmt.Errorf("unknown encoding: %s", raw)
|
||||
}
|
||||
|
||||
func parseCrackOutfileFormat(raw string) (clientpb.CrackOutfileFormat, error) {
|
||||
if raw == "" {
|
||||
return clientpb.CrackOutfileFormat_INVALID_FORMAT, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
||||
return clientpb.CrackOutfileFormat(numeric), nil
|
||||
}
|
||||
key := normalizeEnumKey(raw)
|
||||
if value, ok := clientpb.CrackOutfileFormat_value[key]; ok {
|
||||
return clientpb.CrackOutfileFormat(value), nil
|
||||
}
|
||||
return clientpb.CrackOutfileFormat_INVALID_FORMAT, fmt.Errorf("unknown outfile format: %s", raw)
|
||||
}
|
||||
|
||||
func parseCrackWorkloadProfile(raw string) (clientpb.CrackWorkloadProfile, error) {
|
||||
if raw == "" {
|
||||
return clientpb.CrackWorkloadProfile_INVALID_WORKLOAD_PROFILE, nil
|
||||
}
|
||||
if numeric, err := strconv.ParseInt(raw, 10, 32); err == nil {
|
||||
return clientpb.CrackWorkloadProfile(numeric), nil
|
||||
}
|
||||
key := normalizeEnumKey(raw)
|
||||
if value, ok := clientpb.CrackWorkloadProfile_value[key]; ok {
|
||||
return clientpb.CrackWorkloadProfile(value), nil
|
||||
}
|
||||
return clientpb.CrackWorkloadProfile_INVALID_WORKLOAD_PROFILE, fmt.Errorf("unknown workload profile: %s", raw)
|
||||
}
|
||||
|
||||
func normalizeEnumKey(value string) string {
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.Grow(len(value))
|
||||
underscore := false
|
||||
for i := 0; i < len(value); i++ {
|
||||
ch := value[i]
|
||||
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') {
|
||||
if ch >= 'a' && ch <= 'z' {
|
||||
ch -= 'a' - 'A'
|
||||
}
|
||||
builder.WriteByte(ch)
|
||||
underscore = false
|
||||
continue
|
||||
}
|
||||
if !underscore {
|
||||
builder.WriteByte('_')
|
||||
underscore = true
|
||||
}
|
||||
}
|
||||
return strings.Trim(builder.String(), "_")
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestBuildCrackCommandReadsKeyboardLayoutMapping(t *testing.T) {
|
||||
mapping := []byte("a A\nb B\n")
|
||||
path := filepath.Join(t.TempDir(), "keyboard.hckmap")
|
||||
if err := os.WriteFile(path, mapping, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + path}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand() error = %v", err)
|
||||
}
|
||||
if !bytes.Equal(got.KeyboardLayoutMapping, mapping) {
|
||||
t.Fatalf("KeyboardLayoutMapping = %q, want file contents %q", got.KeyboardLayoutMapping, mapping)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsInvalidKeyboardLayoutMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path func(*testing.T) string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "missing",
|
||||
path: func(t *testing.T) string { return filepath.Join(t.TempDir(), "missing.hckmap") },
|
||||
want: "open keyboard layout mapping",
|
||||
},
|
||||
{
|
||||
name: "directory",
|
||||
path: func(t *testing.T) string { return t.TempDir() },
|
||||
want: "is not a regular file",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
path: func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "empty.hckmap")
|
||||
if err := os.WriteFile(path, nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
},
|
||||
want: "is empty",
|
||||
},
|
||||
{
|
||||
name: "too large",
|
||||
path: func(t *testing.T) string {
|
||||
path := filepath.Join(t.TempDir(), "large.hckmap")
|
||||
if err := os.WriteFile(path, make([]byte, maxKeyboardLayoutMappingBytes+1), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
},
|
||||
want: "exceeds",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--keyboard-layout-mapping=" + test.path(t)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want substring %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrackAttackMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want clientpb.CrackAttackMode
|
||||
}{
|
||||
{name: "straight", want: clientpb.CrackAttackMode_STRAIGHT},
|
||||
{name: "combination", want: clientpb.CrackAttackMode_COMBINATION},
|
||||
{name: "bruteforce", want: clientpb.CrackAttackMode_BRUTEFORCE},
|
||||
{name: "pcfg", want: clientpb.CrackAttackMode_PCFG},
|
||||
{name: "hybrid-wordlist-mask", want: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK},
|
||||
{name: "hybrid-mask-wordlist", want: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST},
|
||||
{name: "generic", want: clientpb.CrackAttackMode_GENERIC},
|
||||
{name: "association", want: clientpb.CrackAttackMode_ASSOCIATION},
|
||||
{name: "no-attack", want: clientpb.CrackAttackMode_NO_ATTACK},
|
||||
{name: "hybrid", want: clientpb.CrackAttackMode_HYBRID},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := parseCrackAttackMode(test.name)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCrackAttackMode(%q) returned error: %v", test.name, err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("parseCrackAttackMode(%q) = %v, want %v", test.name, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandHashcatShortFlags(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"-a", "3", "-m", "1000"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, []string{"hash"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand() error = %v", err)
|
||||
}
|
||||
if got.AttackMode != clientpb.CrackAttackMode_BRUTEFORCE {
|
||||
t.Fatalf("AttackMode = %s, want BRUTEFORCE", got.AttackMode)
|
||||
}
|
||||
if got.HashType != clientpb.HashType(1000) {
|
||||
t.Fatalf("HashType = %d, want 1000", got.HashType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandHashcatV7Options(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
args := []string{
|
||||
"--attack-mode=hybrid",
|
||||
"--hash-mode=0",
|
||||
"--input=first-input",
|
||||
"--input=second-input",
|
||||
"--stdin=stdin-data",
|
||||
"--advice-disable",
|
||||
"--status-timer=0",
|
||||
"--stdin-timeout-abort=0",
|
||||
"--restore-position",
|
||||
"--outfile=outfile.txt",
|
||||
"--outfile-json",
|
||||
"--debug-file=debug.txt",
|
||||
"--induction-dir=induction",
|
||||
"--outfile-check-dir=outfile-check",
|
||||
"--outfile-check-timer=0",
|
||||
"--truecrypt-keyfiles=truecrypt.keys",
|
||||
"--veracrypt-keyfiles=veracrypt.keys",
|
||||
"--veracrypt-pim-start=11",
|
||||
"--veracrypt-pim-stop=12",
|
||||
"--encoding-from=windows-1252",
|
||||
"--encoding-to=utf-8",
|
||||
"--hccapx-message-pair=0",
|
||||
"--nonce-error-corrections=0",
|
||||
"--benchmark-min=13",
|
||||
"--benchmark-max=14",
|
||||
"--pipeline-stats",
|
||||
"--task-time-breakdown",
|
||||
"--bitmap-min=0",
|
||||
"--bitmap-max=0",
|
||||
"--hash-info",
|
||||
"--hash-info-level=2",
|
||||
"--backend-info",
|
||||
"--backend-info-level=2",
|
||||
"--backend-devices-virtmulti=15",
|
||||
"--backend-devices-virthost=16",
|
||||
"--metal-compiler-runtime=17",
|
||||
"--multiply-accel-disable",
|
||||
"--hwmon-temp-abort=0",
|
||||
"--total-candidates",
|
||||
"--lookup=lookup-value",
|
||||
"--crackstation= station-one ",
|
||||
"--dynamic-x",
|
||||
"--seekdb-path=seekdb",
|
||||
"--rule-left=l",
|
||||
"--rule-right=r",
|
||||
"--rules-file=:",
|
||||
"--rules-file=$1",
|
||||
"--custom-charset5=cs5",
|
||||
"--custom-charset6=cs6",
|
||||
"--custom-charset7=cs7",
|
||||
"--custom-charset8=cs8",
|
||||
"--identify=legacy-input",
|
||||
"--increment-inverse",
|
||||
"--bypass-delay=18",
|
||||
"--bypass-threshold=19",
|
||||
"--bridge-parameter1=bridge1",
|
||||
"--bridge-parameter2=bridge2",
|
||||
"--bridge-parameter3=bridge3",
|
||||
"--bridge-parameter4=bridge4",
|
||||
"--scrypt-tmto=0",
|
||||
"--generate-rules-func-min=4",
|
||||
"--generate-rules-func-max=8",
|
||||
"--generate-rules-seed=0",
|
||||
"--brain-client-features=3",
|
||||
"--brain-password=",
|
||||
"--brain-server-timer=0",
|
||||
"--brain-session=deadbeef",
|
||||
"--brain-session-whitelist=1,2,0x3",
|
||||
"--brain-feed",
|
||||
"--color-cracked",
|
||||
"--hash-copy",
|
||||
"--encrypt-with-pubkey=key.pub",
|
||||
}
|
||||
if err := cmd.Flags().Parse(args); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
zero := uint32(0)
|
||||
want := &clientpb.CrackCommand{
|
||||
AttackMode: clientpb.CrackAttackMode_HYBRID,
|
||||
HashType: clientpb.HashType_INVALID,
|
||||
HashMode: &zero,
|
||||
Stdin: []byte("stdin-data"),
|
||||
AdviceDisable: true,
|
||||
StatusTimerV7: &zero,
|
||||
StdinTimeoutAbortV7: &zero,
|
||||
Outfile: "outfile.txt",
|
||||
DebugFile: "debug.txt",
|
||||
InductionDir: "induction",
|
||||
OutfileCheckDir: "outfile-check",
|
||||
OutfileCheckTimerV7: &zero,
|
||||
TruecryptKeyfiles: "truecrypt.keys",
|
||||
VeracryptKeyfiles: "veracrypt.keys",
|
||||
VeracryptPimStart: uint32Pointer(11),
|
||||
VeracryptPimStop: uint32Pointer(12),
|
||||
EncodingFromName: "windows-1252",
|
||||
EncodingToName: "utf-8",
|
||||
HccapxMessagePairV7: &zero,
|
||||
NonceErrorCorrectionsV7: &zero,
|
||||
RuleLeft: "l",
|
||||
RuleRight: "r",
|
||||
RulesFile: []byte(":"),
|
||||
RulesFilesV7: [][]byte{[]byte(":"), []byte("$1")},
|
||||
PipelineStats: true,
|
||||
TaskTimeBreakdown: true,
|
||||
BitmapMinV7: &zero,
|
||||
BitmapMaxV7: &zero,
|
||||
HashInfo: true,
|
||||
HashInfoLevel: 2,
|
||||
BackendInfo: true,
|
||||
BackendInfoLevel: 2,
|
||||
MetalCompilerRuntime: 17,
|
||||
RestorePosition: true,
|
||||
OutfileJSON: true,
|
||||
DynamicX: true,
|
||||
SeekDBPath: "seekdb",
|
||||
BenchmarkMin: 13,
|
||||
BenchmarkMax: uint32Pointer(14),
|
||||
BridgeParameter1: "bridge1",
|
||||
BridgeParameter2: "bridge2",
|
||||
BridgeParameter3: "bridge3",
|
||||
BridgeParameter4: "bridge4",
|
||||
BackendDevicesVirtMulti: 15,
|
||||
BackendDevicesVirtHost: 16,
|
||||
MultiplyAccelDisabled: true,
|
||||
HwmonTempAbortV7: &zero,
|
||||
TotalCandidates: true,
|
||||
Lookup: "lookup-value",
|
||||
Crackstation: "station-one",
|
||||
CustomCharset5: "cs5",
|
||||
CustomCharset6: "cs6",
|
||||
CustomCharset7: "cs7",
|
||||
CustomCharset8: "cs8",
|
||||
IncrementInverse: true,
|
||||
BypassDelay: uint32Pointer(18),
|
||||
BypassThreshold: uint32Pointer(19),
|
||||
ScryptTMTOV7: &zero,
|
||||
GenerateRulesFunMin: 4,
|
||||
GenerateRulesFunMax: 8,
|
||||
GenerateRulesFuncMinV7: uint32Pointer(4),
|
||||
GenerateRulesFuncMaxV7: uint32Pointer(8),
|
||||
GenerateRulesSeedV7: &zero,
|
||||
BrainClientFeatures: "3",
|
||||
BrainClientFeaturesV7: 3,
|
||||
BrainPasswordV7: stringPointer(""),
|
||||
BrainSession: "deadbeef",
|
||||
BrainSessionV7: uint32Pointer(0xdeadbeef),
|
||||
BrainServerTimerV7: &zero,
|
||||
BrainSessionWhitelist: "1,2,0x3",
|
||||
BrainSessionWhitelistV7: []uint32{1, 2, 3},
|
||||
BrainFeed: true,
|
||||
ColorCracked: true,
|
||||
HashCopy: true,
|
||||
EncryptWithPubkey: "key.pub",
|
||||
Identify: "legacy-input", //nolint:staticcheck // Verify the legacy positional operand remains populated for wire compatibility.
|
||||
PositionalArguments: []string{"first-input", "second-input"},
|
||||
}
|
||||
if !proto.Equal(got, want) {
|
||||
t.Fatal("buildCrackCommand() did not map the Hashcat v7 options as expected")
|
||||
}
|
||||
}
|
||||
|
||||
func uint32Pointer(value uint32) *uint32 {
|
||||
return &value
|
||||
}
|
||||
|
||||
func stringPointer(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandBrainSessionsAreHexadecimal(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--brain-session=10",
|
||||
"--brain-session-whitelist=10,20",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if got.BrainSessionV7 == nil || *got.BrainSessionV7 != 0x10 {
|
||||
t.Fatalf("BrainSessionV7 = %v, want 0x10", got.BrainSessionV7)
|
||||
}
|
||||
wantWhitelist := []uint32{0x10, 0x20}
|
||||
if !slices.Equal(got.BrainSessionWhitelistV7, wantWhitelist) {
|
||||
t.Fatalf("BrainSessionWhitelistV7 = %#v, want %#v", got.BrainSessionWhitelistV7, wantWhitelist)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandPreservesCommasInPositionalInputs(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--input=hashes,2026.txt",
|
||||
"--input=?d?d,?l?l",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
want := []string{"hashes,2026.txt", "?d?d,?l?l"}
|
||||
if !slices.Equal(got.PositionalArguments, want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", got.PositionalArguments, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandPreservesCommasInHashes(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--hash=digest:salt,with,commas",
|
||||
"--hash=second:digest,salt",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
want := []string{"digest:salt,with,commas", "second:digest,salt"}
|
||||
if !slices.Equal(got.Hashes, want) {
|
||||
t.Fatalf("Hashes = %#v, want %#v", got.Hashes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsConflictingRestoreModes(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--restore-position", "--restore-show-command"}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
_, err := buildCrackCommand(cmd, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want restore conflict error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandIdentifyHasNoImplicitHashType(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--identify-mode", "--input=hashes.txt"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.HashType != clientpb.HashType_INVALID || got.HashMode != nil {
|
||||
t.Fatalf("identify command inherited hash mode/type: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandLegacyFlagAliases(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--multiply-accel-disabled",
|
||||
"--generate-rules-fun-min=2",
|
||||
"--generate-rules-fun-max=6",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if !got.MultiplyAccelDisabled || got.GenerateRulesFunMin != 2 || got.GenerateRulesFunMax != 6 {
|
||||
t.Fatalf("legacy aliases were not preserved: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsSegmentSize(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Set("segment-size", "32"); err != nil {
|
||||
t.Fatalf("set segment-size: %v", err)
|
||||
}
|
||||
|
||||
_, err := buildCrackCommand(cmd, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "removed in Hashcat v7") {
|
||||
t.Fatalf("buildCrackCommand() error = %v, want Hashcat v7 removal error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandCredentialSelectors(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{
|
||||
"--credential= first-id ",
|
||||
"--credential=second-id",
|
||||
"--credential-collection= domain-admins ",
|
||||
"--include-cracked-credentials",
|
||||
}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
|
||||
got, err := buildCrackCommand(cmd, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("buildCrackCommand returned error: %v", err)
|
||||
}
|
||||
if !slices.Equal(got.CredentialIDs, []string{"first-id", "second-id"}) {
|
||||
t.Fatalf("CredentialIDs = %#v", got.CredentialIDs)
|
||||
}
|
||||
if got.CredentialCollection != "domain-admins" {
|
||||
t.Fatalf("CredentialCollection = %q", got.CredentialCollection)
|
||||
}
|
||||
if !got.IncludeCrackedCredentials {
|
||||
t.Fatal("IncludeCrackedCredentials is false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandRejectsEmptyCredential(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--credential= "}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "cannot be empty") {
|
||||
t.Fatalf("buildCrackCommand error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCrackCommandIncludeCrackedRequiresSelector(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse([]string{"--include-cracked-credentials"}); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
if _, err := buildCrackCommand(cmd, nil); err == nil || !strings.Contains(err.Error(), "requires --credential") {
|
||||
t.Fatalf("buildCrackCommand error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
const (
|
||||
maxCrackBackendInfoLevel uint32 = 2
|
||||
maxCrackHashInfoLevel uint32 = 2
|
||||
)
|
||||
|
||||
type crackInvocationMode uint8
|
||||
|
||||
const (
|
||||
crackInvocationDashboard crackInvocationMode = iota
|
||||
crackInvocationJob
|
||||
crackInvocationBackendInfo
|
||||
crackInvocationKeyspace
|
||||
crackInvocationTotalCandidates
|
||||
crackInvocationLookup
|
||||
crackInvocationIdentify
|
||||
crackInvocationHashInfo
|
||||
)
|
||||
|
||||
type crackInvocation struct {
|
||||
mode crackInvocationMode
|
||||
backendInfoLevel uint32
|
||||
hashInfoLevel uint32
|
||||
crackstation string
|
||||
}
|
||||
|
||||
type crackQueryModeSelection struct {
|
||||
mode crackInvocationMode
|
||||
flagName string
|
||||
}
|
||||
|
||||
var incompatibleCandidateQueryFlags = map[string]struct{}{
|
||||
"benchmark": {},
|
||||
"benchmark-all": {},
|
||||
"benchmark-min": {},
|
||||
"benchmark-max": {},
|
||||
"brain-client": {},
|
||||
"brain-client-features": {},
|
||||
"brain-feed": {},
|
||||
"brain-host": {},
|
||||
"brain-password": {},
|
||||
"brain-port": {},
|
||||
"brain-server": {},
|
||||
"brain-server-timer": {},
|
||||
"brain-session": {},
|
||||
"brain-session-whitelist": {},
|
||||
"left": {},
|
||||
"progress-only": {},
|
||||
"restore": {},
|
||||
"restore-file": {},
|
||||
"restore-position": {},
|
||||
"restore-show-command": {},
|
||||
"show": {},
|
||||
"speed-only": {},
|
||||
}
|
||||
|
||||
// classifyCrackInvocation separates terminal command modes from work that
|
||||
// belongs in the distributed crack queue. Backend information comes from the
|
||||
// registration snapshots; the remaining query modes execute synchronously on
|
||||
// one idle crackstation and never create a CrackJob.
|
||||
//
|
||||
//nolint:gocyclo // The ordered flag matrix keeps mutually exclusive query modes and their validation in one decision point.
|
||||
func classifyCrackInvocation(cmd *cobra.Command, args []string) (crackInvocation, error) {
|
||||
flags := cmd.Flags()
|
||||
backendInfo, _ := flags.GetBool("backend-info")
|
||||
backendInfoLevel, _ := flags.GetUint32("backend-info-level")
|
||||
backendInfoLevelChanged := flags.Changed("backend-info-level")
|
||||
hashInfo, _ := flags.GetBool("hash-info")
|
||||
hashInfoLevel, _ := flags.GetUint32("hash-info-level")
|
||||
hashInfoLevelChanged := flags.Changed("hash-info-level")
|
||||
keyspace, _ := flags.GetBool("keyspace")
|
||||
totalCandidates, _ := flags.GetBool("total-candidates")
|
||||
lookup, _ := flags.GetString("lookup")
|
||||
lookupChanged := flags.Changed("lookup")
|
||||
identify, _ := flags.GetBool("identify-mode")
|
||||
crackstation, _ := flags.GetString("crackstation")
|
||||
crackstation = strings.TrimSpace(crackstation)
|
||||
|
||||
if flags.Changed("crackstation") && crackstation == "" {
|
||||
return crackInvocation{}, fmt.Errorf("--crackstation cannot be empty")
|
||||
}
|
||||
if lookupChanged && lookup == "" {
|
||||
return crackInvocation{}, fmt.Errorf("--lookup cannot be empty")
|
||||
}
|
||||
|
||||
selections := make([]crackQueryModeSelection, 0, 6)
|
||||
if backendInfo || backendInfoLevelChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationBackendInfo, flagName: "--backend-info"})
|
||||
}
|
||||
if keyspace {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationKeyspace, flagName: "--keyspace"})
|
||||
}
|
||||
if totalCandidates {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationTotalCandidates, flagName: "--total-candidates"})
|
||||
}
|
||||
if lookupChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationLookup, flagName: "--lookup"})
|
||||
}
|
||||
if identify {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationIdentify, flagName: "--identify-mode"})
|
||||
}
|
||||
if hashInfo || hashInfoLevelChanged {
|
||||
selections = append(selections, crackQueryModeSelection{mode: crackInvocationHashInfo, flagName: "--hash-info"})
|
||||
}
|
||||
if len(selections) > 1 {
|
||||
names := make([]string, 0, len(selections))
|
||||
for _, selection := range selections {
|
||||
names = append(names, selection.flagName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return crackInvocation{}, fmt.Errorf("crack query modes cannot be combined: %s", strings.Join(names, ", "))
|
||||
}
|
||||
if len(selections) == 0 {
|
||||
if flags.Changed("crackstation") {
|
||||
return crackInvocation{}, fmt.Errorf("--crackstation requires --keyspace, --total-candidates, --lookup, --identify-mode, or --hash-info")
|
||||
}
|
||||
if shouldRunCrack(cmd, args) {
|
||||
return crackInvocation{mode: crackInvocationJob}, nil
|
||||
}
|
||||
return crackInvocation{mode: crackInvocationDashboard}, nil
|
||||
}
|
||||
|
||||
invocation := crackInvocation{mode: selections[0].mode, crackstation: crackstation}
|
||||
switch invocation.mode {
|
||||
case crackInvocationBackendInfo:
|
||||
if !backendInfoLevelChanged {
|
||||
backendInfoLevel = 1
|
||||
}
|
||||
if backendInfoLevel == 0 || backendInfoLevel > maxCrackBackendInfoLevel {
|
||||
return crackInvocation{}, fmt.Errorf("--backend-info-level must be 1 or %d", maxCrackBackendInfoLevel)
|
||||
}
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"backend-info": {}, "backend-info-level": {}, "timeout": {},
|
||||
})
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
if len(conflicts) != 0 {
|
||||
sort.Strings(conflicts)
|
||||
return crackInvocation{}, fmt.Errorf("backend-info query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
invocation.backendInfoLevel = backendInfoLevel
|
||||
case crackInvocationHashInfo:
|
||||
if !hashInfoLevelChanged {
|
||||
hashInfoLevel = 1
|
||||
}
|
||||
if hashInfoLevel == 0 || hashInfoLevel > maxCrackHashInfoLevel {
|
||||
return crackInvocation{}, fmt.Errorf("--hash-info-level must be 1 or %d", maxCrackHashInfoLevel)
|
||||
}
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"crackstation": {}, "hash-info": {}, "hash-info-level": {}, "hash-mode": {}, "hash-type": {}, "timeout": {},
|
||||
})
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
if len(conflicts) != 0 {
|
||||
sort.Strings(conflicts)
|
||||
return crackInvocation{}, fmt.Errorf("hash-info query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
invocation.hashInfoLevel = hashInfoLevel
|
||||
case crackInvocationIdentify:
|
||||
conflicts := changedCrackFlagConflicts(flags, map[string]struct{}{
|
||||
"crackstation": {}, "hash": {}, "identify-mode": {}, "timeout": {},
|
||||
})
|
||||
if len(conflicts) != 0 {
|
||||
return crackInvocation{}, fmt.Errorf("identify query cannot be combined with %s", strings.Join(conflicts, ", "))
|
||||
}
|
||||
if !hasCrackIdentifyInput(flags, args) {
|
||||
return crackInvocation{}, fmt.Errorf("--identify-mode requires a positional hash or --hash")
|
||||
}
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup:
|
||||
if conflicts := candidateQueryConflicts(flags, args, invocation.mode); len(conflicts) != 0 {
|
||||
return crackInvocation{}, fmt.Errorf("%s query cannot be combined with %s", crackInvocationName(invocation.mode), strings.Join(conflicts, ", "))
|
||||
}
|
||||
}
|
||||
return invocation, nil
|
||||
}
|
||||
|
||||
func crackInvocationName(mode crackInvocationMode) string {
|
||||
switch mode {
|
||||
case crackInvocationBackendInfo:
|
||||
return "backend-info"
|
||||
case crackInvocationKeyspace:
|
||||
return "keyspace"
|
||||
case crackInvocationTotalCandidates:
|
||||
return "total-candidates"
|
||||
case crackInvocationLookup:
|
||||
return "lookup"
|
||||
case crackInvocationIdentify:
|
||||
return "identify"
|
||||
case crackInvocationHashInfo:
|
||||
return "hash-info"
|
||||
default:
|
||||
return "crack"
|
||||
}
|
||||
}
|
||||
|
||||
func changedCrackFlagConflicts(flags *pflag.FlagSet, allowed map[string]struct{}) []string {
|
||||
conflicts := make([]string, 0)
|
||||
flags.Visit(func(flag *pflag.Flag) {
|
||||
if _, ok := allowed[flag.Name]; ok || changedBoolFlagIsFalse(flags, flag) {
|
||||
return
|
||||
}
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
})
|
||||
sort.Strings(conflicts)
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func changedBoolFlagIsFalse(flags *pflag.FlagSet, flag *pflag.Flag) bool {
|
||||
if flag.Value.Type() != "bool" {
|
||||
return false
|
||||
}
|
||||
value, err := flags.GetBool(flag.Name)
|
||||
return err == nil && !value
|
||||
}
|
||||
|
||||
func hasCrackIdentifyInput(flags *pflag.FlagSet, args []string) bool {
|
||||
if len(args) != 0 {
|
||||
return true
|
||||
}
|
||||
hashes, _ := flags.GetStringArray("hash")
|
||||
return len(hashes) != 0
|
||||
}
|
||||
|
||||
func candidateQueryConflicts(flags *pflag.FlagSet, args []string, mode crackInvocationMode) []string {
|
||||
conflicts := make([]string, 0)
|
||||
if len(args) != 0 {
|
||||
conflicts = append(conflicts, "positional hash arguments")
|
||||
}
|
||||
flags.Visit(func(flag *pflag.Flag) {
|
||||
if changedBoolFlagIsFalse(flags, flag) {
|
||||
return
|
||||
}
|
||||
switch flag.Name {
|
||||
case "hash", "credential", "credential-collection":
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
return
|
||||
case "include-cracked-credentials":
|
||||
includeCracked, _ := flags.GetBool(flag.Name)
|
||||
if includeCracked {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
return
|
||||
case "skip", "limit":
|
||||
if mode != crackInvocationLookup {
|
||||
value, _ := flags.GetUint64(flag.Name)
|
||||
if value != 0 {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, incompatible := incompatibleCandidateQueryFlags[flag.Name]; incompatible {
|
||||
conflicts = append(conflicts, "--"+flag.Name)
|
||||
}
|
||||
})
|
||||
sort.Strings(conflicts)
|
||||
return conflicts
|
||||
}
|
||||
Loaded 100 of 11756 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user