mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
628
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4afcd5bc9d | ||
|
|
c746a5262c | ||
|
|
e91cdf117f | ||
|
|
1fb671f71b | ||
|
|
c43347886b | ||
|
|
4ef33f389c | ||
|
|
acd52520e4 | ||
|
|
9a373e20f0 | ||
|
|
aad186a632 | ||
|
|
ec9d9b2cc1 | ||
|
|
05d3b2604b | ||
|
|
c3e8a97f66 | ||
|
|
547a56696d | ||
|
|
84bb2397c7 | ||
|
|
503d040885 | ||
|
|
801452eeff | ||
|
|
f6c7d73014 | ||
|
|
228757f656 | ||
|
|
738010e990 | ||
|
|
9dcf9b2d25 | ||
|
|
32ab020dbd | ||
|
|
6fcaf970b1 | ||
|
|
47c83bbaa0 | ||
|
|
7d76febbc9 | ||
|
|
f4c182427f | ||
|
|
836e33ade7 | ||
|
|
0aa7e5bf96 | ||
|
|
b46df4b05e | ||
|
|
cbc9200649 | ||
|
|
a0bf009c06 | ||
|
|
46f516fa3a | ||
|
|
1f82c38e11 | ||
|
|
6ac84ffc11 | ||
|
|
2638b5f384 | ||
|
|
cf19741b9d | ||
|
|
b2c0979b63 | ||
|
|
ea5e1412b2 | ||
|
|
bd948b8ab1 | ||
|
|
937a74d597 | ||
|
|
e747187dda | ||
|
|
ec992dea76 | ||
|
|
544c872881 | ||
|
|
4701b895b7 | ||
|
|
18c56ec3c5 | ||
|
|
4b12c80534 | ||
|
|
a8c6d5e0a3 | ||
|
|
67c6d0d356 | ||
|
|
5e3b432b9b | ||
|
|
fc4d9a1c36 | ||
|
|
55b97df9f7 | ||
|
|
227328fa87 | ||
|
|
071106d0bf | ||
|
|
8c326e6af9 | ||
|
|
5e9ba47bf4 | ||
|
|
0537e285fc | ||
|
|
ca685f5eed | ||
|
|
902999493b | ||
|
|
14363c5d2a | ||
|
|
926b7ae1a7 | ||
|
|
bf815b36ed | ||
|
|
637853e8c2 | ||
|
|
0c022ec722 | ||
|
|
7c83b458fa | ||
|
|
f447a170c6 | ||
|
|
d565592773 | ||
|
|
c21da4dceb | ||
|
|
b08e2ff5c0 | ||
|
|
09b0540e75 | ||
|
|
b4005efda8 | ||
|
|
6d086fd7a7 | ||
|
|
ccebd505b0 | ||
|
|
94c5022dd0 | ||
|
|
b19e383111 | ||
|
|
02ebd99c68 | ||
|
|
83d82e7161 | ||
|
|
c481a4f5db | ||
|
|
eb5ac1b309 | ||
|
|
e73618e0a5 | ||
|
|
0017fd2fe8 | ||
|
|
e44111b6b4 | ||
|
|
aa2dce510a | ||
|
|
5e6acde3c7 | ||
|
|
99a8d3f859 | ||
|
|
fc15a47b1e | ||
|
|
8b32b6525f | ||
|
|
9e0fafb785 | ||
|
|
12ebffb16c | ||
|
|
b539762de7 | ||
|
|
e71af809d4 | ||
|
|
f3ffa3323b | ||
|
|
778331d474 | ||
|
|
8d259577af | ||
|
|
aa108a66a8 | ||
|
|
44e5464950 | ||
|
|
1381200b33 | ||
|
|
f81b250a04 | ||
|
|
8b4ee5f5c5 | ||
|
|
68cb1363e8 | ||
|
|
2344a3ae50 | ||
|
|
8cec5164b7 | ||
|
|
be50c25cb2 | ||
|
|
ee2cc83aa1 | ||
|
|
8e62e2dcd9 | ||
|
|
1347059639 | ||
|
|
29efea561c | ||
|
|
82deca2b0f | ||
|
|
808b3b6408 | ||
|
|
eba10ebbfc | ||
|
|
13903e46a8 | ||
|
|
8aaaa583bd | ||
|
|
4b428065e2 | ||
|
|
578f9a28d0 | ||
|
|
ce2f494f96 | ||
|
|
c1c36491a6 | ||
|
|
a5c3482c6d | ||
|
|
b428274515 | ||
|
|
5dc6adae47 | ||
|
|
264b00f3c7 | ||
|
|
ada79c8ecd | ||
|
|
edd4c3d097 | ||
|
|
38f84a4f6f | ||
|
|
aee0958e99 | ||
|
|
23c067982a | ||
|
|
7e917da3ac | ||
|
|
674b49f41f | ||
|
|
4ba04df621 | ||
|
|
3274382bc7 | ||
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf | ||
|
|
9c9943c94c | ||
|
|
2e9b52371e | ||
|
|
6d6df68972 | ||
|
|
7f863d3172 | ||
|
|
6ecb379bce | ||
|
|
db45c4aaf9 | ||
|
|
198913d6f8 | ||
|
|
cd85824185 | ||
|
|
63797ac79c | ||
|
|
4633409623 | ||
|
|
13c2d33449 | ||
|
|
8cf478d7b0 | ||
|
|
f55215ff93 | ||
|
|
b5d3d0bad6 | ||
|
|
a32d81252c | ||
|
|
a1537535c4 | ||
|
|
61dd34e989 | ||
|
|
2b67cb917b | ||
|
|
ceead92ec5 | ||
|
|
df9ed94635 | ||
|
|
e891613f6f | ||
|
|
528b04427a | ||
|
|
f60a0e91d3 | ||
|
|
2308314529 | ||
|
|
04bc82bca3 | ||
|
|
acdfa33231 | ||
|
|
a03592de81 | ||
|
|
5cf9d088e8 | ||
|
|
db3294b926 | ||
|
|
1847b8c24b | ||
|
|
79e4259081 | ||
|
|
9ff9b55352 | ||
|
|
9767e24edd | ||
|
|
bb572f3846 | ||
|
|
01c5b5cee4 | ||
|
|
1049dc4a98 | ||
|
|
d1b0ba13c4 | ||
|
|
2d98f14149 | ||
|
|
19e82064b1 | ||
|
|
3b72374685 | ||
|
|
20fb8c4975 | ||
|
|
68829c0a40 | ||
|
|
2c18062f4d | ||
|
|
f17ce0afbe | ||
|
|
116b12885c | ||
|
|
3d9860db6c | ||
|
|
1a6579e954 | ||
|
|
2659d848d7 | ||
|
|
df6571f63f | ||
|
|
aab1470e08 | ||
|
|
19d43fdc37 | ||
|
|
3b09e5ea80 | ||
|
|
b904d29b25 | ||
|
|
8e2b069a7d | ||
|
|
096c418d27 | ||
|
|
d9e241ccb5 | ||
|
|
086da95525 | ||
|
|
27ed557e55 | ||
|
|
af7c823508 | ||
|
|
f2a3d8ee6d | ||
|
|
de7215d2c0 | ||
|
|
2d555b16d9 | ||
|
|
597a2571fc | ||
|
|
409b4c8fdb | ||
|
|
2e72bc96b9 | ||
|
|
fa9557348e | ||
|
|
4768e36074 | ||
|
|
e53f66de72 | ||
|
|
1d33b89ec5 | ||
|
|
7b0f420290 | ||
|
|
00824d1b83 | ||
|
|
99ad05b7b7 | ||
|
|
80273462bd | ||
|
|
2cdefc7369 | ||
|
|
80c752a422 | ||
|
|
8a8a9dc498 | ||
|
|
5f8540b3c4 | ||
|
|
4eef868047 | ||
|
|
5ebfecd822 | ||
|
|
9762560228 | ||
|
|
6310c7d6a0 | ||
|
|
fafe360672 | ||
|
|
d9b6f41940 | ||
|
|
f1ccf0eac3 | ||
|
|
d2aa924bc4 | ||
|
|
46861cd428 | ||
|
|
1c1825f8f0 | ||
|
|
a943995040 | ||
|
|
f0d2a2cf24 | ||
|
|
27ac612c82 | ||
|
|
baedb563f3 | ||
|
|
45c7914602 | ||
|
|
92d85cb8ff | ||
|
|
f5e831844a | ||
|
|
ab2fc7dc4a | ||
|
|
5e377019e4 | ||
|
|
88a4bebca0 | ||
|
|
2daccb5bd3 | ||
|
|
e2a009d4ac | ||
|
|
eda51f826a | ||
|
|
f83dc42a64 | ||
|
|
17e8c996a6 | ||
|
|
2b667f0e7e | ||
|
|
73bcf40bfd | ||
|
|
b635adcb3c | ||
|
|
2b92397e07 | ||
|
|
c23067a0e0 | ||
|
|
3aefc39c08 | ||
|
|
abd8c83df8 | ||
|
|
6cce9426f2 | ||
|
|
2e742863a1 | ||
|
|
dc13a5bb93 | ||
|
|
1f2946eb87 | ||
|
|
5eeda4c02b | ||
|
|
55a3386313 | ||
|
|
b34589a002 | ||
|
|
4486b17b49 | ||
|
|
377ee5ca7c | ||
|
|
4765f40854 | ||
|
|
e239d85821 | ||
|
|
dec419b284 | ||
|
|
6d51a23cc9 | ||
|
|
3d6eec47be | ||
|
|
464fb7008c | ||
|
|
ffa5f89f5f | ||
|
|
f19cbdc957 | ||
|
|
83bfc8d8e6 | ||
|
|
365bb3596e | ||
|
|
9ee96c2ce6 | ||
|
|
e776e266d0 | ||
|
|
d080c8a646 | ||
|
|
cb6402245e | ||
|
|
e4357277ec | ||
|
|
5dc2ab1490 | ||
|
|
9f034a9956 | ||
|
|
f700535997 | ||
|
|
31bca034d8 | ||
|
|
fac4ad5fb7 | ||
|
|
c128d33514 | ||
|
|
4a37262bcf | ||
|
|
a2295112bd | ||
|
|
834dff0f6e | ||
|
|
f4a0431fdd | ||
|
|
d072b328c0 | ||
|
|
435bbcbd7e | ||
|
|
84dc28e584 | ||
|
|
639592cb02 | ||
|
|
8c7a1a6b40 | ||
|
|
06274c4061 | ||
|
|
593925b4a5 | ||
|
|
255477e572 | ||
|
|
0acc6911d8 | ||
|
|
4ca026906b | ||
|
|
97a249e28e | ||
|
|
9f331687d3 | ||
|
|
14d68d6fcd | ||
|
|
05c5334fea | ||
|
|
d277a14c57 | ||
|
|
a7b5dbe6ee | ||
|
|
c188dc1e69 | ||
|
|
9fc9d1762e | ||
|
|
4ef86448d0 | ||
|
|
02117a6072 | ||
|
|
c272c8abac | ||
|
|
cdf2412b39 | ||
|
|
40f52791d3 | ||
|
|
a5d8bd16b9 | ||
|
|
bf113f2151 | ||
|
|
4cd010afb9 | ||
|
|
04b0bdd199 | ||
|
|
9682db93b3 | ||
|
|
1d7ffb4ac2 | ||
|
|
c5e7cd6354 | ||
|
|
b20ba7284b | ||
|
|
49498210f8 | ||
|
|
14e9121302 | ||
|
|
8bd0257dd9 | ||
|
|
93fdfb3add | ||
|
|
e02b03cc70 | ||
|
|
6831fe50a6 | ||
|
|
405108866d | ||
|
|
4d551b71e7 | ||
|
|
a7edd9838d | ||
|
|
9ef54ca6b1 | ||
|
|
4622c50288 | ||
|
|
b1ee0355af | ||
|
|
01d9b213f2 | ||
|
|
f56886bce5 | ||
|
|
88176dfd00 | ||
|
|
45dc3ea09b | ||
|
|
682b3c2409 | ||
|
|
3a93f312b4 | ||
|
|
2ca95f7a7c | ||
|
|
d2319432ab | ||
|
|
253f1a8806 | ||
|
|
2c025d5970 | ||
|
|
f0034683f9 | ||
|
|
8e8398b654 | ||
|
|
aa02293f05 | ||
|
|
f967a32308 | ||
|
|
99ec812455 | ||
|
|
eeef542eeb | ||
|
|
6d7a10b392 | ||
|
|
ca40c0a1e7 | ||
|
|
4a5ad6ea92 | ||
|
|
266dabf6c3 | ||
|
|
a4c8968539 | ||
|
|
613c030ea3 | ||
|
|
9f6bfb6714 | ||
|
|
a764814a8e | ||
|
|
0c9ce138a0 | ||
|
|
6c1dcf6a06 | ||
|
|
fffe93819d | ||
|
|
635ee421e3 | ||
|
|
3ce6d69474 | ||
|
|
cd523c7f77 | ||
|
|
1f1b759914 | ||
|
|
b78915fcbd | ||
|
|
fa0e13a5cc | ||
|
|
924bcddfd2 | ||
|
|
affbf61452 | ||
|
|
7c6b2f70e4 | ||
|
|
c46f2938de | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 | ||
|
|
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 | ||
|
|
3c46cf2210 | ||
|
|
edcdfb5937 | ||
|
|
f81efdb38c | ||
|
|
db34ded0a1 | ||
|
|
8a7a92a9b9 | ||
|
|
3eda32f9d7 | ||
|
|
c18c193ea7 | ||
|
|
79a8758448 |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Preserve pinned upstream source and attribution files byte-for-byte.
|
||||
client/command/opfor/testdata/corpus/** -text
|
||||
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
|
||||
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @BishopFox/red-team
|
||||
* @BishopFox/bf-red-team
|
||||
@@ -2,8 +2,8 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -21,17 +21,36 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Find Semver Tag
|
||||
- name: Validate Release Tag
|
||||
id: tag
|
||||
run: |
|
||||
git fetch --prune --tags -f
|
||||
tag=$(git tag --points-at "$GITHUB_SHA" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
|
||||
git fetch --prune --tags -f origin
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
|
||||
if ! printf '%s\n' "$tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
echo "Tag $tag is not a release semver tag."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "$GITHUB_SHA" "origin/master"; then
|
||||
echo "Tag $tag does not point to a commit on origin/master."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
name: End-to-end test suites
|
||||
needs: tag-check
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
uses: ./.github/workflows/comprehensive-e2e.yml
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: tag-check
|
||||
if: needs.tag-check.outputs.tag != ''
|
||||
needs:
|
||||
- tag-check
|
||||
- e2e
|
||||
if: needs.tag-check.outputs.tag != '' && needs.e2e.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 45
|
||||
@@ -41,10 +60,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Install Minisign
|
||||
run: sudo apt-get update --fix-missing && sudo apt-get -y install minisign
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
# Setup go environment
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
name: Comprehensive e2e Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: true
|
||||
default: comprehensive
|
||||
type: choice
|
||||
options:
|
||||
- comprehensive
|
||||
- rportfwd
|
||||
workflow_call:
|
||||
inputs:
|
||||
test_scope:
|
||||
description: Test scope to run across the complete platform matrix
|
||||
required: false
|
||||
default: comprehensive
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
|
||||
concurrency:
|
||||
group: comprehensive-e2e-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
name: Authorize invocation
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check actor repository permission
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
REQUESTING_ACTOR: ${{ github.triggering_actor }}
|
||||
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
|
||||
with:
|
||||
script: |
|
||||
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
|
||||
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
|
||||
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
|
||||
return;
|
||||
}
|
||||
core.info(`Validated E2E test scope ${testScope}`);
|
||||
|
||||
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
|
||||
if (isReleaseTag) {
|
||||
core.info(`Authorized release test invocation for ${context.ref}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = process.env.REQUESTING_ACTOR || context.actor;
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: actor,
|
||||
});
|
||||
core.info(`${actor} has repository permission ${data.permission} (${data.role_name})`);
|
||||
if (data.permission !== "admin") {
|
||||
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
|
||||
}
|
||||
|
||||
rportfwd-security-regression:
|
||||
name: Reverse-port-forward malicious protocol regression
|
||||
needs: authorize
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Test malicious destination poisoning and cross-session isolation
|
||||
env:
|
||||
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
|
||||
run: >-
|
||||
go test -buildvcs=false -mod=vendor
|
||||
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
|
||||
./server/c2
|
||||
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
|
||||
-count=1 -v
|
||||
|
||||
darwin:
|
||||
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'darwin/%s\n' '${{ matrix.goarch }}' > e2e-results/target-darwin-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os darwin \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-darwin-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
linux-native:
|
||||
name: comprehensive-e2e-linux-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'linux/%s\n' '${{ matrix.goarch }}' > e2e-results/target-linux-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os linux \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-linux-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
windows:
|
||||
name: comprehensive-e2e-windows-${{ matrix.goarch }}
|
||||
needs: authorize
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare coverage artifact
|
||||
run: |
|
||||
mkdir -p e2e-results
|
||||
printf 'windows/%s\n' '${{ matrix.goarch }}' > e2e-results/target-windows-${{ matrix.goarch }}.txt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o sliver-server.exe ./server
|
||||
|
||||
- name: Compile comprehensive E2E driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o sliver-comprehensive-e2e.exe ./test/e2e
|
||||
|
||||
- name: Run selected E2E scope
|
||||
run: |
|
||||
set -o pipefail
|
||||
./sliver-comprehensive-e2e.exe \
|
||||
-test.v \
|
||||
-test.run=^TestComprehensiveE2E$ \
|
||||
-test.timeout=0 \
|
||||
-repo . \
|
||||
-server ./sliver-server.exe \
|
||||
-server-arch ${{ matrix.server_arch }} \
|
||||
-target-os windows \
|
||||
-target-arch ${{ matrix.goarch }} \
|
||||
-results ./e2e-results \
|
||||
-transports mtls,wg,http \
|
||||
-implant-modes session,beacon \
|
||||
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
|
||||
|
||||
- name: Verify target reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input e2e-results
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
-transports mtls,wg,http
|
||||
|
||||
- name: Upload target coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-target-windows-${{ matrix.goarch }}
|
||||
path: e2e-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate-rportfwd:
|
||||
name: Aggregate reverse-port-forward E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
id: download
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: rportfwd-coverage-input
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
|
||||
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce all 162 required cells
|
||||
id: aggregate
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
|
||||
-input rportfwd-coverage-input
|
||||
-output rportfwd-coverage-summary
|
||||
|
||||
- name: Publish reverse-port-forward coverage table
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="rportfwd-coverage-summary/rportfwd-coverage.md"
|
||||
if [[ -s "${report}" ]]; then
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
else
|
||||
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
|
||||
- name: Upload aggregate reverse-port-forward coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-rportfwd-coverage-summary
|
||||
path: rportfwd-coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
aggregate-coverage:
|
||||
name: Aggregate comprehensive E2E coverage
|
||||
needs:
|
||||
- authorize
|
||||
- rportfwd-security-regression
|
||||
- darwin
|
||||
- linux-native
|
||||
- windows
|
||||
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
|
||||
outputs:
|
||||
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download target coverage
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: comprehensive-e2e-target-*
|
||||
path: coverage-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p coverage-input coverage-summary
|
||||
printf 'Comprehensive E2E coverage aggregate\n' > coverage-summary/README.txt
|
||||
|
||||
- name: Aggregate coverage and enforce required cells
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor ./test/e2e/report
|
||||
-input coverage-input
|
||||
-output coverage-summary
|
||||
|
||||
- name: Publish command coverage table
|
||||
id: publish_command_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
detailed_report="coverage-summary/coverage-summary.md"
|
||||
report="coverage-summary/command-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Command coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="COMPREHENSIVE_E2E_COMMAND_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Command coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'command_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [[ -f "${detailed_report}" ]]; then
|
||||
cat "${detailed_report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
printf '\n## Command coverage by OS/architecture/transport\n\n' >> "${GITHUB_STEP_SUMMARY}"
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: comprehensive-e2e-coverage-summary
|
||||
path: coverage-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed prerequisites or failed or not-run coverage
|
||||
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
|
||||
reflektor:
|
||||
name: Existing Reflektor test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/reflektor-tests.yml
|
||||
|
||||
shellcode:
|
||||
name: Shellcode E2E test matrix
|
||||
needs: authorize
|
||||
if: ${{ inputs.test_scope != 'rportfwd' }}
|
||||
uses: ./.github/workflows/shellcode-tests.yml
|
||||
@@ -0,0 +1,56 @@
|
||||
name: golangci-lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- ".golangci.yml"
|
||||
- ".github/workflows/golangci-lint.yml"
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- "go.work"
|
||||
- "go.work.sum"
|
||||
- "Makefile"
|
||||
- "vendor/**"
|
||||
- "**/*.go"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: golangci-lint (${{ matrix.goos }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: amd64
|
||||
CGO_ENABLED: "0"
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.13.1
|
||||
# Gate only findings introduced since the event base revision.
|
||||
args: --new-from-rev=${{ github.event.pull_request.base.sha || github.event.before }} ./client/... ./server/... ./util/... ./protobuf/... ./docs ./implant/...
|
||||
@@ -0,0 +1,164 @@
|
||||
name: Reflektor Integration Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
darwin:
|
||||
name: reflektor-darwin-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: macos-15
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: darwin
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os darwin
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
linux-native:
|
||||
name: reflektor-linux-${{ matrix.goarch }}-native
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
goarch: arm64
|
||||
server_arch: arm64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: linux
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e
|
||||
-repo .
|
||||
-server ./sliver-server
|
||||
-target-os linux
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
|
||||
windows:
|
||||
name: reflektor-windows-${{ matrix.goarch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 180
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
goarch: amd64
|
||||
server_arch: amd64
|
||||
- runner: windows-11-arm
|
||||
goarch: arm64
|
||||
server_arch: amd64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -mod=vendor ./util/cmd/assets
|
||||
|
||||
- name: Build Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags go_sqlite,server -o sliver-server.exe ./server
|
||||
|
||||
- name: Build Reflektor integration driver
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
GOOS: windows
|
||||
run: go build -mod=vendor -trimpath -tags client,go_sqlite -o sliver-reflektor-e2e.exe ./test/reflektor
|
||||
|
||||
- name: Load Sliver with Reflektor
|
||||
run: >-
|
||||
./sliver-reflektor-e2e.exe
|
||||
-repo .
|
||||
-server ./sliver-server.exe
|
||||
-target-os windows
|
||||
-target-arch ${{ matrix.goarch }}
|
||||
@@ -0,0 +1,201 @@
|
||||
name: Shellcode E2E Test Matrix
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
shellcode_coverage_markdown:
|
||||
description: Shellcode E2E coverage table in Markdown
|
||||
value: ${{ jobs.aggregate.outputs.shellcode_coverage_markdown }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
target:
|
||||
name: shellcode-e2e-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 360
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-26
|
||||
target_os: darwin
|
||||
target_arch: arm64
|
||||
server_os: darwin
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04
|
||||
target_os: linux
|
||||
target_arch: amd64
|
||||
server_os: linux
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: ubuntu-24.04-arm
|
||||
target_os: linux
|
||||
target_arch: arm64
|
||||
server_os: linux
|
||||
server_arch: arm64
|
||||
server_executable: ./sliver-server
|
||||
test_executable: ./sliver-shellcode-e2e
|
||||
- runner: windows-2022
|
||||
target_os: windows
|
||||
target_arch: amd64
|
||||
server_os: windows
|
||||
server_arch: amd64
|
||||
server_executable: ./sliver-server.exe
|
||||
test_executable: ./sliver-shellcode-e2e.exe
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare target artifact
|
||||
run: |
|
||||
mkdir -p shellcode-results
|
||||
printf '%s/%s\n' \
|
||||
'${{ matrix.target_os }}' \
|
||||
'${{ matrix.target_arch }}' \
|
||||
> 'shellcode-results/target-${{ matrix.target_os }}-${{ matrix.target_arch }}.txt'
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Download Sliver build assets
|
||||
run: go run -buildvcs=false -mod=vendor -trimpath ./util/cmd/assets
|
||||
|
||||
- name: Build unmodified Sliver server
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.server_arch }}
|
||||
GOOS: ${{ matrix.server_os }}
|
||||
run: >-
|
||||
go build -buildvcs=false -mod=vendor -trimpath -tags go_sqlite,server
|
||||
-o ${{ matrix.server_executable }} ./server
|
||||
|
||||
- name: Compile shellcode E2E client
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
GOARCH: ${{ matrix.target_arch }}
|
||||
GOOS: ${{ matrix.target_os }}
|
||||
run: >-
|
||||
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
|
||||
-o ${{ matrix.test_executable }} ./test/e2e
|
||||
|
||||
- name: Run shellcode generation and execution tests
|
||||
run: >-
|
||||
${{ matrix.test_executable }}
|
||||
-test.v
|
||||
-test.run=^TestShellcodeE2E$
|
||||
-test.timeout=0
|
||||
-repo .
|
||||
-server ${{ matrix.server_executable }}
|
||||
-server-arch ${{ matrix.server_arch }}
|
||||
-target-os ${{ matrix.target_os }}
|
||||
-target-arch ${{ matrix.target_arch }}
|
||||
-results ./shellcode-results
|
||||
-transports mtls,wg,http
|
||||
-implant-modes session,beacon
|
||||
-beacon-interval 10s
|
||||
-shellcode-sgn-samples 4
|
||||
|
||||
- name: Upload target shellcode results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-target-${{ matrix.target_os }}-${{ matrix.target_arch }}
|
||||
path: shellcode-results
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 14
|
||||
|
||||
aggregate:
|
||||
name: Aggregate shellcode E2E coverage
|
||||
needs: target
|
||||
if: ${{ always() }}
|
||||
outputs:
|
||||
shellcode_coverage_markdown: ${{ steps.publish_shellcode_coverage.outputs.shellcode_coverage_markdown }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Prepare aggregate artifact
|
||||
run: |
|
||||
mkdir -p shellcode-input shellcode-summary
|
||||
printf 'Shellcode E2E coverage aggregate\n' > shellcode-summary/README.txt
|
||||
|
||||
- name: Download target shellcode results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: shellcode-e2e-target-*
|
||||
path: shellcode-input
|
||||
merge-multiple: true
|
||||
|
||||
- name: Aggregate shellcode coverage
|
||||
id: aggregate
|
||||
continue-on-error: true
|
||||
run: >-
|
||||
go run -buildvcs=false -mod=vendor -trimpath ./test/e2e/shellcode-report
|
||||
-input ./shellcode-input
|
||||
-output ./shellcode-summary
|
||||
|
||||
- name: Publish shellcode coverage table
|
||||
id: publish_shellcode_coverage
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
report="shellcode-summary/shellcode-coverage.md"
|
||||
if [[ ! -s "${report}" ]]; then
|
||||
printf 'Shellcode coverage report is missing or empty: %s\n' "${report}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
delimiter="SHELLCODE_E2E_COVERAGE_EOF"
|
||||
if grep -Fxq "${delimiter}" "${report}"; then
|
||||
printf 'Shellcode coverage report contains the reserved output delimiter: %s\n' "${delimiter}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf 'shellcode_coverage_markdown<<%s\n' "${delimiter}"
|
||||
cat "${report}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Upload aggregate shellcode coverage
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shellcode-e2e-coverage-summary
|
||||
path: shellcode-summary
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail on failed or not-run shellcode coverage
|
||||
if: ${{ always() && steps.aggregate.outcome != 'success' }}
|
||||
run: exit 1
|
||||
@@ -6,11 +6,15 @@ on:
|
||||
paths:
|
||||
- "go.mod"
|
||||
- "go.sum"
|
||||
- ".gitattributes"
|
||||
- "client/command/opfor/testdata/corpus/**"
|
||||
- ".github/workflows/unit-tests.yml"
|
||||
- "Makefile"
|
||||
- "**/*.go"
|
||||
- "**/*.c"
|
||||
- "**/*.h"
|
||||
- "**/*.sh"
|
||||
- "**/*.ps1"
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -18,15 +22,15 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
linux-windows-build-test:
|
||||
name: Linux & Windows Test
|
||||
linux-build-test:
|
||||
name: Linux Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: OS Packages
|
||||
run: |
|
||||
@@ -48,8 +52,64 @@ jobs:
|
||||
- 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
|
||||
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:
|
||||
@@ -57,10 +117,10 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Go 1.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -74,18 +134,31 @@ jobs:
|
||||
- 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
|
||||
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.25
|
||||
- name: Go 1.27.1
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.25"
|
||||
go-version: "1.27.1"
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/crush
|
||||
|
||||
/examples
|
||||
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
modules-download-mode: vendor
|
||||
build-tags:
|
||||
- client
|
||||
- server
|
||||
- osusergo
|
||||
- netgo
|
||||
- go_sqlite
|
||||
# Raw implant entry points are Go templates; lint their supporting packages.
|
||||
- sliver_lint
|
||||
|
||||
linters:
|
||||
default: standard
|
||||
enable:
|
||||
- gocyclo
|
||||
- misspell
|
||||
- revive
|
||||
settings:
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
@@ -1,41 +1,18 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `client/` holds the CLI client and interactive commands.
|
||||
- `server/` contains the server runtime, C2 logic, configs, and web assets.
|
||||
- `implant/` is the implant runtime and transport implementations.
|
||||
- `protobuf/` stores protobuf definitions and generated Go types.
|
||||
- `util/` is shared helpers (crypto, encoders, minisign, etc.).
|
||||
- `docs/sliver-docs/` is the Next.js documentation site and prebuild scripts.
|
||||
- `vendor/` is the vendored Go dependency tree used by builds.
|
||||
- Build outputs land in the repo root as `sliver-client*` and `sliver-server*`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
|
||||
- `make client` builds a CGO-free client; `make debug` keeps symbols.
|
||||
- `make pb` regenerates protobuf stubs from `protobuf/`.
|
||||
- `./go-tests.sh` runs the curated Go test suite with build tags.
|
||||
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
|
||||
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
|
||||
- Go toolchain: the Makefile enforces Go >= 1.24.
|
||||
|
||||
## 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`.
|
||||
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
|
||||
|
||||
## Testing Guidelines
|
||||
- Go tests follow standard `_test.go` naming within each package.
|
||||
- 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.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits must be signed for PRs to `master`; work in a feature branch.
|
||||
- Commit subjects are short and imperative (e.g., "Fix ...", "Add ...", "Update ...", "Bump ...").
|
||||
- Changes to `vendor/` should be in a distinct commit.
|
||||
- PRs should link the tracking issue, note tests run, and confirm `gofmt` plus unit tests as applicable.
|
||||
|
||||
## Security Notes
|
||||
- Default to secure configuration; fail closed rather than open.
|
||||
- Avoid insecure algorithms (MD5/SHA1/AES-ECB); prefer AES-GCM and modern curves.
|
||||
- Treat user input as untrusted: canonicalize paths and use restrictive file permissions.
|
||||
@@ -2,180 +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 date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 24
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 27
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_ARGS) $(GOLANGCI_LINT_PACKAGES)
|
||||
|
||||
ifneq ($(OS),Windows_NT)
|
||||
|
||||
#
|
||||
# Prerequisites
|
||||
#
|
||||
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
|
||||
EXECUTABLES = uname sed git date cut $(GO)
|
||||
K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Build Information
|
||||
#
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
STATIC_TARGET := linux
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
UNAME_P := $(shell uname -p)
|
||||
|
||||
# Programs required for generating protobuf/grpc files
|
||||
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
|
||||
ifeq ($(MAKECMDGOALS), pb)
|
||||
K := $(foreach exec,$(PB_COMPILERS),\
|
||||
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
|
||||
endif
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
|
||||
# *** Darwin ***
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
STATIC_TARGET := macos
|
||||
endif
|
||||
|
||||
# If no target is specified, determine GOARCH
|
||||
ifeq ($(UNAME_P),arm)
|
||||
ifeq ($(MAKECMDGOALS), )
|
||||
ifeq ($(origin GOARCH), undefined)
|
||||
ENV += GOARCH=arm64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
#
|
||||
# Targets
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
|
||||
|
||||
#
|
||||
# Targets
|
||||
# >>> WINDOWS <<<
|
||||
#
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
else
|
||||
|
||||
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
|
||||
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
|
||||
SHELL := cmd.exe
|
||||
.SHELLFLAGS := /C
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
|
||||
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
|
||||
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
|
||||
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
|
||||
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
|
||||
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
|
||||
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
|
||||
|
||||
.PHONY: pb
|
||||
pb:
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
|
||||
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
|
||||
|
||||
validate-go-version:
|
||||
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
exit 0 ;\
|
||||
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
|
||||
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
rm -rf ./server/assets/fs/darwin/amd64
|
||||
rm -rf ./server/assets/fs/darwin/arm64
|
||||
rm -rf ./server/assets/fs/windows/amd64
|
||||
rm -rf ./server/assets/fs/linux/amd64
|
||||
rm -f ./server/assets/fs/*.zip
|
||||
rm -f ./.downloaded_assets
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
|
||||
|
||||
.downloaded_assets:
|
||||
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
|
||||
touch ./.downloaded_assets
|
||||
COMMA := ,
|
||||
|
||||
ifeq ($(MAKECMDGOALS), linux)
|
||||
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
define windows_exec
|
||||
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
|
||||
endef
|
||||
|
||||
define windows_go_build
|
||||
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
|
||||
endef
|
||||
|
||||
.PHONY: default
|
||||
default: clean validate-go-version
|
||||
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: client
|
||||
client: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
|
||||
|
||||
.PHONY: macos-amd64
|
||||
macos-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: macos-arm64
|
||||
macos-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-amd64
|
||||
linux-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: linux-arm64
|
||||
linux-arm64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: windows-amd64
|
||||
windows-amd64: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
|
||||
|
||||
.PHONY: clients
|
||||
clients: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: servers
|
||||
servers: clean .downloaded_assets validate-go-version
|
||||
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
|
||||
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: pb
|
||||
pb: validate-pb-compilers
|
||||
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
|
||||
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
|
||||
|
||||
.PHONY: debug
|
||||
debug: clean
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
|
||||
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
|
||||
|
||||
.PHONY: validate-pb-compilers
|
||||
validate-pb-compilers:
|
||||
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
|
||||
|
||||
validate-go-version:
|
||||
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean
|
||||
-rmdir /S /Q server\assets\fs\darwin\amd64
|
||||
-rmdir /S /Q server\assets\fs\darwin\arm64
|
||||
-rmdir /S /Q server\assets\fs\windows\amd64
|
||||
-rmdir /S /Q server\assets\fs\linux\amd64
|
||||
-del /Q /F server\assets\fs\*.zip 2>NUL
|
||||
-del /Q /F .downloaded_assets 2>NUL
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
|
||||
|
||||
.downloaded_assets:
|
||||
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
|
||||
@type NUL > .downloaded_assets
|
||||
|
||||
endif
|
||||
@@ -4,7 +4,9 @@ Sliver is an open source cross-platform adversary emulation/red team framework,
|
||||
|
||||
The server and client support MacOS, Windows, and Linux. Implants are supported on MacOS, Windows, and Linux (and possibly every Golang compiler target but we've not tested them all).
|
||||
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://goreportcard.com/report/github.com/BishopFox/sliver) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/BishopFox/sliver/actions/workflows/autorelease.yml) [](https://github.com/BishopFox/sliver/actions/workflows/golangci-lint.yml) [](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
Visit [https://sliver.sh/](https://sliver.sh/) for tutorials and documentation.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -42,4 +44,3 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
### 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.
|
||||
|
||||
+12
-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 {
|
||||
@@ -108,7 +115,7 @@ func Setup(force bool, echo bool) {
|
||||
if force || localVer == "" || localVer != ver.GitCommit {
|
||||
if echo {
|
||||
fmt.Printf(`
|
||||
Sliver Copyright (C) 2025 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.`)
|
||||
@@ -116,7 +123,6 @@ under certain conditions; type 'licenses' for details.`)
|
||||
}
|
||||
saveAssetVersion(appDir)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(appDir, settingsFileName)); os.IsNotExist(err) {
|
||||
SaveSettings(nil)
|
||||
}
|
||||
_, _ = LoadSettings()
|
||||
_, _ = LoadSpoofMetadataConfig()
|
||||
}
|
||||
+18
-7
@@ -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: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,9 @@ func init() {
|
||||
|
||||
rootCmd.TraverseChildren = true
|
||||
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
|
||||
rootCmd.PersistentFlags().Bool(enableWGFlag, false, "force multiplayer connections through the operator config's WireGuard wrapper")
|
||||
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "force multiplayer connections to use direct mTLS")
|
||||
rootCmd.MarkFlagsMutuallyExclusive(enableWGFlag, disableWGFlag)
|
||||
|
||||
// Create the console client, without any RPC or commands bound to it yet.
|
||||
// This created before anything so that multiple commands can make use of
|
||||
|
||||
@@ -26,16 +26,16 @@ import (
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() *assets.ClientConfig {
|
||||
func selectConfig() (string, *assets.ClientConfig) {
|
||||
configs := assets.GetConfigs()
|
||||
|
||||
if len(configs) == 0 {
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if len(configs) == 1 {
|
||||
for _, config := range configs {
|
||||
return config
|
||||
for key, config := range configs {
|
||||
return key, config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func selectConfig() *assets.ClientConfig {
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return configs[selection]
|
||||
return selection, configs[selection]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
bspinner "charm.land/bubbles/v2/spinner"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
minConnectSpinnerDuration = 900 * time.Millisecond
|
||||
minConnectStatusDuration = 350 * time.Millisecond
|
||||
)
|
||||
|
||||
type connectResult struct {
|
||||
rpc rpcpb.SliverRPCClient
|
||||
conn *grpc.ClientConn
|
||||
err error
|
||||
}
|
||||
|
||||
func connectWithSpinner(out io.Writer, target string, connect func(transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error)) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
statusCh := make(chan string, 8)
|
||||
resultCh := make(chan connectResult, 1)
|
||||
|
||||
go func() {
|
||||
rpc, conn, err := connect(func(status string) {
|
||||
sendStatus(statusCh, status)
|
||||
})
|
||||
resultCh <- connectResult{rpc: rpc, conn: conn, err: err}
|
||||
}()
|
||||
|
||||
currentStatus := ""
|
||||
currentStatusSince := time.Time{}
|
||||
queuedStatus := ""
|
||||
lastWidth := 0
|
||||
spinner := bspinner.New(bspinner.WithSpinner(bspinner.Line))
|
||||
ticker := time.NewTicker(spinner.Spinner.FPS)
|
||||
defer ticker.Stop()
|
||||
startedAt := time.Now()
|
||||
var pendingResult *connectResult
|
||||
|
||||
setStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" || status == currentStatus {
|
||||
return
|
||||
}
|
||||
currentStatus = status
|
||||
currentStatusSince = time.Now()
|
||||
}
|
||||
|
||||
stageStatus := func(status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
if currentStatus == "" || time.Since(currentStatusSince) >= minConnectStatusDuration {
|
||||
queuedStatus = ""
|
||||
setStatus(status)
|
||||
return
|
||||
}
|
||||
queuedStatus = status
|
||||
}
|
||||
|
||||
flushQueuedStatus := func() {
|
||||
if queuedStatus == "" || time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return
|
||||
}
|
||||
nextStatus := queuedStatus
|
||||
queuedStatus = ""
|
||||
setStatus(nextStatus)
|
||||
}
|
||||
|
||||
canReturnPendingResult := func() bool {
|
||||
if pendingResult == nil {
|
||||
return false
|
||||
}
|
||||
if pendingResult.err != nil {
|
||||
return true
|
||||
}
|
||||
if time.Since(startedAt) < minConnectSpinnerDuration {
|
||||
return false
|
||||
}
|
||||
if queuedStatus != "" {
|
||||
return false
|
||||
}
|
||||
if !currentStatusSince.IsZero() && time.Since(currentStatusSince) < minConnectStatusDuration {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
render := func() {
|
||||
line := fmt.Sprintf("%s %s", spinner.View(), formatConnectSpinnerMessage(target, currentStatus))
|
||||
lastWidth = writeSpinnerLine(out, line, lastWidth)
|
||||
}
|
||||
|
||||
render()
|
||||
for {
|
||||
select {
|
||||
case status := <-statusCh:
|
||||
stageStatus(status)
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
render()
|
||||
|
||||
case result := <-resultCh:
|
||||
pendingResult = &result
|
||||
|
||||
case <-ticker.C:
|
||||
spinner, _ = spinner.Update(spinner.Tick())
|
||||
flushQueuedStatus()
|
||||
render()
|
||||
if canReturnPendingResult() {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
|
||||
if pendingResult != nil && pendingResult.err != nil {
|
||||
result := *pendingResult
|
||||
pendingResult = nil
|
||||
clearSpinnerLine(out, lastWidth)
|
||||
return result.rpc, result.conn, result.err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendStatus(statusCh chan string, status string) {
|
||||
status = strings.TrimSpace(status)
|
||||
if status == "" {
|
||||
return
|
||||
}
|
||||
statusCh <- status
|
||||
}
|
||||
|
||||
func formatConnectSpinnerMessage(target string, status string) string {
|
||||
target = strings.TrimSpace(target)
|
||||
status = strings.TrimSpace(status)
|
||||
|
||||
if target == "" {
|
||||
if status == "" {
|
||||
return "Connecting ..."
|
||||
}
|
||||
return fmt.Sprintf("Connecting (%s) ...", status)
|
||||
}
|
||||
if status == "" {
|
||||
return fmt.Sprintf("Connecting to %s ...", target)
|
||||
}
|
||||
return fmt.Sprintf("Connecting to %s (%s) ...", target, status)
|
||||
}
|
||||
|
||||
func writeSpinnerLine(out io.Writer, line string, lastWidth int) int {
|
||||
width := utf8.RuneCountInString(line)
|
||||
padding := ""
|
||||
if lastWidth > width {
|
||||
padding = strings.Repeat(" ", lastWidth-width)
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s%s", line, padding)
|
||||
if width > lastWidth {
|
||||
return width
|
||||
}
|
||||
return lastWidth
|
||||
}
|
||||
|
||||
func clearSpinnerLine(out io.Writer, lastWidth int) {
|
||||
if lastWidth <= 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(out, "\r%s\r", strings.Repeat(" ", lastWidth))
|
||||
}
|
||||
@@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-34
@@ -19,7 +19,6 @@ package cli
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -30,7 +29,6 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
)
|
||||
|
||||
// consoleCmd generates the console with required pre/post runners.
|
||||
@@ -46,16 +44,17 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
pre = func(cmd *cobra.Command, _ []string) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configs := assets.GetConfigs()
|
||||
if len(configs) == 0 {
|
||||
fmt.Printf("No config files found at %s (see --help)\n", assets.GetConfigDir())
|
||||
return nil
|
||||
}
|
||||
config := selectConfig()
|
||||
configKey, config := selectConfig()
|
||||
if config == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -66,46 +65,29 @@ func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *
|
||||
return nil
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s:%d", config.LHost, config.LPort)
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
var ln *grpc.ClientConn
|
||||
|
||||
// Don't clobber output when simply running an implant command from system shell.
|
||||
if run {
|
||||
fmt.Printf("Connecting to %s:%d ...\n", config.LHost, config.LPort)
|
||||
rpc, ln, err = connectWithSpinner(os.Stdout, target, func(statusFn transport.ConnectStatusFn) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
|
||||
return transport.MTLSConnectWithStatus(config, statusFn)
|
||||
})
|
||||
} else {
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
}
|
||||
|
||||
var rpc rpcpb.SliverRPCClient
|
||||
|
||||
rpc, ln, err = transport.MTLSConnect(config)
|
||||
if err != nil {
|
||||
fmt.Printf("Connection to server failed %s", err)
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait for any connection state changes and exit if the connection is lost.
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
return console.StartClient(con, rpc, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
return console.StartClient(con, rpc, ln, &console.ConnectionDetails{ConfigKey: configKey, Config: config}, command.ServerCommands(con, nil), command.SliverCommands(con), run, rcScript)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
|
||||
}
|
||||
|
||||
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, _ := cmd.Flags().GetString("config")
|
||||
config, err := loadMCPClientConfig(configPath)
|
||||
if err != nil {
|
||||
@@ -55,7 +59,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
|
||||
fmt.Printf("Connection to server failed %s\n", err)
|
||||
return nil
|
||||
}
|
||||
defer ln.Close()
|
||||
defer transport.CloseGRPCConnection(ln)
|
||||
|
||||
con.Rpc = rpc
|
||||
go handleConnectionLost(ln)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
enableWGFlag = "enable-wg"
|
||||
disableWGFlag = "disable-wg"
|
||||
)
|
||||
|
||||
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
|
||||
if cmd == nil {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
return nil
|
||||
}
|
||||
|
||||
enableWG, err := cmd.Flags().GetBool(enableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.Flags().Changed(enableWGFlag) && cmd.Flags().Changed(disableWGFlag) {
|
||||
return fmt.Errorf("--%s and --%s cannot be used together", enableWGFlag, disableWGFlag)
|
||||
}
|
||||
|
||||
mode := transport.MultiplayerConnectAuto
|
||||
switch {
|
||||
case enableWG:
|
||||
mode = transport.MultiplayerConnectEnableWG
|
||||
case disableWG:
|
||||
mode = transport.MultiplayerConnectDisableWG
|
||||
}
|
||||
transport.SetMultiplayerConnectMode(mode)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestWireGuardOverrideFlagsDefaultToFalse(t *testing.T) {
|
||||
for _, name := range []string{enableWGFlag, disableWGFlag} {
|
||||
flag := rootCmd.PersistentFlags().Lookup(name)
|
||||
if flag == nil {
|
||||
t.Fatalf("expected --%s persistent flag", name)
|
||||
}
|
||||
if flag.DefValue != "false" {
|
||||
t.Fatalf("expected --%s to default to false, got %q", name, flag.DefValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDefaultsToAuto(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to honor an explicitly enabled WireGuard config")
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected automatic mode to ignore a legacy WireGuard config without the opt-in marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeEnableWGForcesLegacyConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if !transport.MultiplayerConnectUsesWireGuard(legacyWGClientConfig()) {
|
||||
t.Fatal("expected --enable-wg to force a complete legacy config through WireGuard")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeDisableWGOverridesEnabledConfig(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
if err := applyMultiplayerConnectMode(cmd); err != nil {
|
||||
t.Fatalf("apply multiplayer mode: %v", err)
|
||||
}
|
||||
if transport.MultiplayerConnectUsesWireGuard(enabledWGClientConfig()) {
|
||||
t.Fatal("expected --disable-wg to force direct mTLS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsConflictingOverrides(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "true"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected conflicting WireGuard overrides to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyMultiplayerConnectModeRejectsBothOverridesWhenExplicitlyFalse(t *testing.T) {
|
||||
resetMultiplayerConnectModeForTest(t)
|
||||
cmd := newTransportModeTestCommand()
|
||||
if err := cmd.Flags().Set(enableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", enableWGFlag, err)
|
||||
}
|
||||
if err := cmd.Flags().Set(disableWGFlag, "false"); err != nil {
|
||||
t.Fatalf("set --%s: %v", disableWGFlag, err)
|
||||
}
|
||||
|
||||
err := applyMultiplayerConnectMode(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected explicitly combining both WireGuard override flags to fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot be used together") {
|
||||
t.Fatalf("unexpected conflict error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTransportModeTestCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool(enableWGFlag, false, "")
|
||||
cmd.Flags().Bool(disableWGFlag, false, "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func resetMultiplayerConnectModeForTest(t *testing.T) {
|
||||
t.Helper()
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
t.Cleanup(func() {
|
||||
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
|
||||
})
|
||||
}
|
||||
|
||||
func enabledWGClientConfig() *assets.ClientConfig {
|
||||
config := legacyWGClientConfig()
|
||||
config.WG.Enabled = true
|
||||
return config
|
||||
}
|
||||
|
||||
func legacyWGClientConfig() *assets.ClientConfig {
|
||||
return &assets.ClientConfig{
|
||||
WG: &assets.ClientWGConfig{
|
||||
ServerPubKey: "server-pub",
|
||||
ClientPrivateKey: "client-priv",
|
||||
ClientIP: "100.64.0.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
|
||||
## Sub-packages
|
||||
|
||||
- `ai/` – Implements the 'ai' Bubble Tea layout preview command for the Sliver client console.
|
||||
- `alias/` – Implements the 'alias' command group for the Sliver client console.
|
||||
- `armory/` – Implements the 'armory' command group for the Sliver client console.
|
||||
- `backdoor/` – Implements the 'backdoor' command group for the Sliver client console.
|
||||
@@ -25,6 +26,7 @@ Shared registration glue, scaffolding, and helpers for Sliver client console com
|
||||
- `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.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# client/command/ai
|
||||
|
||||
Implements the `ai` command for the Sliver client console.
|
||||
|
||||
The TUI loads server-backed AI conversation threads over gRPC, submits prompts to the server, and refreshes when `AIConversationEvent` updates arrive with assistant replies or failure messages.
|
||||
@@ -0,0 +1,242 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/termio"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var openAIProgramTTY = tea.OpenTTY
|
||||
|
||||
// AICmd launches the AI conversation TUI.
|
||||
func AICmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Connect to a server before using `ai`.\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
listenerID, listener := con.CreateEventListener()
|
||||
defer con.RemoveEventListener(listenerID)
|
||||
restoreEventNotifications := con.SuppressEventNotifications()
|
||||
defer restoreEventNotifications()
|
||||
|
||||
model := newAIModel(con, buildAIContext(con), listener)
|
||||
model.showExperimentalWarningModal()
|
||||
|
||||
width, height := 100, 30
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
opts := []tea.ProgramOption{
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
}
|
||||
ttyOpts, cleanup := configureAIProgramTTY()
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
opts = append(opts, ttyOpts...)
|
||||
|
||||
program := tea.NewProgram(model, opts...)
|
||||
if _, err := program.Run(); err != nil {
|
||||
con.PrintErrorf("AI TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Console logging can swap stdout/stderr to pipes so session output can be tee'd
|
||||
// to disk. Bubble Tea needs a real TTY output to enable terminal features like
|
||||
// mouse reporting, so bind the AI TUI back to the interactive terminal when
|
||||
// stdout is no longer the controlling TTY.
|
||||
func configureAIProgramTTY() ([]tea.ProgramOption, func()) {
|
||||
stdinTTY := isAITTY(os.Stdin)
|
||||
stdoutTTY := isAITTY(os.Stdout)
|
||||
|
||||
switch {
|
||||
case !stdinTTY || stdoutTTY:
|
||||
return nil, nil
|
||||
|
||||
case isAITTY(termio.InteractiveInput()) && isAITTY(termio.InteractiveOutput()):
|
||||
return []tea.ProgramOption{
|
||||
tea.WithInput(termio.InteractiveInput()),
|
||||
tea.WithOutput(termio.InteractiveOutput()),
|
||||
}, nil
|
||||
|
||||
default:
|
||||
inTTY, outTTY, err := openAIProgramTTY()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []tea.ProgramOption{
|
||||
tea.WithInput(inTTY),
|
||||
tea.WithOutput(outTTY),
|
||||
}, func() {
|
||||
_ = inTTY.Close()
|
||||
if outTTY != inTTY {
|
||||
_ = outTTY.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isAITTY(file *os.File) bool {
|
||||
return file != nil && term.IsTerminal(int(file.Fd()))
|
||||
}
|
||||
|
||||
type aiContext struct {
|
||||
target aiTargetSummary
|
||||
connection aiConnectionSummary
|
||||
status string
|
||||
}
|
||||
|
||||
type aiTargetSummary struct {
|
||||
SessionID string
|
||||
BeaconID string
|
||||
Label string
|
||||
Host string
|
||||
OS string
|
||||
Arch string
|
||||
C2 string
|
||||
Mode string
|
||||
Details []string
|
||||
}
|
||||
|
||||
type aiConnectionSummary struct {
|
||||
Profile string
|
||||
Server string
|
||||
Operator string
|
||||
State string
|
||||
}
|
||||
|
||||
func defaultAITargetSummary() aiTargetSummary {
|
||||
return aiTargetSummary{
|
||||
Label: "No active target",
|
||||
Host: "Select a session or beacon with `use` or ctrl+s",
|
||||
Mode: "offline preview",
|
||||
C2: "n/a",
|
||||
OS: "unknown",
|
||||
Arch: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
func aiSessionTargetSummary(session *clientpb.Session) aiTargetSummary {
|
||||
if session == nil {
|
||||
return defaultAITargetSummary()
|
||||
}
|
||||
|
||||
return aiTargetSummary{
|
||||
SessionID: session.ID,
|
||||
Label: fmt.Sprintf("Session %s", fallback(session.Name, session.ID)),
|
||||
Host: fallback(session.Hostname, "<unknown host>"),
|
||||
OS: fallback(session.OS, "unknown"),
|
||||
Arch: fallback(session.Arch, "unknown"),
|
||||
C2: fallback(session.ActiveC2, "unknown"),
|
||||
Mode: "interactive session",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(session.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", session.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(session.RemoteAddress, "<unknown>")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func aiBeaconTargetSummary(beacon *clientpb.Beacon) aiTargetSummary {
|
||||
if beacon == nil {
|
||||
return defaultAITargetSummary()
|
||||
}
|
||||
|
||||
return aiTargetSummary{
|
||||
BeaconID: beacon.ID,
|
||||
Label: fmt.Sprintf("Beacon %s", fallback(beacon.Name, beacon.ID)),
|
||||
Host: fallback(beacon.Hostname, "<unknown host>"),
|
||||
OS: fallback(beacon.OS, "unknown"),
|
||||
Arch: fallback(beacon.Arch, "unknown"),
|
||||
C2: fallback(beacon.ActiveC2, "unknown"),
|
||||
Mode: "asynchronous beacon",
|
||||
Details: []string{
|
||||
fmt.Sprintf("User: %s", fallback(beacon.Username, "<unknown>")),
|
||||
fmt.Sprintf("PID: %d", beacon.PID),
|
||||
fmt.Sprintf("Remote: %s", fallback(beacon.RemoteAddress, "<unknown>")),
|
||||
fmt.Sprintf("Interval: %s", time.Duration(beacon.Interval).String()),
|
||||
fmt.Sprintf("Next checkin: %s", formatUnix(beacon.NextCheckin)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildAIContext(con *console.SliverClient) aiContext {
|
||||
ctx := aiContext{
|
||||
target: defaultAITargetSummary(),
|
||||
connection: aiConnectionSummary{
|
||||
Profile: "<disconnected>",
|
||||
Server: "<unknown>",
|
||||
Operator: "<unknown>",
|
||||
State: "idle",
|
||||
},
|
||||
status: "Loading AI conversations from the server...",
|
||||
}
|
||||
|
||||
if con != nil {
|
||||
if details, state, ok := con.CurrentConnection(); ok {
|
||||
ctx.connection.State = strings.ToLower(state.String())
|
||||
if details != nil && details.Config != nil {
|
||||
ctx.connection.Profile = fallback(details.ConfigKey, "<profile unavailable>")
|
||||
ctx.connection.Server = fmt.Sprintf("%s:%d", details.Config.LHost, details.Config.LPort)
|
||||
ctx.connection.Operator = fallback(details.Config.Operator, "<unknown>")
|
||||
}
|
||||
}
|
||||
|
||||
session, beacon := con.ActiveTarget.Get()
|
||||
switch {
|
||||
case session != nil:
|
||||
ctx.target = aiSessionTargetSummary(session)
|
||||
case beacon != nil:
|
||||
ctx.target = aiBeaconTargetSummary(beacon)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func fallback(value, def string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatUnix(ts int64) string {
|
||||
if ts <= 0 {
|
||||
return "<unknown>"
|
||||
}
|
||||
return time.Unix(ts, 0).Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the ai command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the ai command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newAICommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newAICommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.AIStr,
|
||||
Short: "Open the Sliver AI conversation TUI",
|
||||
Long: help.GetHelpFor([]string{consts.AIStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
AICmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package ai
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestLoadAIStateCmdLoadsConversationOverRPC(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{
|
||||
Conversations: []*clientpb.AIConversation{
|
||||
{ID: "conv-1", Provider: "openai", Model: "gpt-test", Title: "First"},
|
||||
{ID: "conv-2", Provider: "openai", Model: "gpt-test", Title: "Second"},
|
||||
},
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-1": {
|
||||
ID: "conv-1",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "First",
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "Hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
cmd := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")
|
||||
msg := cmd()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.selectedID != "conv-1" {
|
||||
t.Fatalf("unexpected selected conversation id: got=%q want=%q", loaded.selectedID, "conv-1")
|
||||
}
|
||||
if loaded.conversation == nil || loaded.conversation.GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected loaded conversation: %+v", loaded.conversation)
|
||||
}
|
||||
if len(loaded.conversations) != 2 {
|
||||
t.Fatalf("unexpected conversation count: got=%d want=%d", len(loaded.conversations), 2)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if server.getProvidersCalls != 1 {
|
||||
t.Fatalf("expected GetAIProviders to be called once, got %d", server.getProvidersCalls)
|
||||
}
|
||||
if server.getConversationsCalls != 1 {
|
||||
t.Fatalf("expected GetAIConversations to be called once, got %d", server.getConversationsCalls)
|
||||
}
|
||||
if len(server.getConversationReqs) != 1 {
|
||||
t.Fatalf("expected GetAIConversation to be called once, got %d", len(server.getConversationReqs))
|
||||
}
|
||||
if server.getConversationReqs[0].GetID() != "conv-1" || !server.getConversationReqs[0].GetIncludeMessages() {
|
||||
t.Fatalf("unexpected GetAIConversation request: %+v", server.getConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAIStateCmdCreatesConversationWhenNoneExist(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
providersResp: &clientpb.AIProviderConfigs{
|
||||
Providers: []*clientpb.AIProviderConfig{{Name: "openai", Configured: true}},
|
||||
Config: &clientpb.AIConfigSummary{
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
SystemPrompt: "Stay concise.",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
conversationsResp: &clientpb.AIConversations{},
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
SystemPrompt: "Stay concise.",
|
||||
},
|
||||
conversationByID: map[string]*clientpb.AIConversation{
|
||||
"conv-created": {
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "New conversation",
|
||||
SystemPrompt: "Stay concise.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := loadAIStateCmd(&console.SliverClient{Rpc: rpcClient}, aiTargetSummary{}, "")()
|
||||
|
||||
loaded, ok := msg.(aiLoadedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiLoadedMsg, got %T", msg)
|
||||
}
|
||||
if loaded.status != "Created a new AI conversation." {
|
||||
t.Fatalf("unexpected status: %q", loaded.status)
|
||||
}
|
||||
if loaded.selectedID != "conv-created" {
|
||||
t.Fatalf("unexpected selected conversation id: %q", loaded.selectedID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
request := server.saveConversationReqs[0]
|
||||
if request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetTitle() != "New conversation" {
|
||||
t.Fatalf("unexpected SaveAIConversation request: %+v", request)
|
||||
}
|
||||
if request.GetSystemPrompt() != "Stay concise." {
|
||||
t.Fatalf("unexpected SaveAIConversation system prompt: %q", request.GetSystemPrompt())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdCreatesConversationAndSavesUserMessage(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveConversationResp: &clientpb.AIConversation{
|
||||
ID: "conv-created",
|
||||
Provider: "openai",
|
||||
Model: "gpt-test",
|
||||
Title: "Explain the workflow.",
|
||||
},
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-created",
|
||||
Role: "user",
|
||||
Content: "Explain the workflow.\n\nWith details.",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{SessionID: "session-1"},
|
||||
nil,
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Stay concise.",
|
||||
"Explain the workflow.\n\nWith details.",
|
||||
)()
|
||||
|
||||
submitted, ok := msg.(aiPromptSubmittedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
if submitted.conversationID != "conv-created" {
|
||||
t.Fatalf("unexpected conversation id: %q", submitted.conversationID)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversation to be called once, got %d", len(server.saveConversationReqs))
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTitle() != "Explain the workflow." {
|
||||
t.Fatalf("unexpected conversation title: %q", server.saveConversationReqs[0].GetTitle())
|
||||
}
|
||||
if server.saveConversationReqs[0].GetTargetSessionID() != "session-1" {
|
||||
t.Fatalf("expected target session to be forwarded, got %+v", server.saveConversationReqs[0])
|
||||
}
|
||||
if server.saveConversationReqs[0].GetSystemPrompt() != "Stay concise." {
|
||||
t.Fatalf("unexpected conversation system prompt: %q", server.saveConversationReqs[0].GetSystemPrompt())
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetConversationID() != "conv-created" || request.GetProvider() != "openai" || request.GetModel() != "gpt-test" || request.GetRole() != "user" {
|
||||
t.Fatalf("unexpected SaveAIConversationMessage request: %+v", request)
|
||||
}
|
||||
if request.GetContent() != "Explain the workflow.\n\nWith details." {
|
||||
t.Fatalf("unexpected prompt content: %q", request.GetContent())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitPromptCmdUsesExistingConversationSettings(t *testing.T) {
|
||||
server := &aiRPCServer{
|
||||
saveMessageResp: &clientpb.AIConversationMessage{
|
||||
ID: "msg-1",
|
||||
ConversationID: "conv-1",
|
||||
Role: "user",
|
||||
Content: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := submitPromptCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
aiTargetSummary{},
|
||||
&clientpb.AIConversation{ID: "conv-1", Provider: "anthropic", Model: "claude-test", Title: "Thread"},
|
||||
"openai",
|
||||
"gpt-test",
|
||||
"Stay concise.",
|
||||
"hello",
|
||||
)()
|
||||
|
||||
if _, ok := msg.(aiPromptSubmittedMsg); !ok {
|
||||
t.Fatalf("expected aiPromptSubmittedMsg, got %T", msg)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.saveConversationReqs) != 0 {
|
||||
t.Fatalf("did not expect SaveAIConversation to be called, got %d calls", len(server.saveConversationReqs))
|
||||
}
|
||||
if len(server.saveMessageReqs) != 1 {
|
||||
t.Fatalf("expected SaveAIConversationMessage to be called once, got %d", len(server.saveMessageReqs))
|
||||
}
|
||||
request := server.saveMessageReqs[0]
|
||||
if request.GetProvider() != "anthropic" || request.GetModel() != "claude-test" {
|
||||
t.Fatalf("unexpected conversation settings in message request: %+v", request)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConversationCmdSendsDeleteRequest(t *testing.T) {
|
||||
server := &aiRPCServer{}
|
||||
|
||||
rpcClient, cleanup := newAITestRPCClient(t, server)
|
||||
defer cleanup()
|
||||
|
||||
msg := deleteConversationCmd(
|
||||
&console.SliverClient{Rpc: rpcClient},
|
||||
"conv-1",
|
||||
"conv-2",
|
||||
`Deleted "Thread".`,
|
||||
)()
|
||||
|
||||
deleted, ok := msg.(aiConversationDeletedMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationDeletedMsg, got %T", msg)
|
||||
}
|
||||
if deleted.conversationID != "conv-1" || deleted.selectedID != "conv-2" {
|
||||
t.Fatalf("unexpected delete message: %+v", deleted)
|
||||
}
|
||||
|
||||
server.mu.Lock()
|
||||
defer server.mu.Unlock()
|
||||
if len(server.deleteConversationReqs) != 1 {
|
||||
t.Fatalf("expected DeleteAIConversation to be called once, got %d", len(server.deleteConversationReqs))
|
||||
}
|
||||
if server.deleteConversationReqs[0].GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected DeleteAIConversation request: %+v", server.deleteConversationReqs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdFiltersForAIEvents(t *testing.T) {
|
||||
eventPayload := &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-1",
|
||||
OperatorName: "alice",
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(eventPayload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal conversation event: %v", err)
|
||||
}
|
||||
|
||||
listener := make(chan *clientpb.Event, 2)
|
||||
listener <- &clientpb.Event{EventType: consts.WatchtowerEvent}
|
||||
listener <- &clientpb.Event{EventType: consts.AIConversationEvent, Data: data}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
aiEvent, ok := msg.(aiConversationEventMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiConversationEventMsg, got %T", msg)
|
||||
}
|
||||
if aiEvent.event == nil || aiEvent.event.GetConversation() == nil || aiEvent.event.GetConversation().GetID() != "conv-1" {
|
||||
t.Fatalf("unexpected AI conversation event payload: %+v", aiEvent.event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsToastMessages(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event, 1)
|
||||
listener <- &clientpb.Event{
|
||||
EventType: consts.ClientToastEvent,
|
||||
Data: []byte("Session alpha connected"),
|
||||
Err: "info",
|
||||
}
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
|
||||
toast, ok := msg.(aiToastMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected aiToastMsg, got %T", msg)
|
||||
}
|
||||
if toast.level != "info" || toast.message != "Session alpha connected" {
|
||||
t.Fatalf("unexpected toast payload: %+v", toast)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForAIConversationEventCmdReturnsClosedMessage(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
msg := waitForAIConversationEventCmd(listener)()
|
||||
if _, ok := msg.(aiListenerClosedMsg); !ok {
|
||||
t.Fatalf("expected aiListenerClosedMsg, got %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesSharedConversationEventsWithoutReload(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.conversations = []*clientpb.AIConversation{{ID: "conv-1", Title: "First"}}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_UPDATED,
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "bob",
|
||||
Title: "Shared thread",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect shared conversation event to trigger a reload")
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") < 0 {
|
||||
t.Fatalf("expected shared conversation to be merged locally, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected shared conversation event to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIModelAppliesDeleteEventsWhileAwaitingResponse(t *testing.T) {
|
||||
listener := make(chan *clientpb.Event)
|
||||
close(listener)
|
||||
|
||||
model := newAIModel(nil, aiContext{
|
||||
connection: aiConnectionSummary{Operator: "alice"},
|
||||
}, listener)
|
||||
model.loading = false
|
||||
model.awaitingResponse = true
|
||||
model.currentConversation = &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
OperatorName: "alice",
|
||||
UpdatedAt: 100,
|
||||
TurnState: clientpb.AIConversationTurnState_AI_TURN_STATE_IN_PROGRESS,
|
||||
Messages: []*clientpb.AIConversationMessage{
|
||||
{Role: "user", Content: "still waiting"},
|
||||
},
|
||||
}
|
||||
model.conversations = []*clientpb.AIConversation{
|
||||
{ID: "conv-2", Title: "Current"},
|
||||
{ID: "conv-1", Title: "Fallback"},
|
||||
}
|
||||
|
||||
updated, cmd := model.Update(aiConversationEventMsg{event: &clientpb.AIConversationEvent{
|
||||
EventType: clientpb.AIConversationEventType_AI_CONVERSATION_EVENT_TYPE_CONVERSATION_DELETED,
|
||||
ConversationID: "conv-2",
|
||||
Conversation: &clientpb.AIConversation{
|
||||
ID: "conv-2",
|
||||
},
|
||||
}})
|
||||
|
||||
nextModel := updated.(*aiModel)
|
||||
if nextModel.loading {
|
||||
t.Fatal("did not expect delete tombstone to trigger a reload")
|
||||
}
|
||||
if nextModel.currentConversation != nil {
|
||||
t.Fatalf("expected deleted conversation to be cleared, got %+v", nextModel.currentConversation)
|
||||
}
|
||||
if conversationIndexByID(nextModel.conversations, "conv-2") >= 0 {
|
||||
t.Fatalf("expected deleted conversation to be removed, got %+v", nextModel.conversations)
|
||||
}
|
||||
if cmd == nil {
|
||||
t.Fatal("expected delete tombstone to continue listening for events")
|
||||
}
|
||||
}
|
||||
|
||||
type aiRPCServer struct {
|
||||
rpcpb.UnimplementedSliverRPCServer
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
providersResp *clientpb.AIProviderConfigs
|
||||
conversationsResp *clientpb.AIConversations
|
||||
conversationByID map[string]*clientpb.AIConversation
|
||||
saveConversationResp *clientpb.AIConversation
|
||||
saveMessageResp *clientpb.AIConversationMessage
|
||||
sessionsResp *clientpb.Sessions
|
||||
beaconsResp *clientpb.Beacons
|
||||
|
||||
getProvidersCalls int
|
||||
getConversationsCalls int
|
||||
getSessionsCalls int
|
||||
getBeaconsCalls int
|
||||
getConversationReqs []*clientpb.AIConversationReq
|
||||
deleteConversationReqs []*clientpb.AIConversationReq
|
||||
saveConversationReqs []*clientpb.AIConversation
|
||||
saveMessageReqs []*clientpb.AIConversationMessage
|
||||
getConversationStart chan struct{}
|
||||
getConversationWait <-chan struct{}
|
||||
saveMessageStarted chan struct{}
|
||||
saveMessageRelease <-chan struct{}
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIProviders(context.Context, *commonpb.Empty) (*clientpb.AIProviderConfigs, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getProvidersCalls++
|
||||
if s.providersResp == nil {
|
||||
return &clientpb.AIProviderConfigs{}, nil
|
||||
}
|
||||
return proto.Clone(s.providersResp).(*clientpb.AIProviderConfigs), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversations(context.Context, *commonpb.Empty) (*clientpb.AIConversations, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationsCalls++
|
||||
if s.conversationsResp == nil {
|
||||
return &clientpb.AIConversations{}, nil
|
||||
}
|
||||
return proto.Clone(s.conversationsResp).(*clientpb.AIConversations), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetSessions(context.Context, *commonpb.Empty) (*clientpb.Sessions, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getSessionsCalls++
|
||||
if s.sessionsResp == nil {
|
||||
return &clientpb.Sessions{}, nil
|
||||
}
|
||||
return proto.Clone(s.sessionsResp).(*clientpb.Sessions), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetBeacons(context.Context, *commonpb.Empty) (*clientpb.Beacons, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getBeaconsCalls++
|
||||
if s.beaconsResp == nil {
|
||||
return &clientpb.Beacons{}, nil
|
||||
}
|
||||
return proto.Clone(s.beaconsResp).(*clientpb.Beacons), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) GetAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.getConversationReqs = append(s.getConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
if s.getConversationStart != nil {
|
||||
select {
|
||||
case s.getConversationStart <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.getConversationWait != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.getConversationWait
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.conversationByID == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
conversation := s.conversationByID[req.GetID()]
|
||||
if conversation == nil {
|
||||
return &clientpb.AIConversation{}, nil
|
||||
}
|
||||
return proto.Clone(conversation).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversation(_ context.Context, req *clientpb.AIConversation) (*clientpb.AIConversation, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveConversationReqs = append(s.saveConversationReqs, proto.Clone(req).(*clientpb.AIConversation))
|
||||
if s.saveConversationResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversation), nil
|
||||
}
|
||||
return proto.Clone(s.saveConversationResp).(*clientpb.AIConversation), nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) DeleteAIConversation(_ context.Context, req *clientpb.AIConversationReq) (*commonpb.Empty, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.deleteConversationReqs = append(s.deleteConversationReqs, proto.Clone(req).(*clientpb.AIConversationReq))
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *aiRPCServer) SaveAIConversationMessage(_ context.Context, req *clientpb.AIConversationMessage) (*clientpb.AIConversationMessage, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.saveMessageReqs = append(s.saveMessageReqs, proto.Clone(req).(*clientpb.AIConversationMessage))
|
||||
if s.saveMessageStarted != nil {
|
||||
select {
|
||||
case s.saveMessageStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if s.saveMessageRelease != nil {
|
||||
s.mu.Unlock()
|
||||
<-s.saveMessageRelease
|
||||
s.mu.Lock()
|
||||
}
|
||||
if s.saveMessageResp == nil {
|
||||
return proto.Clone(req).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
return proto.Clone(s.saveMessageResp).(*clientpb.AIConversationMessage), nil
|
||||
}
|
||||
|
||||
func newAITestRPCClient(t *testing.T, srv rpcpb.SliverRPCServer) (rpcpb.SliverRPCClient, func()) {
|
||||
t.Helper()
|
||||
|
||||
listener := bufconn.Listen(1024 * 1024)
|
||||
grpcServer := grpc.NewServer()
|
||||
rpcpb.RegisterSliverRPCServer(grpcServer, srv)
|
||||
|
||||
go func() {
|
||||
_ = grpcServer.Serve(listener)
|
||||
}()
|
||||
|
||||
dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() }
|
||||
conn, err := grpc.DialContext(context.Background(), "bufnet",
|
||||
grpc.WithContextDialer(dialer),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
t.Fatalf("dial bufconn: %v", err)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
_ = conn.Close()
|
||||
grpcServer.Stop()
|
||||
_ = listener.Close()
|
||||
}
|
||||
|
||||
return rpcpb.NewSliverRPCClient(conn), cleanup
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,251 @@
|
||||
package alias
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Alias commands run with cobra's DisableFlagParsing enabled (see LoadAlias),
|
||||
// the same treatment extension commands received for their arguments. Before
|
||||
// that, an unknown flag such as the payload's own `-pid` was rejected outright
|
||||
// by pflag ("unknown flag"), and operators had to inject cobra's "--"
|
||||
// separator (documented on the wiki) to pass arguments through at all (#2264).
|
||||
//
|
||||
// With flag parsing disabled cobra hands Run the raw, unstripped slice, and we
|
||||
// extract the Sliver-owned flags ourselves here. Everything we do not own is
|
||||
// passed to the payload untouched, so all of these now work:
|
||||
//
|
||||
// amsi-bypass -pid 6969
|
||||
// fakealias --process notepad.exe arg1 arg2
|
||||
// fakealias -- --anything still-here (existing workaround, unchanged)
|
||||
type ownedFlagKind int
|
||||
|
||||
const (
|
||||
ownedBool ownedFlagKind = iota
|
||||
ownedString
|
||||
ownedUint32
|
||||
ownedInt64
|
||||
)
|
||||
|
||||
// ownedFlagSpec describes one Sliver-owned flag that splitAliasArgs extracts
|
||||
// from the raw argument slice before the rest is handed to the alias payload.
|
||||
type ownedFlagSpec struct {
|
||||
long string // canonical pflag name, e.g. "process-arguments"
|
||||
short string // shorthand letter, e.g. "A" ("" if the flag has none)
|
||||
kind ownedFlagKind // how the value is consumed and validated
|
||||
}
|
||||
|
||||
// baseAliasOwnedFlags are registered for every alias command.
|
||||
var baseAliasOwnedFlags = []ownedFlagSpec{
|
||||
{long: "process", short: "p", kind: ownedString},
|
||||
{long: "process-arguments", short: "A", kind: ownedString},
|
||||
{long: "ppid", short: "P", kind: ownedUint32},
|
||||
{long: "save", short: "s", kind: ownedBool},
|
||||
{long: "timeout", short: "t", kind: ownedInt64},
|
||||
}
|
||||
|
||||
// assemblyAliasOwnedFlags are only owned when the alias wraps a .NET assembly;
|
||||
// for every other alias they are unknown tokens and pass through to the payload.
|
||||
var assemblyAliasOwnedFlags = []ownedFlagSpec{
|
||||
{long: "method", short: "m", kind: ownedString},
|
||||
{long: "class", short: "c", kind: ownedString},
|
||||
{long: "app-domain", short: "d", kind: ownedString},
|
||||
{long: "arch", short: "a", kind: ownedString},
|
||||
{long: "in-process", short: "i", kind: ownedBool},
|
||||
{long: "runtime", short: "r", kind: ownedString},
|
||||
{long: "amsi-bypass", short: "M", kind: ownedBool},
|
||||
{long: "etw-bypass", short: "E", kind: ownedBool},
|
||||
}
|
||||
|
||||
// aliasOwnedFlagSpecs returns the flag set owned by a particular alias.
|
||||
func aliasOwnedFlagSpecs(isAssembly bool) []ownedFlagSpec {
|
||||
if !isAssembly {
|
||||
return baseAliasOwnedFlags
|
||||
}
|
||||
specs := make([]ownedFlagSpec, 0, len(baseAliasOwnedFlags)+len(assemblyAliasOwnedFlags))
|
||||
specs = append(specs, baseAliasOwnedFlags...)
|
||||
return append(specs, assemblyAliasOwnedFlags...)
|
||||
}
|
||||
|
||||
// ownedFlagMatch is a raw token that spelled one of the Sliver-owned flags.
|
||||
type ownedFlagMatch struct {
|
||||
spec ownedFlagSpec
|
||||
value string // value from a "--flag=value" / "-f=value" spelling
|
||||
inline bool // whether the token carried its value inline
|
||||
}
|
||||
|
||||
// splitAliasArgs separates the Sliver-owned flags from the alias payload's own
|
||||
// arguments in a raw (DisableFlagParsing) argument slice. It accepts the long
|
||||
// forms --flag, --flag=value, --flag value and the shorthand forms -f, -f=value,
|
||||
// -f value, mirroring pflag. Two deliberate differences from pflag:
|
||||
//
|
||||
// - a glued shorthand value (-pnotepad) does NOT match --process: tokens
|
||||
// like the payload's own "-pid" must survive intact (#2264);
|
||||
// - after a literal "--" every token is copied through verbatim, preserving
|
||||
// the long-standing separator workaround.
|
||||
//
|
||||
// The returned map holds the raw string value of each flag that was provided;
|
||||
// callers re-publish the values with applyOwnedAliasFlags.
|
||||
func splitAliasArgs(raw []string, specs []ownedFlagSpec) (map[string]string, bool, []string, error) {
|
||||
values := make(map[string]string)
|
||||
extArgs := make([]string, 0, len(raw))
|
||||
byLong, byShort := ownedFlagTables(specs)
|
||||
|
||||
passthrough := false
|
||||
for i := 0; i < len(raw); i++ {
|
||||
tok := raw[i]
|
||||
|
||||
// Once we have seen "--", copy everything through untouched.
|
||||
if passthrough {
|
||||
extArgs = append(extArgs, tok)
|
||||
continue
|
||||
}
|
||||
if tok == "--" {
|
||||
passthrough = true
|
||||
continue
|
||||
}
|
||||
if tok == "--help" || tok == "-h" {
|
||||
return values, true, extArgs, nil
|
||||
}
|
||||
|
||||
match, ok := matchOwnedFlag(tok, byLong, byShort)
|
||||
if !ok {
|
||||
// Not one of ours (including glued shorthands and the payload's
|
||||
// own flags): the alias owns the token, pass it through untouched.
|
||||
extArgs = append(extArgs, tok)
|
||||
continue
|
||||
}
|
||||
i = takeOwnedValue(match, raw, i, values)
|
||||
}
|
||||
|
||||
if err := validateOwnedValues(specs, values); err != nil {
|
||||
return nil, false, nil, err
|
||||
}
|
||||
return values, false, extArgs, nil
|
||||
}
|
||||
|
||||
// ownedFlagTables indexes the specs by long name and shorthand.
|
||||
func ownedFlagTables(specs []ownedFlagSpec) (byLong, byShort map[string]ownedFlagSpec) {
|
||||
byLong = make(map[string]ownedFlagSpec, len(specs))
|
||||
byShort = make(map[string]ownedFlagSpec, len(specs))
|
||||
for _, spec := range specs {
|
||||
byLong[spec.long] = spec
|
||||
if spec.short != "" {
|
||||
byShort[spec.short] = spec
|
||||
}
|
||||
}
|
||||
return byLong, byShort
|
||||
}
|
||||
|
||||
// matchOwnedFlag checks whether tok spells one of the Sliver-owned flags, in
|
||||
// the long form (--name, --name=value) or shorthand form (-f, -f=value). A
|
||||
// glued shorthand value (-pnotepad) deliberately does not match: pflag would
|
||||
// read "-pid" as -p with value "id", which is exactly the mangling that broke
|
||||
// #2264, so such tokens must fall through to the payload.
|
||||
func matchOwnedFlag(tok string, byLong, byShort map[string]ownedFlagSpec) (ownedFlagMatch, bool) {
|
||||
var match ownedFlagMatch
|
||||
var name, value string
|
||||
var hasValue, isShort bool
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(tok, "--"):
|
||||
name, value, hasValue = strings.Cut(tok[2:], "=")
|
||||
case strings.HasPrefix(tok, "-") && len(tok) > 1:
|
||||
name, value, hasValue = strings.Cut(tok[1:], "=")
|
||||
isShort = len(name) == 1
|
||||
default:
|
||||
return match, false
|
||||
}
|
||||
|
||||
spec, ok := byLong[name]
|
||||
if isShort {
|
||||
spec, ok = byShort[name]
|
||||
}
|
||||
if !ok {
|
||||
return match, false
|
||||
}
|
||||
match.spec, match.value, match.inline = spec, value, hasValue
|
||||
return match, true
|
||||
}
|
||||
|
||||
// takeOwnedValue records the flag's value in values and returns the index of
|
||||
// the last raw token it consumed. The "--flag value" / "-f value" spellings
|
||||
// consume the following token; a bool without an inline value defaults to
|
||||
// true; a value flag as the very last token is dropped and keeps its default,
|
||||
// mirroring the extension behaviour.
|
||||
func takeOwnedValue(match ownedFlagMatch, raw []string, i int, values map[string]string) int {
|
||||
switch {
|
||||
case match.spec.kind == ownedBool:
|
||||
if !match.inline {
|
||||
match.value = "true"
|
||||
}
|
||||
values[match.spec.long] = match.value
|
||||
case match.inline:
|
||||
values[match.spec.long] = match.value
|
||||
case i+1 < len(raw):
|
||||
values[match.spec.long] = raw[i+1]
|
||||
return i + 1
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// validateOwnedValues rejects non-numeric values for the numeric flag kinds,
|
||||
// the way pflag's typed values would have.
|
||||
func validateOwnedValues(specs []ownedFlagSpec, values map[string]string) error {
|
||||
for _, spec := range specs {
|
||||
value, provided := values[spec.long]
|
||||
if !provided {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
switch spec.kind {
|
||||
case ownedUint32:
|
||||
_, err = strconv.ParseUint(value, 10, 32)
|
||||
case ownedInt64:
|
||||
_, err = strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value %q for --%s", value, spec.long)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyOwnedAliasFlags republishes the manually parsed Sliver-owned flag values
|
||||
// onto the command's flag set. DisableFlagParsing prevents pflag from
|
||||
// populating them, so readers such as runAliasCommand's Get* calls and
|
||||
// ActiveTarget.Request (which derives the gRPC timeout from --timeout) would
|
||||
// otherwise observe only the defaults.
|
||||
func applyOwnedAliasFlags(cmd *cobra.Command, values map[string]string) error {
|
||||
for name, value := range values {
|
||||
flag := cmd.Flags().Lookup(name)
|
||||
if flag == nil {
|
||||
continue // e.g. an assembly flag on a non-assembly alias
|
||||
}
|
||||
if err := flag.Value.Set(value); err != nil {
|
||||
return fmt.Errorf("invalid value %q for --%s: %w", value, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package alias
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2021 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// TestSplitAliasArgs covers the argument passthrough regression from #2264:
|
||||
// the alias payload's own flags must reach the payload intact, while the
|
||||
// Sliver-owned flags are extracted and validated.
|
||||
func TestSplitAliasArgs(t *testing.T) {
|
||||
base := aliasOwnedFlagSpecs(false)
|
||||
assembly := aliasOwnedFlagSpecs(true)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
specs []ownedFlagSpec
|
||||
in []string
|
||||
values map[string]string
|
||||
help bool
|
||||
extArgs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "payload flag without -- separator (#2264 regression)",
|
||||
specs: base,
|
||||
in: []string{"-pid", "6969"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"-pid", "6969"},
|
||||
},
|
||||
{
|
||||
name: "glued shorthand is not ours and passes through",
|
||||
specs: base,
|
||||
in: []string{"-pid"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"-pid"},
|
||||
},
|
||||
{
|
||||
name: "payload flag with -- separator (existing workaround)",
|
||||
specs: base,
|
||||
in: []string{"--", "--pid", "6969"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--pid", "6969"},
|
||||
},
|
||||
{
|
||||
name: "owned flag after -- belongs to the payload",
|
||||
specs: base,
|
||||
in: []string{"--", "--save"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--save"},
|
||||
},
|
||||
{
|
||||
name: "long flag with separate value, then payload args",
|
||||
specs: base,
|
||||
in: []string{"--process", "notepad.exe", "arg1", "arg2"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1", "arg2"},
|
||||
},
|
||||
{
|
||||
name: "long flag with equals value",
|
||||
specs: base,
|
||||
in: []string{"--process=notepad.exe", "arg1"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "shorthand with separate value",
|
||||
specs: base,
|
||||
in: []string{"-p", "notepad.exe", "arg1"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "shorthand with equals value",
|
||||
specs: base,
|
||||
in: []string{"-p=notepad.exe"},
|
||||
values: map[string]string{"process": "notepad.exe"},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "bool shorthand and long false form",
|
||||
specs: base,
|
||||
in: []string{"-s", "--save=false", "arg1"},
|
||||
values: map[string]string{"save": "false"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "quoted value containing spaces stays a single argument",
|
||||
specs: base,
|
||||
in: []string{"-A", "-kalc -kdisable", "arg1"},
|
||||
values: map[string]string{"process-arguments": "-kalc -kdisable"},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "valid uint32 and int64 values",
|
||||
specs: base,
|
||||
in: []string{"-P", "1234", "--timeout=90"},
|
||||
values: map[string]string{"ppid": "1234", "timeout": "90"},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "invalid uint32 value errors",
|
||||
specs: base,
|
||||
in: []string{"--ppid", "notanumber"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid int64 value errors",
|
||||
specs: base,
|
||||
in: []string{"--timeout=x"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "assembly flags are owned for assembly aliases",
|
||||
specs: assembly,
|
||||
in: []string{"--method", "Run", "--class", "Prog", "-i", "-M", "--arch", "x64", "payloadArg"},
|
||||
values: map[string]string{
|
||||
"method": "Run", "class": "Prog", "in-process": "true",
|
||||
"amsi-bypass": "true", "arch": "x64",
|
||||
},
|
||||
extArgs: []string{"payloadArg"},
|
||||
},
|
||||
{
|
||||
name: "assembly flags pass through for non-assembly aliases",
|
||||
specs: base,
|
||||
in: []string{"--method", "Run"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"--method", "Run"},
|
||||
},
|
||||
{
|
||||
name: "help flags are detected",
|
||||
specs: base,
|
||||
in: []string{"--help"},
|
||||
help: true,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "short help is detected",
|
||||
specs: base,
|
||||
in: []string{"-h"},
|
||||
help: true,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "value flag as last token keeps its default",
|
||||
specs: base,
|
||||
in: []string{"arg1", "--process"},
|
||||
values: map[string]string{},
|
||||
extArgs: []string{"arg1"},
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
specs: base,
|
||||
in: nil,
|
||||
values: map[string]string{},
|
||||
extArgs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
values, help, extArgs, err := splitAliasArgs(tc.in, tc.specs)
|
||||
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got values=%#v help=%v extArgs=%#v", values, help, extArgs)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if help != tc.help {
|
||||
t.Errorf("help: got %v, want %v", help, tc.help)
|
||||
}
|
||||
if !reflect.DeepEqual(values, tc.values) {
|
||||
t.Errorf("values: got %#v, want %#v", values, tc.values)
|
||||
}
|
||||
if !reflect.DeepEqual(extArgs, tc.extArgs) {
|
||||
t.Errorf("extArgs: got %#v, want %#v", extArgs, tc.extArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyOwnedAliasFlags checks that manually parsed values are re-published
|
||||
// on the command's flag set for every value type an alias can own.
|
||||
func TestApplyOwnedAliasFlags(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
f := pflag.NewFlagSet("alias-test", pflag.ContinueOnError)
|
||||
f.StringP("process", "p", "", "host process")
|
||||
f.StringP("process-arguments", "A", "", "host process arguments")
|
||||
f.Uint32P("ppid", "P", 0, "parent pid")
|
||||
f.BoolP("save", "s", false, "save output")
|
||||
f.Int64P("timeout", "t", 60, "timeout")
|
||||
f.BoolP("amsi-bypass", "M", false, "amsi bypass")
|
||||
cmd.Flags().AddFlagSet(f)
|
||||
|
||||
err := applyOwnedAliasFlags(cmd, map[string]string{
|
||||
"process": "notepad.exe",
|
||||
"process-arguments": "-k foo",
|
||||
"ppid": "1234",
|
||||
"save": "true",
|
||||
"timeout": "90",
|
||||
"amsi-bypass": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
for name, want := range map[string]interface{}{
|
||||
"process": "notepad.exe", "process-arguments": "-k foo",
|
||||
} {
|
||||
got, _ := cmd.Flags().GetString(name)
|
||||
if got != want.(string) {
|
||||
t.Errorf("%s: got %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
if ppid, _ := cmd.Flags().GetUint32("ppid"); ppid != 1234 {
|
||||
t.Errorf("ppid: got %d, want 1234", ppid)
|
||||
}
|
||||
if save, _ := cmd.Flags().GetBool("save"); !save {
|
||||
t.Error("save: got false, want true")
|
||||
}
|
||||
if timeout, _ := cmd.Flags().GetInt64("timeout"); timeout != 90 {
|
||||
t.Errorf("timeout: got %d, want 90", timeout)
|
||||
}
|
||||
if amsi, _ := cmd.Flags().GetBool("amsi-bypass"); !amsi {
|
||||
t.Error("amsi-bypass: got false, want true")
|
||||
}
|
||||
|
||||
// Unset flags keep their defaults.
|
||||
if inProc := cmd.Flags().Lookup("in-process"); inProc != nil {
|
||||
t.Error("in-process should not be registered on this command")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package alias
|
||||
|
||||
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]",
|
||||
|
||||
@@ -215,8 +215,6 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
longHelp.WriteString(fmt.Sprintf("%s (%s):\t%s%s", strings.ToUpper(arg.Name), aType, optStr, arg.Desc))
|
||||
}
|
||||
}
|
||||
longHelp.WriteString("\n\n⚠️ If you're having issues passing arguments to the alias please read:\n")
|
||||
longHelp.WriteString("https://github.com/BishopFox/sliver/wiki/Aliases-&-Extensions#aliases-command-parsing")
|
||||
|
||||
// for each alias command, add a new app command
|
||||
helpMsg := fmt.Sprintf("[%s] %s", aliasManifest.Name, aliasManifest.Help)
|
||||
@@ -227,9 +225,14 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runAliasCommand(cmd, con, args)
|
||||
},
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
Args: cobra.ArbitraryArgs, // a.StringList("arguments", "arguments", grumble.Default([]string{}))
|
||||
// DisableFlagParsing keeps the alias payload's own flags (which are
|
||||
// unknown to pflag) from being rejected or swallowed; we split the
|
||||
// Sliver-owned flags out of the raw slice ourselves in
|
||||
// runAliasCommand (#2264, same treatment as extensions).
|
||||
DisableFlagParsing: true,
|
||||
GroupID: consts.AliasHelpGroup,
|
||||
Annotations: makeAliasPlatformFilters(aliasManifest),
|
||||
}
|
||||
|
||||
if aliasManifest.IsAssembly {
|
||||
@@ -239,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)
|
||||
}
|
||||
|
||||
@@ -250,7 +253,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
|
||||
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
|
||||
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
|
||||
f.BoolP("save", "s", false, "Save output to disk")
|
||||
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
|
||||
addAliasCmd.Flags().AddFlagSet(f)
|
||||
|
||||
// Setup completions for alias arguments
|
||||
@@ -333,6 +336,27 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
return
|
||||
}
|
||||
aliasManifest := loadedAlias.Manifest
|
||||
|
||||
// The command runs with DisableFlagParsing, so args is the raw, unstripped
|
||||
// slice. Separate the Sliver-owned flags from the payload's own arguments
|
||||
// and re-publish them on the flag set (so the Get* readers below and
|
||||
// Request()'s --timeout still work), then hand only the payload arguments
|
||||
// to the execution paths (#2264, same treatment as extensions in #2323).
|
||||
ownedValues, helpRequested, payloadArgs, err := splitAliasArgs(args, aliasOwnedFlagSpecs(aliasManifest.IsAssembly))
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if helpRequested {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
if err := applyOwnedAliasFlags(cmd, ownedValues); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
args = payloadArgs
|
||||
|
||||
binPath, err := aliasManifest.getFileForTarget(cmd.Name(), goos, goarch)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Fail to find alias file: %s\n", err)
|
||||
@@ -374,7 +398,7 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
con.PrintWarnf("%s", msgStr)
|
||||
confirm := false
|
||||
_ = forms.Confirm("Do you want to continue?", &confirm)
|
||||
forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -400,7 +424,13 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
|
||||
var outFilePath *os.File
|
||||
if save, _ := cmd.Flags().GetBool("save"); save {
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(session.GetHostname())))
|
||||
hostname := ""
|
||||
if session != nil {
|
||||
hostname = session.GetHostname()
|
||||
} else if beacon != nil {
|
||||
hostname = beacon.GetHostname()
|
||||
}
|
||||
outFile := filepath.Base(fmt.Sprintf("%s_%s*.log", filepath.Base(cmd.Name()), filepath.Base(hostname)))
|
||||
outFilePath, err = os.CreateTemp("", outFile)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
|
||||
@@ -164,7 +164,7 @@ func ArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok {
|
||||
// Something is wrong with this entry
|
||||
pkgCache.Delete(value)
|
||||
pkgCache.Delete(key)
|
||||
return true
|
||||
}
|
||||
if cacheEntry.ArmoryConfig.PublicKey != index.ArmoryConfig.PublicKey {
|
||||
@@ -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"
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
|
||||
@@ -301,32 +301,44 @@ func getCommandsInCache(armoryPK string) []string {
|
||||
return commandNames
|
||||
}
|
||||
|
||||
func packageMatchesInstallName(cacheEntry pkgCacheEntry, name string) bool {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
return cacheEntry.Alias != nil && cacheEntry.Alias.CommandName == name
|
||||
}
|
||||
if cacheEntry.Extension == nil {
|
||||
return false
|
||||
}
|
||||
if cacheEntry.Pkg.CommandName == name || cacheEntry.Extension.PackageName == name {
|
||||
return true
|
||||
}
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCacheEntry {
|
||||
packages := []*pkgCacheEntry{}
|
||||
|
||||
pkgCache.Range(func(key, value interface{}) bool {
|
||||
cacheEntry := value.(pkgCacheEntry)
|
||||
if cacheEntry.LastErr == nil {
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
if cacheEntry.Alias.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Alias.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, command := range cacheEntry.Extension.ExtCommand {
|
||||
if command.CommandName == name {
|
||||
if minimumVersion == "" || (minimumVersion != "" && cacheEntry.Extension.Version >= minimumVersion) {
|
||||
if armoryPK == "" || (armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey == armoryPK) {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cacheEntry.LastErr != nil || !packageMatchesInstallName(cacheEntry, name) {
|
||||
return true
|
||||
}
|
||||
if armoryPK != "" && cacheEntry.ArmoryConfig.PublicKey != armoryPK {
|
||||
return true
|
||||
}
|
||||
|
||||
packageVersion := ""
|
||||
if cacheEntry.Pkg.IsAlias {
|
||||
packageVersion = cacheEntry.Alias.Version
|
||||
} else {
|
||||
packageVersion = cacheEntry.Extension.Version
|
||||
}
|
||||
if minimumVersion == "" || packageVersion >= minimumVersion {
|
||||
packages = append(packages, &cacheEntry)
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -336,7 +348,7 @@ func getPackagesWithCommandName(name, armoryPK, minimumVersion string) []*pkgCac
|
||||
|
||||
func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
selectedPackageKey := ""
|
||||
optionKeys := util.Keys(options)
|
||||
optionKeys := slices.Collect(maps.Keys(options))
|
||||
slices.Sort(optionKeys)
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
@@ -635,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")
|
||||
}
|
||||
@@ -729,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"
|
||||
|
||||
@@ -21,7 +21,9 @@ package armory
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -33,13 +35,13 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
type VersionInformation struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
ArmoryName string
|
||||
PackageID string
|
||||
}
|
||||
|
||||
type PackageType uint
|
||||
@@ -124,9 +126,9 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
updatedPackage, err := getPackageForCommand(update.Name, armoryPK, extVersionInfo.NewVersion)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, err)
|
||||
updatedPackage := packageCacheLookupByID(extVersionInfo.PackageID)
|
||||
if updatedPackage == nil {
|
||||
con.PrintErrorf("Could not get update package for extension %s: %s\n", update.Name, ErrPackageNotFound)
|
||||
continue
|
||||
}
|
||||
err = installExtensionPackage(updatedPackage, false, clientConfig, con)
|
||||
@@ -173,8 +175,21 @@ func checkForAliasUpdates(armoryPK string) map[string]VersionInformation {
|
||||
return results
|
||||
}
|
||||
|
||||
func extensionManifestsMatch(localManifest, latestManifest *extensions.ExtensionManifest) bool {
|
||||
if localManifest.PackageName != "" {
|
||||
return latestManifest.PackageName != "" && localManifest.PackageName == latestManifest.PackageName
|
||||
}
|
||||
for _, localCommand := range localManifest.ExtCommand {
|
||||
for _, latestCommand := range latestManifest.ExtCommand {
|
||||
if localCommand.CommandName == latestCommand.CommandName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
_, cachedExtensions := packageManifestsInCache()
|
||||
// Return a map of extension name to minimum version to install
|
||||
results := make(map[string]VersionInformation)
|
||||
for _, extManifestPath := range assets.GetInstalledExtensionManifests() {
|
||||
@@ -186,7 +201,12 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, latestExt := range cachedExtensions {
|
||||
pkgCache.Range(func(_, value interface{}) bool {
|
||||
cacheEntry, ok := value.(pkgCacheEntry)
|
||||
if !ok || cacheEntry.LastErr != nil || cacheEntry.Pkg.IsAlias || cacheEntry.Extension == nil {
|
||||
return true
|
||||
}
|
||||
latestExt := cacheEntry.Extension
|
||||
/*
|
||||
We used to check if the version identifiers were different between the installed version and the
|
||||
version in the armory. This worked when we had one armory, but with multiple potential armories,
|
||||
@@ -194,16 +214,22 @@ func checkForExtensionUpdates(armoryPK string) map[string]VersionInformation {
|
||||
so we will rely on the author of the package incrementing the version identifier somehow to determine
|
||||
if a package is newer.
|
||||
*/
|
||||
if latestExt.Name == localManifest.Name && latestExt.Version > localManifest.Version {
|
||||
if extensionManifestsMatch(localManifest, latestExt) && latestExt.Version > localManifest.Version {
|
||||
if latestExt.ArmoryPK == armoryPK || armoryPK == "" {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
current, found := results[localManifest.Name]
|
||||
if !found || latestExt.Version > current.NewVersion ||
|
||||
(latestExt.Version == current.NewVersion && cacheEntry.ID < current.PackageID) {
|
||||
results[localManifest.Name] = VersionInformation{
|
||||
OldVersion: localManifest.Version,
|
||||
NewVersion: latestExt.Version,
|
||||
ArmoryName: latestExt.ArmoryName,
|
||||
PackageID: cacheEntry.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -219,8 +245,8 @@ func sortUpdateIdentifiers(aliasUpdates, extensionUpdates map[string]VersionInfo
|
||||
|
||||
result := []UpdateIdentifier{}
|
||||
|
||||
aliasNames := util.Keys(aliasUpdates)
|
||||
extensionNames := util.Keys(extensionUpdates)
|
||||
aliasNames := slices.Collect(maps.Keys(aliasUpdates))
|
||||
extensionNames := slices.Collect(maps.Keys(extensionUpdates))
|
||||
for _, name := range aliasNames {
|
||||
result = append(result, UpdateIdentifier{
|
||||
Type: AliasPackage,
|
||||
@@ -257,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",
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/kill"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/command/targettable"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -121,50 +122,30 @@ func renderBeacons(beacons []*clientpb.Beacon, filter string, filterRegex *regex
|
||||
if err != nil {
|
||||
width = 999
|
||||
}
|
||||
return renderBeaconsAtWidth(beacons, filter, filterRegex, con, width, time.Now())
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
wideTermWidth := con.Settings.SmallTermWidth < width
|
||||
// renderBeaconsAtWidth uses one reference time for all candidate layouts,
|
||||
// preventing a second boundary from changing the table width while it is fitted.
|
||||
func renderBeaconsAtWidth(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, width int, now time.Time) table.Writer {
|
||||
windowsBeaconInList := false
|
||||
for _, beacon := range beacons {
|
||||
if beacon.OS == "windows" {
|
||||
windowsBeaconInList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if wideTermWidth {
|
||||
if windowsBeaconInList {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Integrity",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
} else {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
"Tasks",
|
||||
"Transport",
|
||||
"Remote Address",
|
||||
"Hostname",
|
||||
"Username",
|
||||
"Process (PID)",
|
||||
"Operating System",
|
||||
"Locale",
|
||||
"Last Check-in",
|
||||
"Next Check-in",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
return targettable.Fit(width, windowsBeaconInList, func(layout targettable.Layout) table.Writer {
|
||||
return buildBeaconTable(beacons, filter, filterRegex, con, layout, windowsBeaconInList, now)
|
||||
})
|
||||
}
|
||||
|
||||
func buildBeaconTable(beacons []*clientpb.Beacon, filter string, filterRegex *regexp.Regexp, con *console.SliverClient, layout targettable.Layout, windowsBeaconInList bool, now time.Time) table.Writer {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
includeIntegrity := windowsBeaconInList && layout.IncludeIntegrity
|
||||
if layout.Compact {
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
"Name",
|
||||
@@ -175,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,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
}
|
||||
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("kill", "k", "", "kill the designated beacon")
|
||||
@@ -145,8 +145,8 @@ func filterCompletionValues(values []string, prefix string) []string {
|
||||
filtered := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
candidate := value
|
||||
if tab := strings.IndexByte(value, '\t'); tab >= 0 {
|
||||
candidate = value[:tab]
|
||||
if before, _, ok := strings.Cut(value, "\t"); ok {
|
||||
candidate = before
|
||||
}
|
||||
if strings.HasPrefix(candidate, prefix) {
|
||||
filtered = append(filtered, value)
|
||||
|
||||
@@ -28,11 +28,11 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,5 +6,7 @@ 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as crack files.
|
||||
Implements the 'crack' command group for the Sliver client console. Handlers map Cobra invocations to crack workflows such as cached benchmark inspection, durable jobs, real-time monitoring, and crack files.
|
||||
|
||||
## Go Files
|
||||
|
||||
- `commands.go` – Defines the crack command hierarchy, attaching operations for stations and file management.
|
||||
- `crack-benchmarks.go` – Displays bounded cached crackstation benchmark summaries, freshness metadata, and representative or complete hash-mode rates.
|
||||
- `crack-files.go` – Manages cracking asset uploads, listings, and removals, handling streaming transfers and chunking.
|
||||
- `crack-jobs.go` – Lists durable cracking jobs and renders detailed task progress, device telemetry, and recovered credentials.
|
||||
- `crack-top.go` – Runs the full-screen Bubble Tea monitor, including responsive job/worker panes and a Huh-backed status filter.
|
||||
- `crack-top-loader.go` – Loads one lean, target-free snapshot containing every active job, bounded recent history, and connected crackstation telemetry.
|
||||
- `crack-top-telemetry.go` – Calculates weighted shard progress plus per-worker and global live hash rates without treating cached benchmarks as current throughput.
|
||||
- `crack.go` – Implements station status reporting, health checks, and list rendering for distributed cracking nodes.
|
||||
- `helpers.go` – Supplies completion helpers and byte formatting utilities for crack subcommands.
|
||||
@@ -1,6 +1,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,67 @@ 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")
|
||||
f.BoolP("watch", "w", false, "watch until the job reaches a terminal state")
|
||||
f.Duration("poll-interval", time.Second, "job status polling interval")
|
||||
})
|
||||
carapace.Gen(crackJobCmd).PositionalCompletion(CrackJobIDCompleter(con).Usage("crack job ID (leave empty to select)"))
|
||||
registerCrackJobIDCompletion(crackJobCmd, con)
|
||||
crackCmd.AddCommand(crackJobCmd)
|
||||
|
||||
wordlistsCmd := &cobra.Command{
|
||||
Use: consts.WordlistsStr,
|
||||
Short: "Manage wordlists",
|
||||
@@ -57,6 +127,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 +162,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 +198,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
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func newCrackDispatchCommand(t *testing.T, flags ...string) *cobra.Command {
|
||||
t.Helper()
|
||||
cmd := &cobra.Command{Use: "crack-test"}
|
||||
cmd.SetContext(context.Background())
|
||||
cmd.Flags().Int64("timeout", 0, "test timeout")
|
||||
bindCrackFlags(cmd.Flags())
|
||||
if err := cmd.Flags().Parse(flags); err != nil {
|
||||
t.Fatalf("parse flags: %v", err)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestClassifyCrackInvocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
args []string
|
||||
wantMode crackInvocationMode
|
||||
wantBackendLevel uint32
|
||||
wantHashLevel uint32
|
||||
wantCrackstation string
|
||||
wantError string
|
||||
}{
|
||||
{name: "dashboard", wantMode: crackInvocationDashboard},
|
||||
{name: "timeout only", flags: []string{"--timeout=5"}, wantMode: crackInvocationDashboard},
|
||||
{name: "positional hash job", args: []string{"hash"}, wantMode: crackInvocationJob},
|
||||
{name: "job flag", flags: []string{"--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "explicit false is a no-op", flags: []string{"--backend-info=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "explicit false with job flag", flags: []string{"--backend-info=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "keyspace false is a no-op", flags: []string{"--keyspace=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "keyspace false with job flag", flags: []string{"--keyspace=false", "--attack-mode=3"}, wantMode: crackInvocationJob},
|
||||
{name: "keyspace", flags: []string{"--keyspace"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace candidate flags", flags: []string{"--keyspace", "--attack-mode=3", "--input=?d?d", "--increment"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace selected station", flags: []string{"--keyspace", "--crackstation= station-one "}, wantMode: crackInvocationKeyspace, wantCrackstation: "station-one"},
|
||||
{name: "keyspace timeout", flags: []string{"--keyspace", "--timeout=5"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace skip conflict", flags: []string{"--keyspace", "--skip=1"}, wantError: "--skip"},
|
||||
{name: "keyspace zero skip no-op", flags: []string{"--keyspace", "--skip=0"}, wantMode: crackInvocationKeyspace},
|
||||
{name: "keyspace positional hash conflict", flags: []string{"--keyspace"}, args: []string{"hash"}, wantError: "positional hash arguments"},
|
||||
{name: "keyspace hash flag conflict", flags: []string{"--keyspace", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "keyspace credential conflict", flags: []string{"--keyspace", "--credential=deadbeef"}, wantError: "--credential"},
|
||||
{name: "keyspace collection conflict", flags: []string{"--keyspace", "--credential-collection=secrets"}, wantError: "--credential-collection"},
|
||||
{name: "keyspace include cracked conflict", flags: []string{"--keyspace", "--include-cracked-credentials"}, wantError: "--include-cracked-credentials"},
|
||||
{name: "keyspace and backend info conflict", flags: []string{"--keyspace", "--backend-info"}, wantError: "--keyspace"},
|
||||
{name: "backend info ignores disabled keyspace", flags: []string{"--backend-info", "--keyspace=false"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "backend info", flags: []string{"--backend-info"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "backend info and timeout", flags: []string{"--backend-info", "--timeout=5"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "level one", flags: []string{"--backend-info-level=1"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 1},
|
||||
{name: "level two", flags: []string{"--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "explicit level wins", flags: []string{"--backend-info", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "level independently enables mode", flags: []string{"--backend-info=false", "--backend-info-level=2"}, wantMode: crackInvocationBackendInfo, wantBackendLevel: 2},
|
||||
{name: "zero level", flags: []string{"--backend-info-level=0"}, wantError: "must be 1 or 2"},
|
||||
{name: "unsupported level", flags: []string{"--backend-info-level=3"}, wantError: "must be 1 or 2"},
|
||||
{name: "positional conflict", flags: []string{"--backend-info"}, args: []string{"hash"}, wantError: "positional hash arguments"},
|
||||
{name: "job flag conflict", flags: []string{"--backend-info", "--benchmark"}, wantError: "--benchmark"},
|
||||
{name: "input conflict", flags: []string{"--backend-info-level=2", "--input=wordlist"}, wantError: "--input"},
|
||||
{name: "backend info station conflict", flags: []string{"--backend-info", "--crackstation=station-one"}, wantError: "--crackstation"},
|
||||
{name: "total candidates", flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationTotalCandidates},
|
||||
{name: "total candidates false", flags: []string{"--total-candidates=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "total candidates hash conflict", flags: []string{"--total-candidates", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "total candidates limit conflict", flags: []string{"--total-candidates", "--limit=1"}, wantError: "--limit"},
|
||||
{name: "lookup", flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"}, wantMode: crackInvocationLookup},
|
||||
{name: "lookup allows window", flags: []string{"--lookup=candidate", "--skip=1", "--limit=2"}, wantMode: crackInvocationLookup},
|
||||
{name: "lookup empty", flags: []string{"--lookup="}, wantError: "cannot be empty"},
|
||||
{name: "query modes conflict", flags: []string{"--keyspace", "--total-candidates"}, wantError: "cannot be combined"},
|
||||
{name: "candidate query terminal conflict", flags: []string{"--keyspace", "--show"}, wantError: "--show"},
|
||||
{name: "identify positional", flags: []string{"--identify-mode"}, args: []string{"digest"}, wantMode: crackInvocationIdentify},
|
||||
{name: "identify hash flag", flags: []string{"--identify-mode", "--hash=digest"}, wantMode: crackInvocationIdentify},
|
||||
{name: "identify false", flags: []string{"--identify-mode=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "identify missing hash", flags: []string{"--identify-mode"}, wantError: "requires a positional hash or --hash"},
|
||||
{name: "identify credential conflict", flags: []string{"--identify-mode", "--credential=deadbeef"}, wantError: "--credential"},
|
||||
{name: "identify hash mode conflict", flags: []string{"--identify-mode", "--hash=digest", "--hash-mode=0"}, wantError: "--hash-mode"},
|
||||
{name: "hash info", flags: []string{"--hash-info"}, wantMode: crackInvocationHashInfo, wantHashLevel: 1},
|
||||
{name: "hash info false", flags: []string{"--hash-info=false"}, wantMode: crackInvocationDashboard},
|
||||
{name: "hash info level", flags: []string{"--hash-info=false", "--hash-info-level=2", "--hash-mode=0", "--crackstation=station-one"}, wantMode: crackInvocationHashInfo, wantHashLevel: 2, wantCrackstation: "station-one"},
|
||||
{name: "hash info zero level", flags: []string{"--hash-info-level=0"}, wantError: "must be 1 or 2"},
|
||||
{name: "hash info unsupported level", flags: []string{"--hash-info-level=3"}, wantError: "must be 1 or 2"},
|
||||
{name: "hash info hash conflict", flags: []string{"--hash-info", "--hash=digest"}, wantError: "--hash"},
|
||||
{name: "crackstation requires query", flags: []string{"--crackstation=station-one"}, wantError: "requires --keyspace"},
|
||||
{name: "crackstation cannot be empty", flags: []string{"--keyspace", "--crackstation= "}, wantError: "cannot be empty"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
invocation, err := classifyCrackInvocation(newCrackDispatchCommand(t, test.flags...), test.args)
|
||||
if test.wantError != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("classifyCrackInvocation() error = %v, want substring %q", err, test.wantError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("classifyCrackInvocation() error = %v", err)
|
||||
}
|
||||
if invocation.mode != test.wantMode || invocation.backendInfoLevel != test.wantBackendLevel ||
|
||||
invocation.hashInfoLevel != test.wantHashLevel || invocation.crackstation != test.wantCrackstation {
|
||||
t.Fatalf("classifyCrackInvocation() = %#v, want mode %d backend level %d hash level %d station %q",
|
||||
invocation, test.wantMode, test.wantBackendLevel, test.wantHashLevel, test.wantCrackstation)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type crackDispatchRPCCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
crackstationsCalls int
|
||||
crackCalls int
|
||||
crackFilesCalls int
|
||||
hadDeadline bool
|
||||
crackHadDeadline bool
|
||||
err error
|
||||
response *clientpb.Crackstations
|
||||
crackResponse *clientpb.CrackResponse
|
||||
crackFilesResponse *clientpb.CrackFiles
|
||||
crackRequest *clientpb.CrackCommand
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) Crackstations(ctx context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.Crackstations, error) {
|
||||
capture.crackstationsCalls++
|
||||
_, capture.hadDeadline = ctx.Deadline()
|
||||
if capture.err != nil {
|
||||
return nil, capture.err
|
||||
}
|
||||
if capture.response != nil {
|
||||
return capture.response, nil
|
||||
}
|
||||
return &clientpb.Crackstations{}, nil
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) Crack(ctx context.Context, request *clientpb.CrackCommand, _ ...grpc.CallOption) (*clientpb.CrackResponse, error) {
|
||||
capture.crackCalls++
|
||||
_, capture.crackHadDeadline = ctx.Deadline()
|
||||
capture.crackRequest = request
|
||||
if capture.crackResponse != nil {
|
||||
return capture.crackResponse, nil
|
||||
}
|
||||
return &clientpb.CrackResponse{}, nil
|
||||
}
|
||||
|
||||
func (capture *crackDispatchRPCCapture) CrackFilesList(_ context.Context, _ *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
|
||||
capture.crackFilesCalls++
|
||||
if capture.crackFilesResponse != nil {
|
||||
return capture.crackFilesResponse, nil
|
||||
}
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
|
||||
func TestCrackBackendInfoQueriesStationsWithoutCreatingJob(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, "--backend-info", "--timeout=1"), con, nil)
|
||||
if capture.crackstationsCalls != 1 {
|
||||
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackCalls != 0 || capture.crackFilesCalls != 0 {
|
||||
t.Fatalf("backend-info used job path: Crack=%d CrackFilesList=%d", capture.crackCalls, capture.crackFilesCalls)
|
||||
}
|
||||
if !capture.hadDeadline {
|
||||
t.Fatal("Crackstations context did not inherit --timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackKeyspaceUsesSynchronousCrackResponse(t *testing.T) {
|
||||
digest := strings.Repeat("a", 64)
|
||||
capture := &crackDispatchRPCCapture{
|
||||
crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: "100",
|
||||
}},
|
||||
crackFilesResponse: &clientpb.CrackFiles{
|
||||
Files: []*clientpb.CrackFile{{
|
||||
ID: "wordlist-id", Name: "words.txt", Type: clientpb.CrackFileType_WORDLIST, Sha2_256: digest,
|
||||
}},
|
||||
},
|
||||
}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, "--keyspace", "--input=words.txt", "--crackstation=station-one", "--timeout=1"), con, nil)
|
||||
if capture.crackCalls != 1 {
|
||||
t.Fatalf("Crack calls = %d, want 1", capture.crackCalls)
|
||||
}
|
||||
if capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("keyspace queried crackstations directly %d times", capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackFilesCalls != 1 {
|
||||
t.Fatalf("CrackFilesList calls = %d, want 1", capture.crackFilesCalls)
|
||||
}
|
||||
if !capture.crackHadDeadline {
|
||||
t.Fatal("Crack context did not inherit --timeout")
|
||||
}
|
||||
if capture.crackRequest == nil || !capture.crackRequest.Keyspace {
|
||||
t.Fatalf("Crack request = %#v, want keyspace mode", capture.crackRequest)
|
||||
}
|
||||
if capture.crackRequest.Crackstation != "station-one" {
|
||||
t.Fatalf("Crack request station = %q, want station-one", capture.crackRequest.Crackstation)
|
||||
}
|
||||
wantInput := "crackfile://wordlist/" + digest
|
||||
if len(capture.crackRequest.PositionalArguments) != 1 || capture.crackRequest.PositionalArguments[0] != wantInput {
|
||||
t.Fatalf("resolved keyspace inputs = %#v, want %q", capture.crackRequest.PositionalArguments, wantInput)
|
||||
}
|
||||
if len(capture.crackRequest.Hashes) != 0 || len(capture.crackRequest.CredentialIDs) != 0 {
|
||||
t.Fatalf("keyspace request contains hash selectors: %#v", capture.crackRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackSynchronousQueriesUseCrackRPCWithoutCreatingJob(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string
|
||||
args []string
|
||||
resultMode clientpb.CrackQueryMode
|
||||
result string
|
||||
check func(*testing.T, *clientpb.CrackCommand)
|
||||
}{
|
||||
{
|
||||
name: "total candidates",
|
||||
flags: []string{"--total-candidates", "--attack-mode=3", "--input=?d", "--crackstation=station-one"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES,
|
||||
result: "100",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if !command.TotalCandidates || command.Crackstation != "station-one" {
|
||||
t.Fatalf("total-candidates request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lookup",
|
||||
flags: []string{"--lookup=candidate", "--attack-mode=3", "--input=?d"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
|
||||
result: "lookup output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if command.Lookup != "candidate" {
|
||||
t.Fatalf("lookup request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identify",
|
||||
flags: []string{"--identify-mode"},
|
||||
args: []string{"digest"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY,
|
||||
result: "identify output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if !command.IdentifyMode || len(command.Hashes) != 1 || command.Hashes[0] != "digest" {
|
||||
t.Fatalf("identify request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hash info",
|
||||
flags: []string{"--hash-info-level=2", "--hash-mode=1000"},
|
||||
resultMode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO,
|
||||
result: "hash-info output",
|
||||
check: func(t *testing.T, command *clientpb.CrackCommand) {
|
||||
if command.HashInfoLevel != 2 || command.HashMode == nil || *command.HashMode != 1000 {
|
||||
t.Fatalf("hash-info request = %#v", command)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{crackResponse: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: test.resultMode, Value: test.result,
|
||||
}}}
|
||||
con := console.NewConsole(false)
|
||||
emptyCommands := func() *cobra.Command { return &cobra.Command{Use: "test"} }
|
||||
if err := console.StartClient(con, capture, nil, nil, emptyCommands, emptyCommands, false, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
CrackCmd(newCrackDispatchCommand(t, test.flags...), con, test.args)
|
||||
if capture.crackCalls != 1 || capture.crackstationsCalls != 0 {
|
||||
t.Fatalf("RPC calls: Crack=%d Crackstations=%d", capture.crackCalls, capture.crackstationsCalls)
|
||||
}
|
||||
if capture.crackRequest == nil {
|
||||
t.Fatal("Crack request was not captured")
|
||||
}
|
||||
test.check(t, capture.crackRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalCrackKeyspace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
response *clientpb.CrackResponse
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "canonical", response: &clientpb.CrackResponse{Keyspace: "100"}, want: "100"},
|
||||
{name: "normalizes", response: &clientpb.CrackResponse{Keyspace: " 00100 "}, want: "100"},
|
||||
{name: "zero", response: &clientpb.CrackResponse{Keyspace: "0"}, want: "0"},
|
||||
{name: "missing response", wantErr: true},
|
||||
{name: "missing value", response: &clientpb.CrackResponse{}, wantErr: true},
|
||||
{name: "negative", response: &clientpb.CrackResponse{Keyspace: "-1"}, wantErr: true},
|
||||
{name: "overflow", response: &clientpb.CrackResponse{Keyspace: "18446744073709551616"}, wantErr: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := canonicalCrackKeyspace(test.response)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("canonicalCrackKeyspace() error = %v, wantErr %v", err, test.wantErr)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("canonicalCrackKeyspace() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackQueryResponseValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
invocation crackInvocation
|
||||
response *clientpb.CrackResponse
|
||||
want string
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "typed keyspace",
|
||||
invocation: crackInvocation{mode: crackInvocationKeyspace},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, Value: " 00100 ",
|
||||
}},
|
||||
want: "100",
|
||||
},
|
||||
{
|
||||
name: "typed total candidates",
|
||||
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES, Value: "42\n",
|
||||
}},
|
||||
want: "42",
|
||||
},
|
||||
{
|
||||
name: "text remains verbatim",
|
||||
invocation: crackInvocation{mode: crackInvocationLookup},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP, Value: "line one\nline two\n",
|
||||
}},
|
||||
want: "line one\nline two\n",
|
||||
},
|
||||
{
|
||||
name: "wrong typed mode",
|
||||
invocation: crackInvocation{mode: crackInvocationLookup},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY, Value: "output",
|
||||
}},
|
||||
wantError: "for lookup query",
|
||||
},
|
||||
{
|
||||
name: "new mode cannot use legacy keyspace",
|
||||
invocation: crackInvocation{mode: crackInvocationTotalCandidates},
|
||||
response: &clientpb.CrackResponse{Keyspace: "42"},
|
||||
wantError: "empty total-candidates query result",
|
||||
},
|
||||
{
|
||||
name: "selected keyspace cannot use legacy response",
|
||||
invocation: crackInvocation{mode: crackInvocationKeyspace, crackstation: "station-one"},
|
||||
response: &clientpb.CrackResponse{Keyspace: "42"},
|
||||
wantError: "empty keyspace query result",
|
||||
},
|
||||
{
|
||||
name: "empty text",
|
||||
invocation: crackInvocation{mode: crackInvocationHashInfo},
|
||||
response: &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO, Value: " \n",
|
||||
}},
|
||||
wantError: "empty hash-info query result",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := crackQueryResponseValue(test.response, test.invocation)
|
||||
if test.wantError != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
||||
t.Fatalf("crackQueryResponseValue() error = %v, want substring %q", err, test.wantError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Fatalf("crackQueryResponseValue() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackQueryResponsePreservesStderr(t *testing.T) {
|
||||
response := &clientpb.CrackResponse{Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP,
|
||||
Value: "lookup output",
|
||||
Stderr: "hashcat warning\n",
|
||||
}}
|
||||
value, stderr, err := crackQueryResponse(response, crackInvocation{mode: crackInvocationLookup})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value != "lookup output" || stderr != "hashcat warning\n" {
|
||||
t.Fatalf("crackQueryResponse() = (%q, %q), want preserved value and stderr", value, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackResponseKeyspaceFieldNumberAndRoundTrip(t *testing.T) {
|
||||
field := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Keyspace")
|
||||
if field == nil {
|
||||
t.Fatal("CrackResponse is missing field Keyspace")
|
||||
}
|
||||
if got, want := field.Number(), protoreflect.FieldNumber(2); got != want {
|
||||
t.Fatalf("CrackResponse.Keyspace tag = %d, want %d", got, want)
|
||||
}
|
||||
queryField := (&clientpb.CrackResponse{}).ProtoReflect().Descriptor().Fields().ByName("Query")
|
||||
if queryField == nil {
|
||||
t.Fatal("CrackResponse is missing field Query")
|
||||
}
|
||||
if got, want := queryField.Number(), protoreflect.FieldNumber(3); got != want {
|
||||
t.Fatalf("CrackResponse.Query tag = %d, want %d", got, want)
|
||||
}
|
||||
stderrField := (&clientpb.CrackQueryResult{}).ProtoReflect().Descriptor().Fields().ByName("Stderr")
|
||||
if stderrField == nil {
|
||||
t.Fatal("CrackQueryResult is missing field Stderr")
|
||||
}
|
||||
if got, want := stderrField.Number(), protoreflect.FieldNumber(6); got != want {
|
||||
t.Fatalf("CrackQueryResult.Stderr tag = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
want := &clientpb.CrackResponse{
|
||||
Job: &clientpb.CrackJob{ID: "job-id"},
|
||||
Keyspace: "18446744073709551615",
|
||||
Query: &clientpb.CrackQueryResult{
|
||||
Mode: clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE, CrackstationHostUUID: "host-id", Value: "42", Stderr: "warning\n",
|
||||
},
|
||||
}
|
||||
raw, err := proto.Marshal(want)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal crack response: %v", err)
|
||||
}
|
||||
got := &clientpb.CrackResponse{}
|
||||
if err := proto.Unmarshal(raw, got); err != nil {
|
||||
t.Fatalf("unmarshal crack response: %v", err)
|
||||
}
|
||||
if !proto.Equal(got, want) {
|
||||
t.Fatalf("crack response round trip = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectedCrackstationsReturnsRPCError(t *testing.T) {
|
||||
want := errors.New("query failed")
|
||||
capture := &crackDispatchRPCCapture{err: want}
|
||||
if _, err := connectedCrackstations(context.Background(), capture); !errors.Is(err, want) {
|
||||
t.Fatalf("connectedCrackstations() error = %v, want %v", err, want)
|
||||
}
|
||||
if capture.crackstationsCalls != 1 {
|
||||
t.Fatalf("Crackstations calls = %d, want 1", capture.crackstationsCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectedCrackstationsReturnsEveryNonNilStation(t *testing.T) {
|
||||
capture := &crackDispatchRPCCapture{response: &clientpb.Crackstations{Crackstations: []*clientpb.Crackstation{
|
||||
nil,
|
||||
{Name: "one"},
|
||||
{Name: "two"},
|
||||
}}}
|
||||
stations, err := connectedCrackstations(context.Background(), capture)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(stations) != 2 || stations[0].Name != "one" || stations[1].Name != "two" {
|
||||
t.Fatalf("connectedCrackstations() = %#v, want one and two", stations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackHashcatFlagsAreRootLocal(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
if root.Flags().Lookup("backend-info") == nil {
|
||||
t.Fatal("root crack command is missing --backend-info")
|
||||
}
|
||||
if root.Flags().Lookup("crackstation") == nil {
|
||||
t.Fatal("root crack command is missing --crackstation")
|
||||
}
|
||||
if root.PersistentFlags().Lookup("backend-info") != nil {
|
||||
t.Fatal("--backend-info must not be inherited by crack subcommands")
|
||||
}
|
||||
if root.Flags().Lookup("stdout") != nil {
|
||||
t.Fatal("root crack command unexpectedly exposes removed --stdout")
|
||||
}
|
||||
|
||||
stations, _, err := root.Find([]string{"stations"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack stations: %v", err)
|
||||
}
|
||||
if stations.InheritedFlags().Lookup("backend-info") != nil {
|
||||
t.Fatal("crack stations unexpectedly inherits --backend-info")
|
||||
}
|
||||
if stations.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack stations must continue to inherit --timeout")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const crackFileScheme = "crackfile://"
|
||||
|
||||
type crackFilesListFunc func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error)
|
||||
|
||||
// resolveManagedCrackFileReferences replaces exact server-side crack file
|
||||
// names, IDs, and digests with the content-addressed URI understood by a
|
||||
// crackstation. Inputs that do not match a managed file remain ordinary
|
||||
// Hashcat operands, masks, paths, or inline rule content.
|
||||
//
|
||||
//nolint:gocyclo // Resolve every managed-file role together in one deterministic pass.
|
||||
func resolveManagedCrackFileReferences(ctx context.Context, command *clientpb.CrackCommand, list crackFilesListFunc) error {
|
||||
if command == nil {
|
||||
return fmt.Errorf("missing crack command")
|
||||
}
|
||||
if list == nil {
|
||||
return fmt.Errorf("missing crack file lister")
|
||||
}
|
||||
|
||||
wordlistIndexes := make([]int, 0, len(command.PositionalArguments))
|
||||
wordlistInputs := make([]string, 0, len(command.PositionalArguments))
|
||||
for index, argument := range command.PositionalArguments {
|
||||
// Only wordlist-role operands may be resolved by alias. An inline mask can
|
||||
// legitimately have the same text as a managed file name and must remain
|
||||
// an inline mask. Explicit crackfile URIs are still validated here so a
|
||||
// mistyped managed reference fails at the client boundary.
|
||||
if !crackOperandIsWordlist(command.AttackMode, index) && !strings.HasPrefix(argument, crackFileScheme) {
|
||||
continue
|
||||
}
|
||||
wordlistIndexes = append(wordlistIndexes, index)
|
||||
wordlistInputs = append(wordlistInputs, argument)
|
||||
}
|
||||
if len(wordlistInputs) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, wordlistInputs, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index, argument := range resolved {
|
||||
command.PositionalArguments[wordlistIndexes[index]] = argument
|
||||
}
|
||||
}
|
||||
legacyIdentify := command.Identify //nolint:staticcheck // Identify remains required for legacy wire compatibility.
|
||||
if len(command.PositionalArguments) == 0 && legacyIdentify != "" &&
|
||||
(crackOperandIsWordlist(command.AttackMode, 0) || strings.HasPrefix(legacyIdentify, crackFileScheme)) {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{legacyIdentify}, clientpb.CrackFileType_WORDLIST, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.Identify = resolved[0] //nolint:staticcheck // Keep the resolved legacy operand synchronized for older servers.
|
||||
}
|
||||
|
||||
rules := command.RulesFilesV7
|
||||
if len(rules) == 0 && len(command.RulesFile) > 0 {
|
||||
rules = [][]byte{command.RulesFile}
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
values := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
values = append(values, string(rule))
|
||||
}
|
||||
resolved, err := resolveCrackFileValues(ctx, values, clientpb.CrackFileType_RULES, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.RulesFilesV7 = make([][]byte, 0, len(resolved))
|
||||
for _, rule := range resolved {
|
||||
command.RulesFilesV7 = append(command.RulesFilesV7, []byte(rule))
|
||||
}
|
||||
command.RulesFile = append([]byte(nil), command.RulesFilesV7[0]...)
|
||||
}
|
||||
|
||||
if len(command.MarkovHcstat2) > 0 {
|
||||
resolved, err := resolveCrackFileValues(ctx, []string{string(command.MarkovHcstat2)}, clientpb.CrackFileType_MARKOV_HCSTAT2, list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command.MarkovHcstat2 = []byte(resolved[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackOperandIsWordlist(attackMode clientpb.CrackAttackMode, index int) bool {
|
||||
switch attackMode {
|
||||
case clientpb.CrackAttackMode_STRAIGHT, clientpb.CrackAttackMode_COMBINATION:
|
||||
return true
|
||||
case clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK:
|
||||
return index == 0
|
||||
case clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST:
|
||||
return index == 1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Validation and alias disambiguation intentionally share the manifest lookup and preserve input ordering.
|
||||
func resolveCrackFileValues(ctx context.Context, values []string, fileType clientpb.CrackFileType, list crackFilesListFunc) ([]string, error) {
|
||||
needsManifest := false
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
if err := validateCrackFileURI(value, fileType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
needsManifest = true
|
||||
}
|
||||
if !needsManifest {
|
||||
return append([]string(nil), values...), nil
|
||||
}
|
||||
|
||||
manifest, err := list(ctx, &clientpb.CrackFile{Type: fileType})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list managed %s files: %w", crackFileTypeToken(fileType), err)
|
||||
}
|
||||
files := []*clientpb.CrackFile(nil)
|
||||
if manifest != nil {
|
||||
files = manifest.Files
|
||||
}
|
||||
resolved := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, crackFileScheme) {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
matches := make([]*clientpb.CrackFile, 0, 1)
|
||||
for _, file := range files {
|
||||
if file == nil || file.Type != fileType {
|
||||
continue
|
||||
}
|
||||
if value == file.Name || value == file.ID || strings.EqualFold(value, file.Sha2_256) {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return nil, fmt.Errorf("managed %s file reference %q is ambiguous", crackFileTypeToken(fileType), value)
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
resolved = append(resolved, value)
|
||||
continue
|
||||
}
|
||||
uri, err := crackFileURI(matches[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved = append(resolved, uri)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func crackFileURI(file *clientpb.CrackFile) (string, error) {
|
||||
if file == nil {
|
||||
return "", fmt.Errorf("missing managed crack file")
|
||||
}
|
||||
digest := strings.ToLower(strings.TrimSpace(file.Sha2_256))
|
||||
if err := validateCrackFileDigest(digest); err != nil {
|
||||
return "", fmt.Errorf("managed crack file %q has invalid SHA-256: %w", file.Name, err)
|
||||
}
|
||||
fileType := crackFileTypeToken(file.Type)
|
||||
if fileType == "" {
|
||||
return "", fmt.Errorf("managed crack file %q has unsupported type %s", file.Name, file.Type.String())
|
||||
}
|
||||
return crackFileScheme + fileType + "/" + digest, nil
|
||||
}
|
||||
|
||||
func validateCrackFileURI(value string, expectedType clientpb.CrackFileType) error {
|
||||
remainder := strings.TrimPrefix(value, crackFileScheme)
|
||||
parts := strings.Split(remainder, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return fmt.Errorf("invalid managed crack file URI %q", value)
|
||||
}
|
||||
if parts[0] != crackFileTypeToken(expectedType) {
|
||||
return fmt.Errorf("managed crack file URI %q has type %q, expected %q", value, parts[0], crackFileTypeToken(expectedType))
|
||||
}
|
||||
if parts[1] != strings.ToLower(parts[1]) {
|
||||
return fmt.Errorf("managed crack file URI %q must use a lowercase SHA-256", value)
|
||||
}
|
||||
if err := validateCrackFileDigest(parts[1]); err != nil {
|
||||
return fmt.Errorf("invalid managed crack file URI %q: %w", value, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCrackFileDigest(value string) error {
|
||||
if len(value) != 64 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
decoded, err := hex.DecodeString(value)
|
||||
if err != nil || len(decoded) != 32 {
|
||||
return fmt.Errorf("SHA-256 must contain 64 hexadecimal characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func crackFileTypeToken(fileType clientpb.CrackFileType) string {
|
||||
switch fileType {
|
||||
case clientpb.CrackFileType_WORDLIST:
|
||||
return "wordlist"
|
||||
case clientpb.CrackFileType_RULES:
|
||||
return "rules"
|
||||
case clientpb.CrackFileType_MARKOV_HCSTAT2:
|
||||
return "hcstat2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestResolveManagedCrackFileReferences(t *testing.T) {
|
||||
wordlistDigest := strings.Repeat("a", 64)
|
||||
rulesDigest := strings.Repeat("b", 64)
|
||||
hcstat2Digest := strings.Repeat("c", 64)
|
||||
files := map[clientpb.CrackFileType][]*clientpb.CrackFile{
|
||||
clientpb.CrackFileType_WORDLIST: {{ID: "wordlist-id", Name: "rockyou.txt", Sha2_256: wordlistDigest, Type: clientpb.CrackFileType_WORDLIST}},
|
||||
clientpb.CrackFileType_RULES: {{ID: "rules-id", Name: "best64.rule", Sha2_256: rulesDigest, Type: clientpb.CrackFileType_RULES}},
|
||||
clientpb.CrackFileType_MARKOV_HCSTAT2: {{
|
||||
ID: "hcstat2-id", Name: "hashcat.hcstat2", Sha2_256: hcstat2Digest, Type: clientpb.CrackFileType_MARKOV_HCSTAT2,
|
||||
}},
|
||||
}
|
||||
requested := make([]clientpb.CrackFileType, 0)
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
requested = append(requested, request.Type)
|
||||
return &clientpb.CrackFiles{Files: files[request.Type]}, nil
|
||||
}
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
PositionalArguments: []string{"rockyou.txt", "?d?d?d"},
|
||||
RulesFile: []byte("best64.rule"),
|
||||
RulesFilesV7: [][]byte{[]byte("best64.rule"), []byte(":")},
|
||||
MarkovHcstat2: []byte("hcstat2-id"),
|
||||
}
|
||||
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if want := []string{"crackfile://wordlist/" + wordlistDigest, "?d?d?d"}; !slices.Equal(command.PositionalArguments, want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, want)
|
||||
}
|
||||
wantRules := [][]byte{[]byte("crackfile://rules/" + rulesDigest), []byte(":")}
|
||||
if !slices.EqualFunc(command.RulesFilesV7, wantRules, func(left, right []byte) bool { return slices.Equal(left, right) }) {
|
||||
t.Fatalf("RulesFilesV7 = %#v, want %#v", command.RulesFilesV7, wantRules)
|
||||
}
|
||||
if !slices.Equal(command.RulesFile, wantRules[0]) {
|
||||
t.Fatalf("RulesFile = %q", command.RulesFile)
|
||||
}
|
||||
if want := "crackfile://hcstat2/" + hcstat2Digest; string(command.MarkovHcstat2) != want {
|
||||
t.Fatalf("MarkovHcstat2 = %q, want %q", command.MarkovHcstat2, want)
|
||||
}
|
||||
if want := []clientpb.CrackFileType{clientpb.CrackFileType_WORDLIST, clientpb.CrackFileType_RULES, clientpb.CrackFileType_MARKOV_HCSTAT2}; !slices.Equal(requested, want) {
|
||||
t.Fatalf("requested types = %#v, want %#v", requested, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRespectsAttackOperandRoles(t *testing.T) {
|
||||
digestA := strings.Repeat("a", 64)
|
||||
digestB := strings.Repeat("b", 64)
|
||||
files := []*clientpb.CrackFile{
|
||||
{ID: "wordlist-a", Name: "first", Sha2_256: digestA, Type: clientpb.CrackFileType_WORDLIST},
|
||||
{ID: "wordlist-b", Name: "second", Sha2_256: digestB, Type: clientpb.CrackFileType_WORDLIST},
|
||||
}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
if request.Type != clientpb.CrackFileType_WORDLIST {
|
||||
t.Fatalf("requested file type = %s, want WORDLIST", request.Type)
|
||||
}
|
||||
return &clientpb.CrackFiles{Files: files}, nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attackMode clientpb.CrackAttackMode
|
||||
inputs []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "bruteforce mask collision",
|
||||
attackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
inputs: []string{"first"},
|
||||
want: []string{"first"},
|
||||
},
|
||||
{
|
||||
name: "wordlist mask hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_WORDLIST_MASK,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "second"},
|
||||
},
|
||||
{
|
||||
name: "mask wordlist hybrid",
|
||||
attackMode: clientpb.CrackAttackMode_HYBRID_MASK_WORDLIST,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"first", "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "straight wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_STRAIGHT,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
{
|
||||
name: "combination wordlists",
|
||||
attackMode: clientpb.CrackAttackMode_COMBINATION,
|
||||
inputs: []string{"first", "second"},
|
||||
want: []string{"crackfile://wordlist/" + digestA, "crackfile://wordlist/" + digestB},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: test.attackMode,
|
||||
PositionalArguments: append([]string(nil), test.inputs...),
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatalf("resolveManagedCrackFileReferences: %v", err)
|
||||
}
|
||||
if !slices.Equal(command.PositionalArguments, test.want) {
|
||||
t.Fatalf("PositionalArguments = %#v, want %#v", command.PositionalArguments, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesPreservesInlineBytes(t *testing.T) {
|
||||
inline := []byte{0, 1, 2, 0xff}
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{append([]byte(nil), inline...)}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(command.RulesFilesV7) != 1 || !slices.Equal(command.RulesFilesV7[0], inline) {
|
||||
t.Fatalf("inline rules changed: %#v", command.RulesFilesV7)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesAcceptsCanonicalURIWithoutListing(t *testing.T) {
|
||||
uri := "crackfile://wordlist/" + strings.Repeat("d", 64)
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{uri}}
|
||||
list := func(_ context.Context, _ *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
t.Fatal("canonical URI should not require a manifest lookup")
|
||||
return nil, nil
|
||||
}
|
||||
if err := resolveManagedCrackFileReferences(context.Background(), command, list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if command.PositionalArguments[0] != uri {
|
||||
t.Fatalf("URI changed to %q", command.PositionalArguments[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsWrongURIType(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{RulesFilesV7: [][]byte{[]byte("crackfile://wordlist/" + strings.Repeat("e", 64))}}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, func(context.Context, *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return nil, errors.New("must not be called")
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "expected \"rules\"") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsAmbiguousName(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{PositionalArguments: []string{"duplicate.txt"}}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("a", 64), Type: request.Type},
|
||||
{Name: "duplicate.txt", Sha2_256: strings.Repeat("b", 64), Type: request.Type},
|
||||
}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "ambiguous") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveManagedCrackFileReferencesRejectsInvalidDigest(t *testing.T) {
|
||||
command := &clientpb.CrackCommand{MarkovHcstat2: []byte("broken.hcstat2")}
|
||||
list := func(_ context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return &clientpb.CrackFiles{Files: []*clientpb.CrackFile{{
|
||||
Name: "broken.hcstat2", Sha2_256: "not-a-digest", Type: request.Type,
|
||||
}}}, nil
|
||||
}
|
||||
err := resolveManagedCrackFileReferences(context.Background(), command, list)
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid SHA-256") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -22,18 +22,36 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func localCrackFileInfo(path string) (os.FileInfo, error) {
|
||||
return localCrackFileInfoWith(path, os.Stat)
|
||||
}
|
||||
|
||||
func localCrackFileInfoWith(path string, stat func(string) (os.FileInfo, error)) (os.FileInfo, error) {
|
||||
info, err := stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat local crack file %q: %w", path, err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("local crack file %q is a directory", path)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// CrackWordlistsCmd - Manage GPU cracking stations.
|
||||
func CrackWordlistsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
@@ -106,17 +124,17 @@ func PrintCrackFiles(crackFiles *clientpb.CrackFiles, con *console.SliverClient)
|
||||
|
||||
func PrintCrackFilesByType(crackFiles *clientpb.CrackFiles, con *console.SliverClient) {
|
||||
wordlistTable := table.NewWriter()
|
||||
wordlistTable.SetTitle(console.Bold + "Wordlists" + console.Normal)
|
||||
wordlistTable.SetTitle(console.StyleBold.Render("Wordlists"))
|
||||
wordlistTable.SetStyle(settings.GetTableStyle(con))
|
||||
wordlistTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
rulesTable := table.NewWriter()
|
||||
rulesTable.SetTitle(console.Bold + "Rules" + console.Normal)
|
||||
rulesTable.SetTitle(console.StyleBold.Render("Rules"))
|
||||
rulesTable.SetStyle(settings.GetTableStyle(con))
|
||||
rulesTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
hcTable := table.NewWriter()
|
||||
hcTable.SetTitle(console.Bold + "Markov Hcstat2" + console.Normal)
|
||||
hcTable.SetTitle(console.StyleBold.Render("Markov Hcstat2"))
|
||||
hcTable.SetStyle(settings.GetTableStyle(con))
|
||||
hcTable.AppendHeader(table.Row{"Name", "Size"})
|
||||
|
||||
@@ -173,9 +191,9 @@ func CrackWordlistsAddCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
wordlistStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || wordlistStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
wordlistStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid wordlist file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -217,9 +235,9 @@ func CrackRulesAddCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
rulesStat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || rulesStat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
rulesStat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid rules file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -261,9 +279,9 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
con.PrintErrorf("No path specified, see --help\n")
|
||||
return
|
||||
}
|
||||
hcstat2Stat, err := os.Stat(localPath)
|
||||
if os.IsNotExist(err) || hcstat2Stat.IsDir() {
|
||||
con.PrintErrorf("File does not exist: %s\n", localPath)
|
||||
hcstat2Stat, err := localCrackFileInfo(localPath)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid hcstat2 file: %s\n", err)
|
||||
return
|
||||
}
|
||||
if name == "" {
|
||||
@@ -294,102 +312,176 @@ func CrackHcstat2AddCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
}
|
||||
|
||||
func addCrackFile(localFile *os.File, crackFile *clientpb.CrackFile, con *console.SliverClient) {
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
|
||||
var chunkReaderErr error
|
||||
go func() {
|
||||
chunkReaderErr = chunkReader(wordlistReader, crackFile.ChunkSize, chunks)
|
||||
}()
|
||||
n := uint32(0)
|
||||
total := int64(0)
|
||||
errors := []error{}
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
total, err := uploadCrackFile(localFile, crackFile, con.Rpc, func(total int64, n uint32, chunkSize int) {
|
||||
con.PrintInfof("Uploading %s (chunk %d - %s) ...",
|
||||
util.ByteCountBinary(total),
|
||||
n,
|
||||
util.ByteCountBinary(int64(len(chunk))),
|
||||
util.ByteCountBinary(int64(chunkSize)),
|
||||
)
|
||||
_, err := con.Rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
n++
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if chunkReaderErr != nil {
|
||||
con.PrintErrorf("Failed to read file: %s\n", chunkReaderErr)
|
||||
return
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
for _, err := range errors {
|
||||
con.PrintErrorf("Failed to upload chunk: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := con.Rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
con.Printf(console.Clearln + "\r")
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to complete file upload: %s\n", err)
|
||||
con.PrintErrorf("Failed to upload crack file: %s\n", err)
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Upload completed (compressed: %s)\n", util.ByteCountBinary(total))
|
||||
}
|
||||
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) error {
|
||||
func uploadCrackFile(localFile io.Reader, crackFile *clientpb.CrackFile, rpc rpcpb.SliverRPCClient, progress func(total int64, n uint32, chunkSize int)) (total int64, retErr error) {
|
||||
if localFile == nil {
|
||||
return 0, errors.New("local crack file reader is nil")
|
||||
}
|
||||
if crackFile == nil || crackFile.ID == "" {
|
||||
return 0, errors.New("crack file reservation is missing an ID")
|
||||
}
|
||||
if rpc == nil {
|
||||
return 0, errors.New("crack file RPC client is nil")
|
||||
}
|
||||
|
||||
completed := false
|
||||
defer func() {
|
||||
if retErr == nil || completed {
|
||||
return
|
||||
}
|
||||
_, cleanupErr := rpc.CrackFileDelete(context.Background(), &clientpb.CrackFile{ID: crackFile.ID})
|
||||
if cleanupErr != nil {
|
||||
retErr = errors.Join(retErr, fmt.Errorf("failed to remove crack file reservation %s: %w", crackFile.ID, cleanupErr))
|
||||
}
|
||||
}()
|
||||
|
||||
digest := sha256.New()
|
||||
wordlistReader := io.TeeReader(localFile, digest)
|
||||
|
||||
chunks, chunkReaderErrors := readCrackFileChunks(wordlistReader, crackFile.ChunkSize)
|
||||
n := uint32(0)
|
||||
var uploadErr error
|
||||
for chunk := range chunks {
|
||||
total += int64(len(chunk))
|
||||
if uploadErr == nil {
|
||||
if progress != nil {
|
||||
progress(total, n, len(chunk))
|
||||
}
|
||||
_, err := rpc.CrackFileChunkUpload(context.Background(), &clientpb.CrackFileChunk{
|
||||
CrackFileID: crackFile.ID,
|
||||
N: n,
|
||||
Data: chunk,
|
||||
})
|
||||
if err != nil {
|
||||
uploadErr = fmt.Errorf("upload chunk %d: %w", n, err)
|
||||
}
|
||||
}
|
||||
n++
|
||||
}
|
||||
chunkReaderErr := <-chunkReaderErrors
|
||||
if chunkReaderErr != nil {
|
||||
uploadErr = errors.Join(uploadErr, fmt.Errorf("read and compress crack file: %w", chunkReaderErr))
|
||||
}
|
||||
if uploadErr != nil {
|
||||
return total, uploadErr
|
||||
}
|
||||
_, err := rpc.CrackFileComplete(context.Background(), &clientpb.CrackFile{
|
||||
ID: crackFile.ID,
|
||||
Sha2_256: hex.EncodeToString(digest.Sum(nil)),
|
||||
})
|
||||
if err != nil {
|
||||
return total, fmt.Errorf("complete crack file upload: %w", err)
|
||||
}
|
||||
completed = true
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func readCrackFileChunks(reader io.Reader, chunkSize int64) (<-chan []byte, <-chan error) {
|
||||
chunks := make(chan []byte, 1) // Chunks are in-memory!
|
||||
errors := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(errors)
|
||||
errors <- chunkReader(reader, chunkSize, chunks)
|
||||
}()
|
||||
return chunks, errors
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Compression, fixed-size emission, final remainder handling, and cleanup form one streaming state machine.
|
||||
func chunkReader(wordlistReader io.Reader, chunkSize int64, chunks chan []byte) (retErr error) {
|
||||
defer close(chunks)
|
||||
if chunkSize < 1 {
|
||||
return fmt.Errorf("invalid crack file chunk size %d", chunkSize)
|
||||
}
|
||||
start := int64(0)
|
||||
tmpFile, err := os.CreateTemp("", "sliver-wordlist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
compressor, _ := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
for {
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return err
|
||||
tmpPath := tmpFile.Name()
|
||||
defer func() {
|
||||
if err := tmpFile.Close(); retErr == nil && err != nil {
|
||||
retErr = err
|
||||
}
|
||||
_ = os.Remove(tmpPath)
|
||||
}()
|
||||
compressor, err := zstd.NewWriter(tmpFile, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed := false
|
||||
defer func() {
|
||||
if !compressorClosed {
|
||||
_ = compressor.Close()
|
||||
}
|
||||
}()
|
||||
readBuf := make([]byte, 32*1024*1024) // 32MB
|
||||
for {
|
||||
readN, readErr := wordlistReader.Read(readBuf)
|
||||
if readN != 0 {
|
||||
_, err := compressor.Write(readBuf[:readN])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
compressor.Flush()
|
||||
tmpFileStat, err := os.Stat(tmpFile.Name())
|
||||
if err := compressor.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tmpFileStat.Size()-start >= chunkSize {
|
||||
stop := start + chunkSize
|
||||
for tmpFileStat.Size()-start >= chunkSize {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start = stop
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
}
|
||||
if readErr != nil && readErr != io.EOF {
|
||||
return readErr
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
chunk, err := readChunkAt(tmpFile, start, chunkSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
return nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := compressor.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
compressorClosed = true
|
||||
tmpFileStat, err := tmpFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for start < tmpFileStat.Size() {
|
||||
remaining := tmpFileStat.Size() - start
|
||||
readSize := chunkSize
|
||||
if remaining < readSize {
|
||||
readSize = remaining
|
||||
}
|
||||
chunk, err := readChunkAt(tmpFile, start, readSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chunks <- chunk
|
||||
start += int64(len(chunk))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readChunkAt(tmpFile *os.File, offset int64, chunkSize int64) ([]byte, error) {
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestLocalCrackFileInfoHandlesStatErrors(t *testing.T) {
|
||||
wantErr := errors.New("permission denied")
|
||||
info, err := localCrackFileInfoWith("/restricted/file", func(string) (os.FileInfo, error) {
|
||||
return nil, wantErr
|
||||
})
|
||||
if info != nil {
|
||||
t.Fatalf("localCrackFileInfoWith info = %#v, want nil", info)
|
||||
}
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("localCrackFileInfoWith error = %v, want wrapped %v", err, wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/restricted/file") {
|
||||
t.Fatalf("localCrackFileInfoWith error = %q, want path context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoRejectsDirectories(t *testing.T) {
|
||||
_, err := localCrackFileInfo(t.TempDir())
|
||||
if err == nil || !strings.Contains(err.Error(), "is a directory") {
|
||||
t.Fatalf("localCrackFileInfo directory error = %v, want directory error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalCrackFileInfoAcceptsRegularFile(t *testing.T) {
|
||||
file, err := os.CreateTemp(t.TempDir(), "crack-file-")
|
||||
if err != nil {
|
||||
t.Fatalf("create local crack file: %v", err)
|
||||
}
|
||||
const payload = "hashcat input"
|
||||
if _, err := file.WriteString(payload); err != nil {
|
||||
t.Fatalf("write local crack file: %v", err)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
t.Fatalf("close local crack file: %v", err)
|
||||
}
|
||||
|
||||
info, err := localCrackFileInfo(file.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("localCrackFileInfo: %v", err)
|
||||
}
|
||||
if info.Size() != int64(len(payload)) {
|
||||
t.Fatalf("localCrackFileInfo size = %d, want %d", info.Size(), len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileCleansUpPostCreateFailures(t *testing.T) {
|
||||
readErr := errors.New("read failed")
|
||||
chunkErr := errors.New("chunk upload failed")
|
||||
completeErr := errors.New("complete failed")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reader io.Reader
|
||||
configureRPC func(*fakeCrackFileUploadRPC)
|
||||
wantErr error
|
||||
wantChunkCalls int
|
||||
wantCompleteCalls int
|
||||
}{
|
||||
{
|
||||
name: "read failure",
|
||||
reader: errorReader{err: readErr},
|
||||
wantErr: readErr,
|
||||
},
|
||||
{
|
||||
name: "chunk upload failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.chunkErr = chunkErr
|
||||
},
|
||||
wantErr: chunkErr,
|
||||
wantChunkCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "completion failure",
|
||||
reader: bytes.NewReader([]byte("payload")),
|
||||
configureRPC: func(rpc *fakeCrackFileUploadRPC) {
|
||||
rpc.completeErr = completeErr
|
||||
},
|
||||
wantErr: completeErr,
|
||||
wantChunkCalls: 1,
|
||||
wantCompleteCalls: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
if test.configureRPC != nil {
|
||||
test.configureRPC(rpc)
|
||||
}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(test.reader, reservation, rpc, nil)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped %v", err, test.wantErr)
|
||||
}
|
||||
if rpc.chunkCalls != test.wantChunkCalls {
|
||||
t.Fatalf("CrackFileChunkUpload calls = %d, want %d", rpc.chunkCalls, test.wantChunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != test.wantCompleteCalls {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want %d", rpc.completeCalls, test.wantCompleteCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
if rpc.deleted == nil || rpc.deleted.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileDelete request = %#v, want ID %q", rpc.deleted, reservation.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileReportsCleanupFailure(t *testing.T) {
|
||||
uploadErr := errors.New("chunk upload failed")
|
||||
cleanupErr := errors.New("delete failed")
|
||||
rpc := &fakeCrackFileUploadRPC{chunkErr: uploadErr, deleteErr: cleanupErr}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 1024}
|
||||
|
||||
_, err := uploadCrackFile(bytes.NewReader([]byte("payload")), reservation, rpc, nil)
|
||||
if !errors.Is(err, uploadErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped upload error %v", err, uploadErr)
|
||||
}
|
||||
if !errors.Is(err, cleanupErr) {
|
||||
t.Fatalf("uploadCrackFile error = %v, want wrapped cleanup error %v", err, cleanupErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), reservation.ID) {
|
||||
t.Fatalf("uploadCrackFile error = %q, want reservation ID", err)
|
||||
}
|
||||
if rpc.deleteCalls != 1 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 1", rpc.deleteCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadCrackFileSuccessCompletesWithoutCleanup(t *testing.T) {
|
||||
payload := []byte("correct horse battery staple\n")
|
||||
rpc := &fakeCrackFileUploadRPC{}
|
||||
reservation := &clientpb.CrackFile{ID: "reserved-file", ChunkSize: 8}
|
||||
progressCalls := 0
|
||||
|
||||
total, err := uploadCrackFile(bytes.NewReader(payload), reservation, rpc, func(int64, uint32, int) {
|
||||
progressCalls++
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("uploadCrackFile: %v", err)
|
||||
}
|
||||
if total == 0 {
|
||||
t.Fatal("uploadCrackFile compressed byte count = 0, want non-zero")
|
||||
}
|
||||
if rpc.chunkCalls == 0 {
|
||||
t.Fatal("CrackFileChunkUpload calls = 0, want at least 1")
|
||||
}
|
||||
if progressCalls != rpc.chunkCalls {
|
||||
t.Fatalf("progress calls = %d, want %d", progressCalls, rpc.chunkCalls)
|
||||
}
|
||||
if rpc.completeCalls != 1 {
|
||||
t.Fatalf("CrackFileComplete calls = %d, want 1", rpc.completeCalls)
|
||||
}
|
||||
if rpc.deleteCalls != 0 {
|
||||
t.Fatalf("CrackFileDelete calls = %d, want 0", rpc.deleteCalls)
|
||||
}
|
||||
wantDigest := sha256.Sum256(payload)
|
||||
if rpc.completed == nil || rpc.completed.ID != reservation.ID {
|
||||
t.Fatalf("CrackFileComplete request = %#v, want ID %q", rpc.completed, reservation.ID)
|
||||
}
|
||||
if rpc.completed.Sha2_256 != hex.EncodeToString(wantDigest[:]) {
|
||||
t.Fatalf("CrackFileComplete digest = %q, want %q", rpc.completed.Sha2_256, hex.EncodeToString(wantDigest[:]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderZstdRoundTrip(t *testing.T) {
|
||||
largePayload := make([]byte, 256*1024)
|
||||
if _, err := rand.New(rand.NewSource(42)).Read(largePayload); err != nil {
|
||||
t.Fatalf("create test payload: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
payload []byte
|
||||
chunkSize int64
|
||||
wantMinChunks int
|
||||
}{
|
||||
{
|
||||
name: "single chunk",
|
||||
payload: []byte("correct horse battery staple\n"),
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 1,
|
||||
},
|
||||
{
|
||||
name: "many chunks from one read",
|
||||
payload: largePayload,
|
||||
chunkSize: 1024,
|
||||
wantMinChunks: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader(test.payload), test.chunkSize)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
if chunkCount < test.wantMinChunks {
|
||||
t.Fatalf("chunk count = %d, want at least %d", chunkCount, test.wantMinChunks)
|
||||
}
|
||||
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, test.payload) {
|
||||
t.Fatalf("decoded payload does not match input: got %d bytes, want %d", len(decoded), len(test.payload))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderHandlesDataWithEOF(t *testing.T) {
|
||||
payload := []byte("reader returns data and EOF together")
|
||||
compressed, _, err := collectCompressedCrackFile(&dataEOFReader{data: payload}, 8)
|
||||
if err != nil {
|
||||
t.Fatalf("chunkReader: %v", err)
|
||||
}
|
||||
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
|
||||
if err != nil {
|
||||
t.Fatalf("open zstd stream: %v", err)
|
||||
}
|
||||
decoded, err := io.ReadAll(decoder)
|
||||
decoder.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode zstd stream: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decoded, payload) {
|
||||
t.Fatalf("decoded payload = %q, want %q", decoded, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderRejectsInvalidChunkSize(t *testing.T) {
|
||||
for _, chunkSize := range []int64{0, -1} {
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(bytes.NewReader([]byte("payload")), chunkSize)
|
||||
if err == nil {
|
||||
t.Fatalf("chunkReader accepted chunk size %d", chunkSize)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("invalid chunk size %d emitted %d chunks (%d bytes)", chunkSize, chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkReaderPropagatesReadError(t *testing.T) {
|
||||
wantErr := errors.New("read failed")
|
||||
compressed, chunkCount, err := collectCompressedCrackFile(errorReader{err: wantErr}, 1024)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("chunkReader error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if chunkCount != 0 || len(compressed) != 0 {
|
||||
t.Fatalf("failed read emitted %d chunks (%d bytes)", chunkCount, len(compressed))
|
||||
}
|
||||
}
|
||||
|
||||
func collectCompressedCrackFile(reader io.Reader, chunkSize int64) ([]byte, int, error) {
|
||||
chunks, errCh := readCrackFileChunks(reader, chunkSize)
|
||||
|
||||
compressed := bytes.Buffer{}
|
||||
chunkCount := 0
|
||||
invalidChunk := false
|
||||
for chunk := range chunks {
|
||||
if len(chunk) == 0 || int64(len(chunk)) > chunkSize {
|
||||
invalidChunk = true
|
||||
}
|
||||
_, _ = compressed.Write(chunk)
|
||||
chunkCount++
|
||||
}
|
||||
readerErr := <-errCh
|
||||
if invalidChunk {
|
||||
return nil, chunkCount, errors.New("chunkReader emitted an invalid chunk size")
|
||||
}
|
||||
return compressed.Bytes(), chunkCount, readerErr
|
||||
}
|
||||
|
||||
type dataEOFReader struct {
|
||||
data []byte
|
||||
done bool
|
||||
}
|
||||
|
||||
func (r *dataEOFReader) Read(buffer []byte) (int, error) {
|
||||
if r.done {
|
||||
return 0, io.EOF
|
||||
}
|
||||
r.done = true
|
||||
return copy(buffer, r.data), io.EOF
|
||||
}
|
||||
|
||||
type errorReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r errorReader) Read([]byte) (int, error) {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
type fakeCrackFileUploadRPC struct {
|
||||
rpcpb.SliverRPCClient
|
||||
|
||||
chunkErr error
|
||||
completeErr error
|
||||
deleteErr error
|
||||
|
||||
chunkCalls int
|
||||
completeCalls int
|
||||
deleteCalls int
|
||||
|
||||
uploaded []*clientpb.CrackFileChunk
|
||||
completed *clientpb.CrackFile
|
||||
deleted *clientpb.CrackFile
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileChunkUpload(_ context.Context, request *clientpb.CrackFileChunk, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.chunkCalls++
|
||||
f.uploaded = append(f.uploaded, &clientpb.CrackFileChunk{
|
||||
CrackFileID: request.CrackFileID,
|
||||
N: request.N,
|
||||
Data: append([]byte(nil), request.Data...),
|
||||
})
|
||||
if f.chunkErr != nil {
|
||||
return nil, f.chunkErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileComplete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.completeCalls++
|
||||
f.completed = &clientpb.CrackFile{ID: request.ID, Sha2_256: request.Sha2_256}
|
||||
if f.completeErr != nil {
|
||||
return nil, f.completeErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeCrackFileUploadRPC) CrackFileDelete(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*commonpb.Empty, error) {
|
||||
f.deleteCalls++
|
||||
f.deleted = &clientpb.CrackFile{ID: request.ID}
|
||||
if f.deleteErr != nil {
|
||||
return nil, f.deleteErr
|
||||
}
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package crack
|
||||
|
||||
import "github.com/spf13/pflag"
|
||||
|
||||
func bindCrackFlags(f *pflag.FlagSet) {
|
||||
f.StringP("attack-mode", "a", "", "crack attack mode (straight, combination, bruteforce, pcfg, hybrid-wordlist-mask, hybrid-mask-wordlist, generic, association, no-attack, hybrid, or numeric)")
|
||||
f.StringP("hash-type", "m", "", "hashcat hash type (number or enum name)")
|
||||
f.Uint32("hash-mode", 0, "canonical numeric hashcat mode (overrides hash-type)")
|
||||
f.StringArray("hash", nil, "hashes to crack (repeatable)")
|
||||
f.StringArray("credential", nil, "server credential ID or ID prefix to crack (repeatable)")
|
||||
f.String("credential-collection", "", "server credential collection to crack")
|
||||
f.Bool("include-cracked-credentials", false, "include credentials already marked as cracked")
|
||||
f.StringArray("input", nil, "ordered positional hashcat inputs (repeatable)")
|
||||
f.String("stdin", "", "data supplied to hashcat standard input")
|
||||
f.Bool("quiet", false, "quiet mode")
|
||||
f.Bool("hex-charset", false, "treat charset as hex")
|
||||
f.Bool("hex-salt", false, "treat salt as hex")
|
||||
f.Bool("hex-wordlist", false, "treat wordlist as hex")
|
||||
f.Bool("force", false, "ignore warnings")
|
||||
f.Bool("deprecated-check-disable", false, "disable deprecated check")
|
||||
f.Bool("advice-disable", false, "disable hashcat advice")
|
||||
f.Bool("status", false, "enable status")
|
||||
f.Bool("status-json", false, "emit status in JSON")
|
||||
f.Uint32("status-timer", 0, "status timer interval (seconds)")
|
||||
f.Uint32("stdin-timeout-abort", 0, "abort after stdin timeout (seconds)")
|
||||
f.Bool("machine-readable", false, "machine readable output")
|
||||
f.Bool("keep-guessing", false, "keep guessing")
|
||||
f.Bool("self-test-disable", false, "disable self-test")
|
||||
f.Bool("loopback", false, "use loopback mode")
|
||||
f.String("markov-hcstat2", "", "markov hcstat2 filename or identifier")
|
||||
f.Bool("markov-disable", false, "disable markov mode")
|
||||
f.Bool("markov-classic", false, "use classic markov")
|
||||
f.Bool("markov-inverse", false, "use inverse markov")
|
||||
f.Uint32("markov-threshold", 0, "markov threshold")
|
||||
f.Uint32("runtime", 0, "runtime in seconds")
|
||||
f.String("session", "", "session name")
|
||||
f.Bool("restore", false, "restore session")
|
||||
f.Bool("restore-disable", false, "disable restore")
|
||||
f.String("restore-file", "", "restore file path or identifier")
|
||||
f.Bool("restore-position", false, "restore from the stored position")
|
||||
f.Bool("restore-show-command", false, "display the stored restore command and stop")
|
||||
f.String("outfile", "", "outfile path or identifier")
|
||||
f.StringSlice("outfile-format", nil, "outfile formats (hash-salt, plain, hex-plain, crack-pos, timestamp-absolute, timestamp-relative, or numeric)")
|
||||
f.Bool("outfile-json", false, "write outfile records as JSON")
|
||||
f.Bool("outfile-autohex-disable", false, "disable outfile autohex")
|
||||
f.Uint32("outfile-check-timer", 0, "outfile check timer interval (seconds)")
|
||||
f.Bool("wordlist-autohex-disable", false, "disable wordlist autohex")
|
||||
f.String("separator", "", "separator character")
|
||||
f.Bool("show", false, "show cracked hashes")
|
||||
f.Bool("left", false, "show uncracked hashes")
|
||||
f.Bool("username", false, "enable username handling")
|
||||
f.Bool("remove", false, "remove cracked hashes")
|
||||
f.Uint32("remove-timer", 0, "remove timer interval (seconds)")
|
||||
f.Bool("potfile-disable", false, "disable potfile")
|
||||
f.String("potfile", "", "potfile path or identifier")
|
||||
f.String("encoding-from", "", "input encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.String("encoding-to", "", "output encoding name (legacy enum names and numbers are also accepted)")
|
||||
f.Uint32("debug-mode", 0, "debug mode mask")
|
||||
f.String("debug-file", "", "debug file path or identifier")
|
||||
f.String("induction-dir", "", "induction directory path or identifier")
|
||||
f.String("outfile-check-dir", "", "outfile-check directory path or identifier")
|
||||
f.Bool("logfile-disable", false, "disable logfile")
|
||||
f.Uint32("hccapx-message-pair", 0, "hccapx message pair")
|
||||
f.Uint32("nonce-error-corrections", 0, "nonce error corrections")
|
||||
f.String("keyboard-layout-mapping", "", "local keyboard layout mapping file")
|
||||
f.String("truecrypt-keyfiles", "", "TrueCrypt keyfiles path or identifier")
|
||||
f.String("veracrypt-keyfiles", "", "VeraCrypt keyfiles path or identifier")
|
||||
f.Uint32("veracrypt-pim-start", 0, "VeraCrypt PIM start")
|
||||
f.Uint32("veracrypt-pim-stop", 0, "VeraCrypt PIM stop")
|
||||
f.Bool("benchmark", false, "run benchmark")
|
||||
f.Bool("benchmark-all", false, "run full benchmark")
|
||||
f.Uint32("benchmark-min", 0, "minimum benchmark runtime")
|
||||
f.Uint32("benchmark-max", 0, "maximum benchmark runtime")
|
||||
f.Bool("speed-only", false, "show speed only")
|
||||
f.Bool("progress-only", false, "show progress only")
|
||||
f.Uint32("segment-size", 0, "deprecated: removed in Hashcat v7")
|
||||
f.Bool("pipeline-stats", false, "show pipeline statistics")
|
||||
f.Bool("task-time-breakdown", false, "show task time breakdown")
|
||||
f.Uint32("bitmap-min", 0, "bitmap min")
|
||||
f.Uint32("bitmap-max", 0, "bitmap max")
|
||||
f.UintSlice("cpu-affinity", nil, "CPU affinity list")
|
||||
f.Uint32("hook-threads", 0, "hook threads")
|
||||
f.Bool("hash-info", false, "show hash information from an idle crackstation (does not create a job)")
|
||||
f.Uint32("hash-info-level", 0, "hash information detail level: 1 or 2 (does not create a job)")
|
||||
f.Bool("backend-ignore-cuda", false, "ignore CUDA backend")
|
||||
f.Bool("backend-ignore-hip", false, "ignore HIP backend")
|
||||
f.Bool("backend-ignore-metal", false, "ignore Metal backend")
|
||||
f.Bool("backend-ignore-opencl", false, "ignore OpenCL backend")
|
||||
f.Bool("backend-info", false, "show registered backend info for all connected crackstations (does not create a job)")
|
||||
f.Uint32("backend-info-level", 0, "registered backend information detail level: 1 or 2 (does not create a job)")
|
||||
f.UintSlice("backend-devices", nil, "backend device IDs")
|
||||
f.Uint32("backend-devices-virtmulti", 0, "virtual backend device multiplier")
|
||||
f.Uint32("backend-devices-virthost", 0, "virtual backend host count")
|
||||
f.UintSlice("opencl-device-types", nil, "OpenCL device types")
|
||||
f.Uint32("metal-compiler-runtime", 0, "Metal compiler runtime")
|
||||
f.Bool("optimized-kernel-enable", false, "enable optimized kernels")
|
||||
f.Bool("multiply-accel-disable", false, "disable multiply accel")
|
||||
f.Bool("multiply-accel-disabled", false, "deprecated alias for multiply-accel-disable")
|
||||
_ = f.MarkDeprecated("multiply-accel-disabled", "use --multiply-accel-disable")
|
||||
_ = f.MarkHidden("multiply-accel-disabled")
|
||||
f.String("workload-profile", "", "deprecated: accepted but ignored by Hashcat v7")
|
||||
f.Uint32("kernel-accel", 0, "kernel acceleration")
|
||||
f.Uint32("kernel-loops", 0, "kernel loops")
|
||||
f.Uint32("kernel-threads", 0, "kernel threads")
|
||||
f.Uint32("backend-vector-width", 0, "backend vector width")
|
||||
f.Uint32("spin-damp", 0, "spin damp")
|
||||
f.Bool("hwmon-disable", false, "disable hardware monitoring")
|
||||
f.Uint32("hwmon-temp-abort", 0, "hardware monitor temperature abort")
|
||||
f.Uint32("scrypt-tmto", 0, "scrypt tmto")
|
||||
f.Uint64("skip", 0, "skip count")
|
||||
f.Uint64("limit", 0, "limit count")
|
||||
f.Bool("keyspace", false, "calculate keyspace on an idle crackstation and return it (does not create a job)")
|
||||
f.Bool("total-candidates", false, "calculate the total candidate count on an idle crackstation (does not create a job)")
|
||||
f.String("lookup", "", "locate a candidate in the attack on an idle crackstation (does not create a job)")
|
||||
f.String("crackstation", "", "target an exact crackstation name or host UUID for a synchronous query")
|
||||
f.Bool("dynamic-x", false, "enable dynamic-X mode")
|
||||
f.String("seekdb-path", "", "seek database path or identifier")
|
||||
f.String("rule-left", "", "left combinator rule")
|
||||
f.String("rule-right", "", "right combinator rule")
|
||||
f.StringArray("rules-file", nil, "rules file content or identifier (repeatable)")
|
||||
f.Uint32("generate-rules", 0, "generate rules")
|
||||
f.Uint32("generate-rules-func-min", 0, "generate rules function min")
|
||||
f.Uint32("generate-rules-func-max", 0, "generate rules function max")
|
||||
f.Uint32("generate-rules-fun-min", 0, "deprecated alias for generate-rules-func-min")
|
||||
f.Uint32("generate-rules-fun-max", 0, "deprecated alias for generate-rules-func-max")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-min", "use --generate-rules-func-min")
|
||||
_ = f.MarkHidden("generate-rules-fun-min")
|
||||
_ = f.MarkDeprecated("generate-rules-fun-max", "use --generate-rules-func-max")
|
||||
_ = f.MarkHidden("generate-rules-fun-max")
|
||||
f.String("generate-rules-func-sel", "", "generate rules function selection")
|
||||
f.Int32("generate-rules-seed", 0, "generate rules seed")
|
||||
f.String("custom-charset1", "", "custom charset 1")
|
||||
f.String("custom-charset2", "", "custom charset 2")
|
||||
f.String("custom-charset3", "", "custom charset 3")
|
||||
f.String("custom-charset4", "", "custom charset 4")
|
||||
f.String("custom-charset5", "", "custom charset 5")
|
||||
f.String("custom-charset6", "", "custom charset 6")
|
||||
f.String("custom-charset7", "", "custom charset 7")
|
||||
f.String("custom-charset8", "", "custom charset 8")
|
||||
f.String("identify", "", "deprecated: legacy positional input")
|
||||
f.Bool("identify-mode", false, "identify hash types on an idle crackstation (does not create a job)")
|
||||
f.Bool("increment", false, "incremental mode")
|
||||
f.Bool("increment-inverse", false, "inverse incremental mode")
|
||||
f.Uint32("increment-min", 0, "increment min")
|
||||
f.Uint32("increment-max", 0, "increment max")
|
||||
f.Uint32("bypass-delay", 0, "bypass delay")
|
||||
f.Uint32("bypass-threshold", 0, "bypass threshold")
|
||||
f.Bool("slow-candidates", false, "slow candidates")
|
||||
f.String("bridge-parameter1", "", "bridge parameter 1")
|
||||
f.String("bridge-parameter2", "", "bridge parameter 2")
|
||||
f.String("bridge-parameter3", "", "bridge parameter 3")
|
||||
f.String("bridge-parameter4", "", "bridge parameter 4")
|
||||
f.Bool("brain-server", false, "brain server mode")
|
||||
f.Uint32("brain-server-timer", 0, "brain server timer")
|
||||
f.Bool("brain-client", false, "brain client mode")
|
||||
f.String("brain-client-features", "", "brain client features")
|
||||
f.String("brain-host", "", "brain host")
|
||||
f.Uint32("brain-port", 0, "brain port")
|
||||
f.String("brain-password", "", "brain password")
|
||||
f.String("brain-session", "", "brain session")
|
||||
f.String("brain-session-whitelist", "", "brain session whitelist")
|
||||
f.Bool("brain-feed", false, "brain feed mode")
|
||||
f.Bool("color-cracked", false, "color cracked output")
|
||||
f.Bool("hash-copy", false, "copy hashes")
|
||||
f.String("encrypt-with-pubkey", "", "public key path or identifier used to encrypt output")
|
||||
}
|
||||
@@ -0,0 +1,988 @@
|
||||
package crack
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2026 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
type crackJobFetcher func(context.Context, string) (*clientpb.CrackJob, error)
|
||||
|
||||
var errNoCrackJobs = errors.New("no crack jobs")
|
||||
|
||||
// CrackJobsCmd lists all durable cracking jobs known to the server.
|
||||
//
|
||||
//nolint:revive // Keep the established exported command-handler name for compatibility.
|
||||
func CrackJobsCmd(cmd *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
|
||||
jobs, err := con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||
con.PrintInfof("No crack jobs\n")
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", renderCrackJobs(jobs.Jobs, settings.GetTableStyle(con)))
|
||||
}
|
||||
|
||||
// CrackJobCmd displays one cracking job, optionally polling until completion.
|
||||
//
|
||||
//nolint:revive // Keep the established exported command-handler name for compatibility.
|
||||
func CrackJobCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
pollInterval, _ := cmd.Flags().GetDuration("poll-interval")
|
||||
if pollInterval <= 0 {
|
||||
con.PrintErrorf("--poll-interval must be greater than zero\n")
|
||||
return
|
||||
}
|
||||
jobID, err := resolveCrackJobID(args, func() (*clientpb.CrackJobs, error) {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
return con.Rpc.CrackJobs(ctx, &commonpb.Empty{})
|
||||
}, forms.CrackJobSelectForm)
|
||||
if errors.Is(err, errNoCrackJobs) {
|
||||
con.PrintInfof("No crack jobs\n")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
fetch := func(parent context.Context, id string) (*clientpb.CrackJob, error) {
|
||||
ctx := parent
|
||||
cancel := func() {}
|
||||
if timeoutSeconds > 0 {
|
||||
ctx, cancel = context.WithTimeout(parent, time.Duration(timeoutSeconds)*time.Second)
|
||||
}
|
||||
defer cancel()
|
||||
return con.Rpc.CrackJobByID(ctx, &clientpb.CrackJob{ID: id})
|
||||
}
|
||||
|
||||
watch, _ := cmd.Flags().GetBool("watch")
|
||||
if !watch {
|
||||
job, err := fetch(cmd.Context(), jobID)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
con.Printf("%s\n", renderCrackJob(job, settings.GetTableStyle(con)))
|
||||
return
|
||||
}
|
||||
|
||||
first := true
|
||||
err = watchCrackJob(cmd.Context(), jobID, pollInterval, fetch, func(job *clientpb.CrackJob) {
|
||||
if !first {
|
||||
con.Println()
|
||||
}
|
||||
first = false
|
||||
con.Printf("%s\n", renderCrackJob(job, settings.GetTableStyle(con)))
|
||||
})
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCrackJobID(
|
||||
args []string,
|
||||
listJobs func() (*clientpb.CrackJobs, error),
|
||||
selectJob func([]*clientpb.CrackJob) (string, error),
|
||||
) (string, error) {
|
||||
if len(args) > 1 {
|
||||
return "", errors.New("only one crack job ID may be specified")
|
||||
}
|
||||
if len(args) == 1 {
|
||||
jobID := strings.TrimSpace(args[0])
|
||||
if jobID == "" {
|
||||
return "", errors.New("job ID cannot be empty")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
if listJobs == nil {
|
||||
return "", errors.New("crack job lister is nil")
|
||||
}
|
||||
|
||||
jobs, err := listJobs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if jobs == nil || !hasSelectableCrackJob(jobs.GetJobs()) {
|
||||
return "", errNoCrackJobs
|
||||
}
|
||||
if selectJob == nil {
|
||||
return "", errors.New("crack job selector is nil")
|
||||
}
|
||||
jobID, err := selectJob(jobs.GetJobs())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
jobID = strings.TrimSpace(jobID)
|
||||
if jobID == "" {
|
||||
return "", errors.New("no crack job selected")
|
||||
}
|
||||
return jobID, nil
|
||||
}
|
||||
|
||||
func hasSelectableCrackJob(jobs []*clientpb.CrackJob) bool {
|
||||
for _, job := range jobs {
|
||||
if job != nil && strings.TrimSpace(job.GetID()) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func crackCommandContext(parent context.Context, cmd *cobra.Command) (context.Context, context.CancelFunc) {
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
timeoutSeconds, _ := cmd.Flags().GetInt64("timeout")
|
||||
if timeoutSeconds <= 0 {
|
||||
return parent, func() {}
|
||||
}
|
||||
return context.WithTimeout(parent, time.Duration(timeoutSeconds)*time.Second)
|
||||
}
|
||||
|
||||
func watchCrackJob(ctx context.Context, jobID string, pollInterval time.Duration, fetch crackJobFetcher, onUpdate func(*clientpb.CrackJob)) error {
|
||||
if pollInterval <= 0 {
|
||||
return errors.New("poll interval must be greater than zero")
|
||||
}
|
||||
if fetch == nil {
|
||||
return errors.New("job fetcher is nil")
|
||||
}
|
||||
|
||||
var previous []byte
|
||||
for {
|
||||
job, err := fetch(ctx, jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job == nil {
|
||||
return errors.New("server returned an empty crack job")
|
||||
}
|
||||
serialized, err := proto.MarshalOptions{Deterministic: true}.Marshal(job)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode crack job: %w", err)
|
||||
}
|
||||
if !bytes.Equal(serialized, previous) {
|
||||
if onUpdate != nil {
|
||||
onUpdate(job)
|
||||
}
|
||||
previous = serialized
|
||||
}
|
||||
if crackJobTerminal(job) {
|
||||
return nil
|
||||
}
|
||||
|
||||
timer := time.NewTimer(pollInterval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func crackJobTerminal(job *clientpb.CrackJob) bool {
|
||||
if job == nil {
|
||||
return false
|
||||
}
|
||||
status := strings.ToUpper(job.Status.String())
|
||||
return strings.HasSuffix(status, "COMPLETED") || strings.HasSuffix(status, "FAILED") || strings.HasSuffix(status, "CANCELLED")
|
||||
}
|
||||
|
||||
func crackJobStatusStyle(status string) console.TextStyle {
|
||||
status = strings.ToUpper(strings.TrimSpace(status))
|
||||
switch {
|
||||
case strings.HasSuffix(status, "COMPLETED"):
|
||||
return console.StyleBoldSuccess
|
||||
case strings.HasSuffix(status, "FAILED"):
|
||||
return console.StyleBoldDanger
|
||||
case strings.HasSuffix(status, "CANCELLED"), strings.HasSuffix(status, "CANCELED"):
|
||||
return console.StyleBoldGray
|
||||
case status == "IN_PROGRESS", strings.HasSuffix(status, "RUNNING"), strings.HasSuffix(status, "LEASED"):
|
||||
return console.StyleBoldWarning
|
||||
case strings.HasSuffix(status, "QUEUED"), strings.HasSuffix(status, "PENDING"):
|
||||
return console.StyleBold
|
||||
default:
|
||||
return console.StyleBold
|
||||
}
|
||||
}
|
||||
|
||||
func renderCrackJobStatus(status string) string {
|
||||
status = safeCrackCell(status)
|
||||
return crackJobStatusStyle(status).Render(status)
|
||||
}
|
||||
|
||||
func renderCrackJobTitle(title string) string {
|
||||
return console.StyleBoldPrimary.Render(title)
|
||||
}
|
||||
|
||||
func renderCrackJobs(jobs []*clientpb.CrackJob, style table.Style) string {
|
||||
ordered := append([]*clientpb.CrackJob(nil), jobs...)
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
return crackJobSortTime(ordered[i]).After(crackJobSortTime(ordered[j]))
|
||||
})
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(style)
|
||||
tw.AppendHeader(table.Row{"ID", "Status", "Created", "Updated", "Keyspace", "Tasks", "Results"})
|
||||
for _, job := range ordered {
|
||||
if job == nil {
|
||||
continue
|
||||
}
|
||||
tw.AppendRow(table.Row{
|
||||
console.StyleBoldPrimary.Render(safeCrackCell(job.ID)),
|
||||
renderCrackJobStatus(job.Status.String()),
|
||||
formatCrackTime(job.CreatedAt),
|
||||
formatCrackTime(protoFieldText(job.ProtoReflect(), "UpdatedAt")),
|
||||
valueOrDash(protoFieldText(job.ProtoReflect(), "Keyspace")),
|
||||
strconv.Itoa(len(crackJobTasks(job))),
|
||||
crackJobResultCount(job),
|
||||
})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Keep the summary, task, device, and result sections in one deterministic job rendering pass.
|
||||
func renderCrackJob(job *clientpb.CrackJob, style table.Style) string {
|
||||
if job == nil {
|
||||
return "Empty crack job"
|
||||
}
|
||||
|
||||
sections := make([]string, 0, 4)
|
||||
summary := table.NewWriter()
|
||||
summary.SetStyle(style)
|
||||
summary.SetTitle(renderCrackJobTitle("Crack Job"))
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("ID"), console.StyleBoldPrimary.Render(safeCrackCell(job.ID))})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Status"), renderCrackJobStatus(job.Status.String())})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Created"), formatCrackTime(job.CreatedAt)})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Updated"), formatCrackTime(protoFieldText(job.ProtoReflect(), "UpdatedAt"))})
|
||||
summary.AppendRow(table.Row{console.StyleBold.Render("Keyspace"), valueOrDash(protoFieldText(job.ProtoReflect(), "Keyspace"))})
|
||||
if job.Err != "" {
|
||||
summary.AppendRow(table.Row{console.StyleBoldDanger.Render("Error"), console.StyleDanger.Render(safeCrackCell(job.Err))})
|
||||
}
|
||||
sections = append(sections, summary.Render())
|
||||
|
||||
tasks := crackJobTasks(job)
|
||||
if len(tasks) > 0 {
|
||||
taskTable := table.NewWriter()
|
||||
taskTable.SetStyle(style)
|
||||
taskTable.SetTitle(renderCrackJobTitle("Tasks"))
|
||||
taskTable.AppendHeader(table.Row{"ID", "Kind", "State", "Station", "Range", "Progress", "Speed", "Temp", "Updated"})
|
||||
devices := make([]crackDeviceView, 0)
|
||||
for _, task := range tasks {
|
||||
status, statusErr := crackTaskStatus(task)
|
||||
progress, speed, temperature := "-", "-", "-"
|
||||
if statusErr != nil {
|
||||
progress = console.StyleDanger.Render("invalid status JSON")
|
||||
} else if status != nil {
|
||||
progress = status.progressText()
|
||||
speed = status.speedText()
|
||||
temperature = status.temperatureText()
|
||||
for _, device := range status.Devices {
|
||||
device.TaskID = task.ID
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
taskTable.AppendRow(table.Row{
|
||||
safeCrackCell(task.ID),
|
||||
valueOrDash(protoFieldText(task.ProtoReflect(), "Kind")),
|
||||
renderCrackJobStatus(crackTaskState(task)),
|
||||
valueOrDash(task.HostUUID),
|
||||
crackTaskRange(task),
|
||||
progress,
|
||||
speed,
|
||||
temperature,
|
||||
formatCrackTime(protoFieldText(task.ProtoReflect(), "UpdatedAt", "LastHeartbeatAt")),
|
||||
})
|
||||
}
|
||||
sections = append(sections, taskTable.Render())
|
||||
|
||||
statisticsTable := table.NewWriter()
|
||||
statisticsTable.SetStyle(style)
|
||||
statisticsTable.SetTitle(renderCrackJobTitle("Hashcat Statistics"))
|
||||
statisticsTable.AppendHeader(table.Row{
|
||||
"Task", "Session", "Status", "Target", "Guess", "Progress", "Rejected", "Restore",
|
||||
"Recovered Hashes", "Recovered Salts", "Started", "Estimated Stop",
|
||||
})
|
||||
hasStatistics := false
|
||||
for _, task := range tasks {
|
||||
status, err := crackTaskStatus(task)
|
||||
if err != nil || status == nil {
|
||||
continue
|
||||
}
|
||||
hasStatistics = true
|
||||
statisticsTable.AppendRow(table.Row{
|
||||
safeCrackCell(task.ID),
|
||||
valueOrDash(status.Session),
|
||||
valueOrDash(status.Status),
|
||||
valueOrDash(status.Target),
|
||||
status.guessText(),
|
||||
status.progressText(),
|
||||
valueOrDash(status.Rejected),
|
||||
valueOrDash(status.RestorePoint),
|
||||
pairText(status.RecoveredHashesCurrent, status.RecoveredHashesTotal),
|
||||
pairText(status.RecoveredSaltsCurrent, status.RecoveredSaltsTotal),
|
||||
formatCrackTime(status.TimeStart),
|
||||
formatCrackTime(status.EstimatedStop),
|
||||
})
|
||||
}
|
||||
if hasStatistics {
|
||||
sections = append(sections, statisticsTable.Render())
|
||||
}
|
||||
|
||||
if len(devices) > 0 {
|
||||
deviceTable := table.NewWriter()
|
||||
deviceTable.SetStyle(style)
|
||||
deviceTable.SetTitle(renderCrackJobTitle("Devices"))
|
||||
deviceTable.AppendHeader(table.Row{"Task", "ID", "Name", "Type", "Speed", "Temp", "Util", "Fan", "Core", "Memory", "Bus", "Power"})
|
||||
for _, device := range devices {
|
||||
deviceTable.AppendRow(table.Row{
|
||||
safeCrackCell(device.TaskID),
|
||||
valueOrDash(device.ID),
|
||||
valueOrDash(device.Name),
|
||||
valueOrDash(device.Type),
|
||||
device.speedText(),
|
||||
unitValue(device.Temperature, " C"),
|
||||
unitValue(device.Utilization, "%"),
|
||||
unitValue(device.FanSpeed, "%"),
|
||||
unitValue(device.CoreSpeed, " MHz"),
|
||||
unitValue(device.MemorySpeed, " MHz"),
|
||||
unitValue(device.BusLanes, " lanes"),
|
||||
unitValue(device.Power, " mW"),
|
||||
})
|
||||
}
|
||||
sections = append(sections, deviceTable.Render())
|
||||
}
|
||||
}
|
||||
|
||||
results := crackJobResults(job)
|
||||
if len(results) > 0 {
|
||||
resultTable := table.NewWriter()
|
||||
resultTable.SetStyle(style)
|
||||
resultTable.SetTitle(console.StyleBoldSuccess.Render("Recovered Credentials"))
|
||||
resultTable.AppendHeader(table.Row{"Credential", "Hash", "Plaintext"})
|
||||
for _, result := range results {
|
||||
resultTable.AppendRow(table.Row{
|
||||
valueOrDash(result.CredentialID),
|
||||
valueOrDash(result.Hash),
|
||||
valueOrDash(result.Plaintext),
|
||||
})
|
||||
}
|
||||
sections = append(sections, resultTable.Render())
|
||||
}
|
||||
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func crackJobSortTime(job *clientpb.CrackJob) time.Time {
|
||||
if job == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
for _, value := range []string{protoFieldText(job.ProtoReflect(), "UpdatedAt"), job.CreatedAt} {
|
||||
if unix, err := strconv.ParseInt(value, 10, 64); err == nil && unix > 0 {
|
||||
return time.Unix(unix, 0)
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func crackJobTasks(job *clientpb.CrackJob) []*clientpb.CrackTask {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
list, ok := protoRepeatedMessages(job.ProtoReflect(), "Tasks")
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
tasks := make([]*clientpb.CrackTask, 0, list.Len())
|
||||
for index := 0; index < list.Len(); index++ {
|
||||
if task, ok := list.Get(index).Message().Interface().(*clientpb.CrackTask); ok {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func crackTaskState(task *clientpb.CrackTask) string {
|
||||
if state := protoFieldText(task.ProtoReflect(), "State"); state != "" {
|
||||
return safeCrackCell(state)
|
||||
}
|
||||
if task.Err != "" {
|
||||
return "FAILED"
|
||||
}
|
||||
if task.CompletedAt != 0 {
|
||||
return "COMPLETED"
|
||||
}
|
||||
if task.StartedAt != 0 {
|
||||
return "RUNNING"
|
||||
}
|
||||
return "QUEUED"
|
||||
}
|
||||
|
||||
func crackTaskRange(task *clientpb.CrackTask) string {
|
||||
skip := protoFieldText(task.ProtoReflect(), "ShardSkip")
|
||||
limit := protoFieldText(task.ProtoReflect(), "ShardLimit")
|
||||
if skip == "" && task.Command != nil {
|
||||
skip = strconv.FormatUint(task.Command.Skip, 10)
|
||||
}
|
||||
if limit == "" && task.Command != nil {
|
||||
limit = strconv.FormatUint(task.Command.Limit, 10)
|
||||
}
|
||||
if skip == "" {
|
||||
skip = "0"
|
||||
}
|
||||
if limit == "" || limit == "0" {
|
||||
return skip + "+"
|
||||
}
|
||||
return skip + "+" + limit
|
||||
}
|
||||
|
||||
func crackTaskStatus(task *clientpb.CrackTask) (*crackStatusView, error) {
|
||||
raw := protoFieldBytes(task.ProtoReflect(), "LatestStatusJSON", "RawStatusJSON", "StatusJSON")
|
||||
if len(bytes.TrimSpace(raw)) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return parseCrackStatusJSON(raw)
|
||||
}
|
||||
|
||||
type crackStatusView struct {
|
||||
Session string
|
||||
Status string
|
||||
Target string
|
||||
ProgressCurrent string
|
||||
ProgressTotal string
|
||||
RestorePoint string
|
||||
RecoveredHashesCurrent string
|
||||
RecoveredHashesTotal string
|
||||
RecoveredSaltsCurrent string
|
||||
RecoveredSaltsTotal string
|
||||
Rejected string
|
||||
TimeStart string
|
||||
EstimatedStop string
|
||||
Guess crackGuessView
|
||||
Devices []crackDeviceView
|
||||
}
|
||||
|
||||
type crackGuessView struct {
|
||||
Mode string
|
||||
Base string
|
||||
BaseOffset string
|
||||
BaseCount string
|
||||
BasePercent string
|
||||
MaskLength string
|
||||
Mod string
|
||||
ModOffset string
|
||||
ModCount string
|
||||
ModPercent string
|
||||
}
|
||||
|
||||
func (status *crackStatusView) progressText() string {
|
||||
if status == nil || status.ProgressCurrent == "" || status.ProgressTotal == "" {
|
||||
return "-"
|
||||
}
|
||||
current, currentErr := strconv.ParseFloat(status.ProgressCurrent, 64)
|
||||
total, totalErr := strconv.ParseFloat(status.ProgressTotal, 64)
|
||||
if currentErr == nil && totalErr == nil && total > 0 {
|
||||
return fmt.Sprintf("%s/%s (%.1f%%)", status.ProgressCurrent, status.ProgressTotal, math.Min(100, current/total*100))
|
||||
}
|
||||
return status.ProgressCurrent + "/" + status.ProgressTotal
|
||||
}
|
||||
|
||||
func (status *crackStatusView) speedText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
var total uint64
|
||||
for _, device := range status.Devices {
|
||||
speed, err := strconv.ParseUint(device.Speed, 10, 64)
|
||||
if err == nil {
|
||||
total += speed
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return "-"
|
||||
}
|
||||
return humanizeHashRate(total)
|
||||
}
|
||||
|
||||
func (status *crackStatusView) temperatureText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
var maximum float64
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
temperature, err := strconv.ParseFloat(device.Temperature, 64)
|
||||
if err == nil && temperature >= 0 && (!found || temperature > maximum) {
|
||||
maximum = temperature
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "-"
|
||||
}
|
||||
return fmt.Sprintf("%g C", maximum)
|
||||
}
|
||||
|
||||
func (status *crackStatusView) guessText() string {
|
||||
if status == nil {
|
||||
return "-"
|
||||
}
|
||||
parts := make([]string, 0, 3)
|
||||
if status.Guess.Mode != "" {
|
||||
parts = append(parts, "mode="+status.Guess.Mode)
|
||||
}
|
||||
if status.Guess.Base != "" {
|
||||
base := status.Guess.Base
|
||||
if status.Guess.BaseOffset != "" || status.Guess.BaseCount != "" {
|
||||
base += " [" + pairText(status.Guess.BaseOffset, status.Guess.BaseCount) + "]"
|
||||
}
|
||||
if status.Guess.BasePercent != "" {
|
||||
base += " (" + status.Guess.BasePercent + "%)"
|
||||
}
|
||||
parts = append(parts, "base="+base)
|
||||
}
|
||||
if status.Guess.MaskLength != "" {
|
||||
parts = append(parts, "mask-length="+status.Guess.MaskLength)
|
||||
}
|
||||
if status.Guess.Mod != "" {
|
||||
mod := status.Guess.Mod
|
||||
if status.Guess.ModOffset != "" || status.Guess.ModCount != "" {
|
||||
mod += " [" + pairText(status.Guess.ModOffset, status.Guess.ModCount) + "]"
|
||||
}
|
||||
if status.Guess.ModPercent != "" {
|
||||
mod += " (" + status.Guess.ModPercent + "%)"
|
||||
}
|
||||
parts = append(parts, "mod="+mod)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return safeCrackCell(strings.Join(parts, "; "))
|
||||
}
|
||||
|
||||
type crackDeviceView struct {
|
||||
TaskID string
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Speed string
|
||||
Temperature string
|
||||
Utilization string
|
||||
FanSpeed string
|
||||
CoreSpeed string
|
||||
MemorySpeed string
|
||||
BusLanes string
|
||||
Power string
|
||||
}
|
||||
|
||||
func (device crackDeviceView) speedText() string {
|
||||
speed, err := strconv.ParseUint(device.Speed, 10, 64)
|
||||
if err != nil || speed == 0 {
|
||||
return valueOrDash(device.Speed)
|
||||
}
|
||||
return humanizeHashRate(speed)
|
||||
}
|
||||
|
||||
func parseCrackStatusJSON(raw []byte) (*crackStatusView, error) {
|
||||
root, err := decodeLastJSONObject(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status := &crackStatusView{
|
||||
Session: jsonText(firstJSONValue(root, "session")),
|
||||
Status: jsonText(firstJSONValue(root, "status_string", "status")),
|
||||
Target: jsonText(firstJSONValue(root, "target")),
|
||||
RestorePoint: jsonText(firstJSONValue(root, "restore_point")),
|
||||
Rejected: jsonText(firstJSONValue(root, "rejected")),
|
||||
TimeStart: jsonText(firstJSONValue(root, "time_start")),
|
||||
EstimatedStop: jsonText(firstJSONValue(root, "estimated_stop")),
|
||||
}
|
||||
status.ProgressCurrent, status.ProgressTotal = jsonPair(firstJSONValue(root, "progress"))
|
||||
status.RecoveredHashesCurrent, status.RecoveredHashesTotal = jsonPair(firstJSONValue(root, "recovered_hashes"))
|
||||
status.RecoveredSaltsCurrent, status.RecoveredSaltsTotal = jsonPair(firstJSONValue(root, "recovered_salts"))
|
||||
if guess, ok := firstJSONValue(root, "guess").(map[string]any); ok {
|
||||
status.Guess = crackGuessView{
|
||||
Mode: jsonText(firstJSONValue(guess, "guess_mode", "mode")),
|
||||
Base: jsonText(firstJSONValue(guess, "guess_base", "base")),
|
||||
BaseOffset: jsonText(firstJSONValue(guess, "guess_base_offset", "base_offset")),
|
||||
BaseCount: jsonText(firstJSONValue(guess, "guess_base_count", "base_count")),
|
||||
BasePercent: jsonText(firstJSONValue(guess, "guess_base_percent", "base_percent")),
|
||||
MaskLength: jsonText(firstJSONValue(guess, "guess_mask_length", "mask_length")),
|
||||
Mod: jsonText(firstJSONValue(guess, "guess_mod", "mod")),
|
||||
ModOffset: jsonText(firstJSONValue(guess, "guess_mod_offset", "mod_offset")),
|
||||
ModCount: jsonText(firstJSONValue(guess, "guess_mod_count", "mod_count")),
|
||||
ModPercent: jsonText(firstJSONValue(guess, "guess_mod_percent", "mod_percent")),
|
||||
}
|
||||
}
|
||||
if devices, ok := firstJSONValue(root, "devices").([]any); ok {
|
||||
for _, item := range devices {
|
||||
values, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
status.Devices = append(status.Devices, crackDeviceView{
|
||||
ID: jsonText(firstJSONValue(values, "device_id", "id")),
|
||||
Name: safeCrackCell(jsonText(firstJSONValue(values, "device_name", "name"))),
|
||||
Type: safeCrackCell(jsonText(firstJSONValue(values, "device_type", "type"))),
|
||||
Speed: jsonText(firstJSONValue(values, "speed")),
|
||||
Temperature: jsonText(firstJSONValue(values, "temp", "temperature")),
|
||||
Utilization: jsonText(firstJSONValue(values, "util", "utilization")),
|
||||
FanSpeed: jsonText(firstJSONValue(values, "fanspeed", "fan_speed")),
|
||||
CoreSpeed: jsonText(firstJSONValue(values, "corespeed", "core_speed")),
|
||||
MemorySpeed: jsonText(firstJSONValue(values, "memoryspeed", "memory_speed")),
|
||||
BusLanes: jsonText(firstJSONValue(values, "buslanes", "bus_lanes")),
|
||||
Power: jsonText(firstJSONValue(values, "power")),
|
||||
})
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func decodeLastJSONObject(raw []byte) (map[string]any, error) {
|
||||
decode := func(candidate []byte) (map[string]any, error) {
|
||||
decoder := json.NewDecoder(bytes.NewReader(candidate))
|
||||
decoder.UseNumber()
|
||||
var last map[string]any
|
||||
for {
|
||||
value := map[string]any{}
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
break
|
||||
}
|
||||
last = value
|
||||
}
|
||||
if last == nil {
|
||||
return nil, errors.New("no JSON object")
|
||||
}
|
||||
return last, nil
|
||||
}
|
||||
trimmed := bytes.TrimSpace(raw)
|
||||
if value, err := decode(trimmed); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
lines := bytes.Split(trimmed, []byte{'\n'})
|
||||
for index := len(lines) - 1; index >= 0; index-- {
|
||||
candidate := bytes.TrimSpace(lines[index])
|
||||
if len(candidate) == 0 {
|
||||
continue
|
||||
}
|
||||
if value, err := decode(candidate); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("hashcat status did not contain a JSON object")
|
||||
}
|
||||
|
||||
type crackResultView struct {
|
||||
CredentialID string
|
||||
Hash string
|
||||
Plaintext string
|
||||
}
|
||||
|
||||
func crackJobResults(job *clientpb.CrackJob) []crackResultView {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
results := make([]crackResultView, 0)
|
||||
if list, ok := protoRepeatedMessages(job.ProtoReflect(), "Results"); ok {
|
||||
for index := 0; index < list.Len(); index++ {
|
||||
message := list.Get(index).Message()
|
||||
results = append(results, crackResultView{
|
||||
CredentialID: safeCrackCell(protoFieldText(message, "CredentialID")),
|
||||
Hash: safeCrackCell(protoFieldText(message, "Hash")),
|
||||
Plaintext: displayCrackPlaintext(protoFieldBytes(message, "Plaintext")),
|
||||
})
|
||||
}
|
||||
}
|
||||
// The job-level result rows are authoritative once the server has
|
||||
// materialized them. Fall back to worker JSON only while those rows have not
|
||||
// appeared yet, avoiding duplicate plaintexts in a watched job.
|
||||
if len(results) == 0 {
|
||||
for _, task := range crackJobTasks(job) {
|
||||
results = append(results, parseRecoveredResults(protoFieldBytes(task.ProtoReflect(), "RecoveredJSON"))...)
|
||||
}
|
||||
}
|
||||
|
||||
deduplicated := make([]crackResultView, 0, len(results))
|
||||
seen := map[string]struct{}{}
|
||||
for _, result := range results {
|
||||
key := result.CredentialID + "\x00" + result.Hash + "\x00" + result.Plaintext
|
||||
if _, ok := seen[key]; ok || (result.Hash == "" && result.Plaintext == "" && result.CredentialID == "") {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
deduplicated = append(deduplicated, result)
|
||||
}
|
||||
return deduplicated
|
||||
}
|
||||
|
||||
func crackJobResultCount(job *clientpb.CrackJob) string {
|
||||
if job == nil {
|
||||
return "-"
|
||||
}
|
||||
return valueOrDash(protoFieldText(job.ProtoReflect(), "ResultCount"))
|
||||
}
|
||||
|
||||
func displayCrackPlaintext(plaintext []byte) string {
|
||||
if !utf8.Valid(plaintext) || bytes.IndexByte(plaintext, 0) >= 0 {
|
||||
return "0x" + hex.EncodeToString(plaintext)
|
||||
}
|
||||
return safeCrackCell(string(plaintext))
|
||||
}
|
||||
|
||||
func parseRecoveredResults(raw []byte) []crackResultView {
|
||||
if len(bytes.TrimSpace(raw)) == 0 {
|
||||
return nil
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var items []map[string]any
|
||||
if err := decoder.Decode(&items); err != nil {
|
||||
return nil
|
||||
}
|
||||
results := make([]crackResultView, 0, len(items))
|
||||
for _, item := range items {
|
||||
results = append(results, crackResultView{
|
||||
CredentialID: safeCrackCell(jsonText(firstJSONValue(item, "credential_id", "credentialID"))),
|
||||
Hash: safeCrackCell(jsonText(firstJSONValue(item, "hash"))),
|
||||
Plaintext: decodeRecoveredPlaintext(jsonText(firstJSONValue(item, "plaintext"))),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func decodeRecoveredPlaintext(value string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
// Older crackstations encoded plaintext as a JSON string. Preserve that
|
||||
// representation when it is not valid protobuf bytes JSON.
|
||||
return safeCrackCell(value)
|
||||
}
|
||||
if !utf8.Valid(decoded) || bytes.IndexByte(decoded, 0) >= 0 {
|
||||
return "0x" + hex.EncodeToString(decoded)
|
||||
}
|
||||
return safeCrackCell(string(decoded))
|
||||
}
|
||||
|
||||
func protoRepeatedMessages(message protoreflect.Message, names ...string) (protoreflect.List, bool) {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil || !field.IsList() || field.Kind() != protoreflect.MessageKind {
|
||||
return nil, false
|
||||
}
|
||||
return message.Get(field).List(), true
|
||||
}
|
||||
|
||||
func protoFieldBytes(message protoreflect.Message, names ...string) []byte {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil {
|
||||
return nil
|
||||
}
|
||||
switch field.Kind() {
|
||||
case protoreflect.BytesKind:
|
||||
return append([]byte(nil), message.Get(field).Bytes()...)
|
||||
case protoreflect.StringKind:
|
||||
return []byte(message.Get(field).String())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func protoFieldText(message protoreflect.Message, names ...string) string {
|
||||
field := findProtoField(message, names...)
|
||||
if field == nil {
|
||||
return ""
|
||||
}
|
||||
value := message.Get(field)
|
||||
switch field.Kind() {
|
||||
case protoreflect.StringKind:
|
||||
return safeCrackCell(value.String())
|
||||
case protoreflect.BytesKind:
|
||||
return safeCrackCell(string(value.Bytes()))
|
||||
case protoreflect.EnumKind:
|
||||
if descriptor := field.Enum().Values().ByNumber(value.Enum()); descriptor != nil {
|
||||
return string(descriptor.Name())
|
||||
}
|
||||
return strconv.FormatInt(int64(value.Enum()), 10)
|
||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
||||
return strconv.FormatInt(value.Int(), 10)
|
||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
||||
return strconv.FormatUint(value.Uint(), 10)
|
||||
case protoreflect.FloatKind:
|
||||
return strconv.FormatFloat(value.Float(), 'f', -1, 32)
|
||||
case protoreflect.DoubleKind:
|
||||
return strconv.FormatFloat(value.Float(), 'f', -1, 64)
|
||||
case protoreflect.BoolKind:
|
||||
return strconv.FormatBool(value.Bool())
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func findProtoField(message protoreflect.Message, names ...string) protoreflect.FieldDescriptor {
|
||||
if !message.IsValid() {
|
||||
return nil
|
||||
}
|
||||
wanted := map[string]struct{}{}
|
||||
for _, name := range names {
|
||||
wanted[normalizedProtoName(name)] = struct{}{}
|
||||
}
|
||||
fields := message.Descriptor().Fields()
|
||||
for index := 0; index < fields.Len(); index++ {
|
||||
field := fields.Get(index)
|
||||
if _, ok := wanted[normalizedProtoName(string(field.Name()))]; ok {
|
||||
return field
|
||||
}
|
||||
if _, ok := wanted[normalizedProtoName(field.JSONName())]; ok {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedProtoName(value string) string {
|
||||
value = strings.ToLower(value)
|
||||
return strings.NewReplacer("_", "", "-", "").Replace(value)
|
||||
}
|
||||
|
||||
func firstJSONValue(values map[string]any, names ...string) any {
|
||||
for _, name := range names {
|
||||
if value, ok := values[name]; ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func jsonPair(value any) (string, string) {
|
||||
items, ok := value.([]any)
|
||||
if !ok || len(items) < 2 {
|
||||
return "", ""
|
||||
}
|
||||
return jsonText(items[0]), jsonText(items[1])
|
||||
}
|
||||
|
||||
func pairText(first, second string) string {
|
||||
if first == "" && second == "" {
|
||||
return "-"
|
||||
}
|
||||
return valueOrDash(first) + "/" + valueOrDash(second)
|
||||
}
|
||||
|
||||
func jsonText(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return safeCrackCell(typed)
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case bool:
|
||||
return strconv.FormatBool(typed)
|
||||
default:
|
||||
return safeCrackCell(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
|
||||
func formatCrackTime(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || value == "0" {
|
||||
return "-"
|
||||
}
|
||||
if unix, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return time.Unix(unix, 0).UTC().Format(time.RFC3339)
|
||||
}
|
||||
if parsed, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return parsed.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return safeCrackCell(value)
|
||||
}
|
||||
|
||||
func unitValue(value, suffix string) string {
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
if numeric, err := strconv.ParseFloat(value, 64); err == nil && numeric < 0 {
|
||||
return "-"
|
||||
}
|
||||
return safeCrackCell(value) + suffix
|
||||
}
|
||||
|
||||
func valueOrDash(value string) string {
|
||||
value = safeCrackCell(value)
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func safeCrackCell(value string) string {
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if unicode.IsControl(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
value = strings.TrimSpace(value)
|
||||
const maximumRunes = 96
|
||||
runes := []rune(value)
|
||||
if len(runes) > maximumRunes {
|
||||
value = string(runes[:maximumRunes-1]) + "…"
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
func TestCrackJobCommandsAreRegistered(t *testing.T) {
|
||||
root := Commands(nil)[0]
|
||||
jobs, _, err := root.Find([]string{"jobs"})
|
||||
if err != nil || jobs == root {
|
||||
t.Fatalf("find crack jobs: command=%v error=%v", jobs, err)
|
||||
}
|
||||
top, _, err := root.Find([]string{"top"})
|
||||
if err != nil || top == root {
|
||||
t.Fatalf("find crack top: command=%v error=%v", top, err)
|
||||
}
|
||||
if top.Use != "top" {
|
||||
t.Fatalf("crack top use = %q, want top", top.Use)
|
||||
}
|
||||
if err := top.Args(top, nil); err != nil {
|
||||
t.Fatalf("crack top rejected no arguments: %v", err)
|
||||
}
|
||||
if err := top.Args(top, []string{"unexpected"}); err == nil {
|
||||
t.Fatal("crack top accepted a positional argument")
|
||||
}
|
||||
pollInterval := top.Flags().Lookup("poll-interval")
|
||||
if pollInterval == nil {
|
||||
t.Fatal("crack top is missing --poll-interval")
|
||||
}
|
||||
if got, err := top.Flags().GetDuration("poll-interval"); err != nil || got != time.Second {
|
||||
t.Fatalf("crack top --poll-interval = %s, %v; want %s", got, err, time.Second)
|
||||
}
|
||||
if top.PersistentFlags().Lookup("poll-interval") != nil || root.PersistentFlags().Lookup("poll-interval") != nil {
|
||||
t.Fatal("--poll-interval must remain local to crack top")
|
||||
}
|
||||
if top.InheritedFlags().Lookup("timeout") == nil {
|
||||
t.Fatal("crack top must inherit --timeout")
|
||||
}
|
||||
job, _, err := root.Find([]string{"job", "job-id"})
|
||||
if err != nil || job == root {
|
||||
t.Fatalf("find crack job: command=%v error=%v", job, err)
|
||||
}
|
||||
if job.Flags().Lookup("watch") == nil || job.Flags().Lookup("poll-interval") == nil {
|
||||
t.Fatal("crack job is missing watch flags")
|
||||
}
|
||||
if job.Use != "job [id]" {
|
||||
t.Fatalf("crack job use = %q, want %q", job.Use, "job [id]")
|
||||
}
|
||||
if job.ValidArgsFunction == nil {
|
||||
t.Fatal("crack job is missing ID completion")
|
||||
}
|
||||
if err := job.Args(job, nil); err != nil {
|
||||
t.Fatalf("crack job rejected interactive selection: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"job-id"}); err != nil {
|
||||
t.Fatalf("crack job rejected one job ID: %v", err)
|
||||
}
|
||||
if err := job.Args(job, []string{"first", "second"}); err == nil {
|
||||
t.Fatal("crack job accepted more than one job ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDUsesExplicitIDWithoutListing(t *testing.T) {
|
||||
listed := false
|
||||
selected := false
|
||||
jobID, err := resolveCrackJobID([]string{" full-job-id "}, func() (*clientpb.CrackJobs, error) {
|
||||
listed = true
|
||||
return nil, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selected = true
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "full-job-id" {
|
||||
t.Fatalf("job ID = %q, want full-job-id", jobID)
|
||||
}
|
||||
if listed || selected {
|
||||
t.Fatalf("explicit ID unexpectedly listed=%v selected=%v", listed, selected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDSelectsFromListedJobs(t *testing.T) {
|
||||
wantJobs := []*clientpb.CrackJob{{ID: "first"}, {ID: "selected-full-id"}}
|
||||
listCalls := 0
|
||||
selectCalls := 0
|
||||
jobID, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
listCalls++
|
||||
return &clientpb.CrackJobs{Jobs: wantJobs}, nil
|
||||
}, func(jobs []*clientpb.CrackJob) (string, error) {
|
||||
selectCalls++
|
||||
if len(jobs) != len(wantJobs) || jobs[1] != wantJobs[1] {
|
||||
t.Fatalf("selector jobs = %#v, want %#v", jobs, wantJobs)
|
||||
}
|
||||
return "selected-full-id", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveCrackJobID: %v", err)
|
||||
}
|
||||
if jobID != "selected-full-id" {
|
||||
t.Fatalf("job ID = %q, want selected-full-id", jobID)
|
||||
}
|
||||
if listCalls != 1 || selectCalls != 1 {
|
||||
t.Fatalf("list calls = %d, select calls = %d; want 1 each", listCalls, selectCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDHandlesEmptyAndInvalidSelections(t *testing.T) {
|
||||
selectorCalled := false
|
||||
for _, listedJobs := range [][]*clientpb.CrackJob{nil, {nil, {ID: " "}}} {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: listedJobs}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
selectorCalled = true
|
||||
return "unused", nil
|
||||
})
|
||||
if !errors.Is(err, errNoCrackJobs) {
|
||||
t.Fatalf("empty job error = %v, want %v", err, errNoCrackJobs)
|
||||
}
|
||||
}
|
||||
if selectorCalled {
|
||||
t.Fatal("selector called with no jobs")
|
||||
}
|
||||
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return " ", nil
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "no crack job selected") {
|
||||
t.Fatalf("blank selection error = %v", err)
|
||||
}
|
||||
|
||||
_, err = resolveCrackJobID([]string{" "}, nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "job ID cannot be empty") {
|
||||
t.Fatalf("blank explicit ID error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCrackJobIDPropagatesUserAbort(t *testing.T) {
|
||||
_, err := resolveCrackJobID(nil, func() (*clientpb.CrackJobs, error) {
|
||||
return &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{ID: "job-id"}}}, nil
|
||||
}, func([]*clientpb.CrackJob) (string, error) {
|
||||
return "", forms.ErrUserAborted
|
||||
})
|
||||
if !errors.Is(err, forms.ErrUserAborted) {
|
||||
t.Fatalf("selection error = %v, want user abort", err)
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // One fixture verifies the complete newest-status telemetry contract and all presentation helpers.
|
||||
func TestParseCrackStatusJSONUsesNewestStatus(t *testing.T) {
|
||||
raw := []byte("{\"status\":1,\"progress\":[1,10]}\n" +
|
||||
"{\"session\":\"distributed-job\",\"status\":2,\"target\":\"hashes\",\"progress\":[5,10]," +
|
||||
"\"restore_point\":4,\"recovered_hashes\":[1,3],\"recovered_salts\":[1,2],\"rejected\":7," +
|
||||
"\"time_start\":1700000000,\"estimated_stop\":1700000100," +
|
||||
"\"guess\":{\"guess_mode\":3,\"guess_base\":\"?d?d\",\"guess_base_offset\":1,\"guess_base_count\":10," +
|
||||
"\"guess_base_percent\":25.5,\"guess_mask_length\":2,\"guess_mod\":\"rules\",\"guess_mod_offset\":2,\"guess_mod_count\":4,\"guess_mod_percent\":50},\"devices\":[" +
|
||||
"{\"device_id\":1,\"device_name\":\"GPU 0\",\"device_type\":\"GPU\",\"speed\":1500,\"temp\":65,\"util\":98,\"corespeed\":2100,\"memoryspeed\":9500,\"buslanes\":16}," +
|
||||
"{\"id\":2,\"name\":\"GPU 1\",\"type\":\"GPU\",\"speed\":500,\"temperature\":70,\"utilization\":97}]}\n")
|
||||
|
||||
status, err := parseCrackStatusJSON(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCrackStatusJSON: %v", err)
|
||||
}
|
||||
if status.Status != "2" || status.ProgressCurrent != "5" || status.ProgressTotal != "10" {
|
||||
t.Fatalf("unexpected status: %#v", status)
|
||||
}
|
||||
if got := status.progressText(); got != "5/10 (50.0%)" {
|
||||
t.Fatalf("progressText = %q", got)
|
||||
}
|
||||
if got := status.speedText(); got != "2.00 kH/s" {
|
||||
t.Fatalf("speedText = %q", got)
|
||||
}
|
||||
if got := status.temperatureText(); got != "70 C" {
|
||||
t.Fatalf("temperatureText = %q", got)
|
||||
}
|
||||
if len(status.Devices) != 2 || status.Devices[1].Name != "GPU 1" {
|
||||
t.Fatalf("devices = %#v", status.Devices)
|
||||
}
|
||||
if status.Session != "distributed-job" || status.Target != "hashes" || status.RestorePoint != "4" ||
|
||||
status.RecoveredHashesCurrent != "1" || status.RecoveredHashesTotal != "3" ||
|
||||
status.RecoveredSaltsCurrent != "1" || status.RecoveredSaltsTotal != "2" || status.Rejected != "7" ||
|
||||
status.TimeStart != "1700000000" || status.EstimatedStop != "1700000100" {
|
||||
t.Fatalf("full status fields were not decoded: %#v", status)
|
||||
}
|
||||
if status.Guess.Mode != "3" || status.Guess.Base != "?d?d" || status.Guess.BasePercent != "25.5" || status.Guess.MaskLength != "2" || status.Guess.Mod != "rules" || status.Guess.ModPercent != "50" {
|
||||
t.Fatalf("guess = %#v", status.Guess)
|
||||
}
|
||||
if got := status.guessText(); !strings.Contains(got, "base=?d?d [1/10] (25.5%)") || !strings.Contains(got, "mask-length=2") || !strings.Contains(got, "mod=rules [2/4] (50%)") {
|
||||
t.Fatalf("guessText = %q", got)
|
||||
}
|
||||
if status.Devices[0].CoreSpeed != "2100" || status.Devices[0].MemorySpeed != "9500" || status.Devices[0].BusLanes != "16" {
|
||||
t.Fatalf("extended device telemetry = %#v", status.Devices[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCrackStatusJSONRejectsMalformedOutput(t *testing.T) {
|
||||
if _, err := parseCrackStatusJSON([]byte("hashcat is still starting")); err == nil {
|
||||
t.Fatal("malformed status output was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnavailableDeviceTelemetryRendersAsUnknown(t *testing.T) {
|
||||
status := &crackStatusView{Devices: []crackDeviceView{{Temperature: "-1"}}}
|
||||
if got := status.temperatureText(); got != "-" {
|
||||
t.Fatalf("temperatureText = %q, want -", got)
|
||||
}
|
||||
if got := unitValue("-1", " C"); got != "-" {
|
||||
t.Fatalf("unitValue = %q, want -", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobIncludesTasksProgressDevicesAndResults(t *testing.T) {
|
||||
status := []byte(`{"session":"job-session","status":2,"target":"hash-file","progress":[25,100],"restore_point":24,"recovered_hashes":[1,2],"recovered_salts":[1,1],"rejected":3,"time_start":1700000000,"estimated_stop":1700000100,"guess":{"guess_mode":3,"guess_base":"?d?d","guess_base_offset":0,"guess_base_count":1},"devices":[{"device_id":1,"device_name":"GPU 0","device_type":"GPU","speed":2500000,"temp":61,"util":99,"fanspeed":45,"corespeed":2100,"memoryspeed":9000,"buslanes":16,"power":125000}]}`)
|
||||
recovered := []byte(`[{"hash":"hash-one","plaintext":"cGxhaW4tb25l"}]`)
|
||||
task := &clientpb.CrackTask{
|
||||
ID: "task-id",
|
||||
HostUUID: "station-id",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
UpdatedAt: 1700000002,
|
||||
LatestStatusJSON: status,
|
||||
RecoveredJSON: recovered,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 50,
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000000",
|
||||
Tasks: []*clientpb.CrackTask{task},
|
||||
Results: []*clientpb.CrackResult{{
|
||||
Hash: "hash-one",
|
||||
Plaintext: []byte("plain-one"),
|
||||
}, {
|
||||
CredentialID: "credential-id",
|
||||
Hash: "hash-two",
|
||||
Plaintext: []byte("plain-two"),
|
||||
}},
|
||||
}
|
||||
|
||||
rawOutput := renderCrackJob(job, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{
|
||||
"job-id", "1000000", "task-id", "station-id", "100+50", "25/100 (25.0%)",
|
||||
"2.50 MH/s", "61 C", "GPU 0", "credential-id", "hash-one", "plain-one", "hash-two", "plain-two",
|
||||
"Hashcat Statistics", "job-session", "hash-file", "mode=3", "base=?d?d", "1/2", "1/1",
|
||||
"2100 MHz", "9000 MHz", "16 lanes", "125000 mW", "2023-11-14T22:13:20Z", "2023-11-14T22:15:00Z",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered job does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldPrimary.Render("Crack Job"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String()),
|
||||
console.StyleBoldPrimary.Render("Tasks"),
|
||||
console.StyleBoldWarning.Render(clientpb.CrackTaskState_CRACK_TASK_RUNNING.String()),
|
||||
console.StyleBoldPrimary.Render("Hashcat Statistics"),
|
||||
console.StyleBoldPrimary.Render("Devices"),
|
||||
console.StyleBoldSuccess.Render("Recovered Credentials"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered job is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
if strings.Contains(rawOutput, console.StylePrimary.Render("task-id")) {
|
||||
t.Fatalf("rendered task ID should use the default foreground color:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobsSummarizesExtendedFields(t *testing.T) {
|
||||
job := &clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
CreatedAt: "2026-09-09T00:00:00Z",
|
||||
UpdatedAt: 1700000003,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
Results: []*clientpb.CrackResult{{Hash: "hash", Plaintext: []byte("plain")}},
|
||||
}
|
||||
rawOutput := renderCrackJobs([]*clientpb.CrackJob{job}, settings.SliverDefault)
|
||||
output := ansi.Strip(rawOutput)
|
||||
for _, expected := range []string{"job-id", "IN_PROGRESS", "1000", "1"} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("rendered jobs do not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(rawOutput, console.StyleBoldPrimary.Render("job-id")) ||
|
||||
!strings.Contains(rawOutput, console.StyleBoldWarning.Render(clientpb.CrackJobStatus_IN_PROGRESS.String())) {
|
||||
t.Fatalf("rendered jobs are missing Lip Gloss ID or status styling:\n%s", rawOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobStatusStyleUsesSemanticPalette(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
want console.TextStyle
|
||||
}{
|
||||
{status: "IN_PROGRESS", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_RUNNING", want: console.StyleBoldWarning},
|
||||
{status: "CRACK_TASK_QUEUED", want: console.StyleBold},
|
||||
{status: "CRACK_TASK_LEASED", want: console.StyleBoldWarning},
|
||||
{status: "COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_COMPLETED", want: console.StyleBoldSuccess},
|
||||
{status: "CRACK_TASK_FAILED", want: console.StyleBoldDanger},
|
||||
{status: "CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "CANCELED", want: console.StyleBoldGray},
|
||||
{status: "CRACK_TASK_CANCELLED", want: console.StyleBoldGray},
|
||||
{status: "UNKNOWN", want: console.StyleBold},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.status, func(t *testing.T) {
|
||||
got := crackJobStatusStyle(test.status)
|
||||
if got.GetBold() != test.want.GetBold() {
|
||||
t.Fatalf("bold = %v, want %v", got.GetBold(), test.want.GetBold())
|
||||
}
|
||||
gotColor := got.GetForeground()
|
||||
wantColor := test.want.GetForeground()
|
||||
if (gotColor == nil) != (wantColor == nil) {
|
||||
t.Fatalf("foreground = %v, want %v", gotColor, wantColor)
|
||||
}
|
||||
if gotColor == nil {
|
||||
return
|
||||
}
|
||||
gotR, gotG, gotB, gotA := gotColor.RGBA()
|
||||
wantR, wantG, wantB, wantA := wantColor.RGBA()
|
||||
if gotR != wantR || gotG != wantG || gotB != wantB || gotA != wantA {
|
||||
t.Fatalf("foreground RGBA = (%d,%d,%d,%d), want (%d,%d,%d,%d)", gotR, gotG, gotB, gotA, wantR, wantG, wantB, wantA)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackJobStylesErrors(t *testing.T) {
|
||||
rawOutput := renderCrackJob(&clientpb.CrackJob{
|
||||
ID: "job-id",
|
||||
Status: clientpb.CrackJobStatus_FAILED,
|
||||
Err: "worker failed",
|
||||
}, settings.SliverDefault)
|
||||
for _, styled := range []string{
|
||||
console.StyleBoldDanger.Render(clientpb.CrackJobStatus_FAILED.String()),
|
||||
console.StyleBoldDanger.Render("Error"),
|
||||
console.StyleDanger.Render("worker failed"),
|
||||
} {
|
||||
if !strings.Contains(rawOutput, styled) {
|
||||
t.Fatalf("rendered error is missing Lip Gloss styling %q:\n%s", styled, rawOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultCountUsesSummaryField(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{{Plaintext: []byte("not-a-summary-count")}}}
|
||||
field := findProtoField(job.ProtoReflect(), "ResultCount")
|
||||
want := "-"
|
||||
if field != nil {
|
||||
job.ProtoReflect().Set(field, protoreflect.ValueOfUint64(7))
|
||||
want = "7"
|
||||
}
|
||||
if got := crackJobResultCount(job); got != want {
|
||||
t.Fatalf("crackJobResultCount() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobResultsRenderAuthoritativePlaintextBytes(t *testing.T) {
|
||||
job := &clientpb.CrackJob{Results: []*clientpb.CrackResult{
|
||||
{Hash: "valid", Plaintext: []byte("line\ntext\x1b[31m")},
|
||||
{Hash: "binary", Plaintext: []byte{0xff, 0x00, 0x1b}},
|
||||
{Hash: "nul", Plaintext: []byte{'a', 0, 'b'}},
|
||||
}}
|
||||
results := crackJobResults(job)
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("crackJobResults() = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "linetext[31m" {
|
||||
t.Fatalf("valid UTF-8 plaintext = %q, want sanitized text", results[0].Plaintext)
|
||||
}
|
||||
if results[1].Plaintext != "0xff001b" {
|
||||
t.Fatalf("binary plaintext = %q, want 0xff001b", results[1].Plaintext)
|
||||
}
|
||||
if results[2].Plaintext != "0x610062" {
|
||||
t.Fatalf("NUL plaintext = %q, want 0x610062", results[2].Plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobPrintsChangesAndStopsAtTerminalState(t *testing.T) {
|
||||
inProgress := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}
|
||||
completed := &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_COMPLETED}
|
||||
responses := []*clientpb.CrackJob{inProgress, inProgress, completed}
|
||||
requests := 0
|
||||
fetch := func(_ context.Context, id string) (*clientpb.CrackJob, error) {
|
||||
if id != "job-id" {
|
||||
t.Fatalf("job ID = %q", id)
|
||||
}
|
||||
response := responses[requests]
|
||||
requests++
|
||||
return response, nil
|
||||
}
|
||||
updates := make([]clientpb.CrackJobStatus, 0)
|
||||
err := watchCrackJob(context.Background(), "job-id", time.Millisecond, fetch, func(job *clientpb.CrackJob) {
|
||||
updates = append(updates, job.Status)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("watchCrackJob: %v", err)
|
||||
}
|
||||
if requests != 3 {
|
||||
t.Fatalf("requests = %d, want 3", requests)
|
||||
}
|
||||
if len(updates) != 2 || updates[0] != clientpb.CrackJobStatus_IN_PROGRESS || updates[1] != clientpb.CrackJobStatus_COMPLETED {
|
||||
t.Fatalf("updates = %#v", updates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchCrackJobCancellationAndValidation(t *testing.T) {
|
||||
fetch := func(_ context.Context, _ string) (*clientpb.CrackJob, error) {
|
||||
return &clientpb.CrackJob{ID: "job-id", Status: clientpb.CrackJobStatus_IN_PROGRESS}, nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := watchCrackJob(ctx, "job-id", time.Hour, fetch, nil); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("watchCrackJob cancellation error = %v", err)
|
||||
}
|
||||
if err := watchCrackJob(context.Background(), "job-id", 0, fetch, nil); err == nil {
|
||||
t.Fatal("watchCrackJob accepted zero poll interval")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsStripTerminalControlCharacters(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash\u001b[31m","plaintext":"cGxhaW4KdGV4dA=="}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if strings.ContainsAny(results[0].Hash, "\x1b\n\r") || strings.ContainsAny(results[0].Plaintext, "\x1b\n\r") {
|
||||
t.Fatalf("control characters were not stripped: %#v", results[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveredResultsRenderNULPlaintextAsHex(t *testing.T) {
|
||||
results := parseRecoveredResults([]byte(`[{"hash":"hash","plaintext":"YQBi"}]`))
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("results = %#v", results)
|
||||
}
|
||||
if results[0].Plaintext != "0x610062" {
|
||||
t.Fatalf("plaintext = %q, want 0x610062", results[0].Plaintext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const crackTopWarningRunes = 256
|
||||
|
||||
// crackTopRPC is the complete wire-level dependency of the crack top client.
|
||||
// Keeping it narrow also makes it difficult for the dashboard to accidentally
|
||||
// fall back to RPCs that expose crack targets or recovered material.
|
||||
type crackTopRPC interface {
|
||||
CrackTop(context.Context, *commonpb.Empty, ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
type crackTopTaskTelemetry struct {
|
||||
status *crackStatusView
|
||||
parseErr bool
|
||||
progressExact bool
|
||||
}
|
||||
|
||||
// crackTopSnapshot is the client-side representation consumed by the existing
|
||||
// dashboard telemetry code. Jobs and stations contain only fields supplied by
|
||||
// the narrow CrackTop RPC; parsed status is kept separately so no sensitive raw
|
||||
// status JSON has to be reconstructed.
|
||||
type crackTopSnapshot struct {
|
||||
Jobs []*clientpb.CrackJob
|
||||
Stations []*clientpb.Crackstation
|
||||
RefreshedAt time.Time
|
||||
taskTelemetry map[string]crackTopTaskTelemetry
|
||||
}
|
||||
|
||||
// CrackTop snapshots are atomic. A successful refresh replaces all previously
|
||||
// displayed server state, while a failed refresh returns no snapshot and leaves
|
||||
// the model's existing value untouched.
|
||||
func mergeCrackTopSnapshot(_ *crackTopSnapshot, incoming *crackTopSnapshot) *crackTopSnapshot {
|
||||
return incoming
|
||||
}
|
||||
|
||||
func loadCrackTopSnapshot(ctx context.Context, rpc crackTopRPC, perCallTimeout time.Duration, completionTime time.Time) (*crackTopSnapshot, error) {
|
||||
if rpc == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("rpc client is unavailable"))
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if perCallTimeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, perCallTimeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
wire, err := rpc.CrackTop(ctx, &commonpb.Empty{})
|
||||
if completionTime.IsZero() {
|
||||
completionTime = time.Now()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", err)
|
||||
}
|
||||
if wire == nil {
|
||||
return nil, crackTopContextError("load crack top snapshot", errors.New("server returned an empty response"))
|
||||
}
|
||||
|
||||
refreshedAt := completionTime
|
||||
if wire.GetObservedAt() != 0 {
|
||||
refreshedAt = time.Unix(wire.GetObservedAt(), 0)
|
||||
}
|
||||
snapshot := &crackTopSnapshot{
|
||||
Jobs: make([]*clientpb.CrackJob, len(wire.GetJobs())),
|
||||
Stations: make([]*clientpb.Crackstation, len(wire.GetCrackstations())),
|
||||
RefreshedAt: refreshedAt,
|
||||
taskTelemetry: map[string]crackTopTaskTelemetry{},
|
||||
}
|
||||
for index, job := range wire.GetJobs() {
|
||||
snapshot.Jobs[index] = crackTopJobFromWire(job, snapshot.taskTelemetry)
|
||||
}
|
||||
for index, station := range wire.GetCrackstations() {
|
||||
snapshot.Stations[index] = crackTopStationFromWire(station)
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
func crackTopJobFromWire(wire *clientpb.CrackTopJob, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackJob {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
command := &clientpb.CrackCommand{
|
||||
AttackMode: wire.GetAttackMode(),
|
||||
HashType: wire.GetHashType(),
|
||||
}
|
||||
if wire.HashMode != nil {
|
||||
hashMode := wire.GetHashMode()
|
||||
command.HashMode = &hashMode
|
||||
}
|
||||
job := &clientpb.CrackJob{
|
||||
ID: wire.GetID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Status: wire.GetStatus(),
|
||||
Command: command,
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
ResultCount: wire.GetResultCount(),
|
||||
Tasks: make([]*clientpb.CrackTask, len(wire.GetTasks())),
|
||||
}
|
||||
for index, task := range wire.GetTasks() {
|
||||
job.Tasks[index] = crackTopTaskFromWire(task, telemetry)
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
func crackTopTaskFromWire(wire *clientpb.CrackTopTask, telemetry map[string]crackTopTaskTelemetry) *clientpb.CrackTask {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
task := &clientpb.CrackTask{
|
||||
ID: wire.GetID(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
CreatedAt: wire.GetCreatedAt(),
|
||||
StartedAt: wire.GetStartedAt(),
|
||||
CompletedAt: wire.GetCompletedAt(),
|
||||
Kind: wire.GetKind(),
|
||||
State: wire.GetState(),
|
||||
Attempt: wire.GetAttempt(),
|
||||
LeaseExpiresAt: wire.GetLeaseExpiresAt(),
|
||||
UpdatedAt: wire.GetUpdatedAt(),
|
||||
LastHeartbeatAt: wire.GetLastHeartbeatAt(),
|
||||
ShardSkip: wire.GetShardSkip(),
|
||||
ShardLimit: wire.GetShardLimit(),
|
||||
}
|
||||
|
||||
taskID := strings.TrimSpace(task.GetID())
|
||||
if telemetry == nil || taskID == "" {
|
||||
return task
|
||||
}
|
||||
metadata := crackTopTaskTelemetry{
|
||||
parseErr: wire.GetStatusParseError(),
|
||||
progressExact: wire.GetProgressExact(),
|
||||
}
|
||||
if wire.GetStatusAvailable() && !metadata.parseErr {
|
||||
metadata.status = &crackStatusView{Devices: crackTopDevicesFromWire(wire.GetDevices())}
|
||||
if metadata.progressExact || wire.GetShardSkip() == 0 {
|
||||
metadata.status.ProgressCurrent = wire.GetProgressCurrent()
|
||||
metadata.status.ProgressTotal = wire.GetProgressTotal()
|
||||
}
|
||||
}
|
||||
if metadata.status != nil || metadata.parseErr {
|
||||
telemetry[taskID] = metadata
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func crackTopDevicesFromWire(devices []*clientpb.CrackTopDevice) []crackDeviceView {
|
||||
converted := make([]crackDeviceView, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
view := crackDeviceView{ID: device.GetID()}
|
||||
if device.GetSpeedAvailable() {
|
||||
view.Speed = strconv.FormatUint(device.GetSpeed(), 10)
|
||||
}
|
||||
if device.GetTemperatureAvailable() {
|
||||
view.Temperature = strconv.FormatFloat(device.GetTemperature(), 'f', -1, 64)
|
||||
}
|
||||
if device.GetUtilizationAvailable() {
|
||||
view.Utilization = strconv.FormatFloat(device.GetUtilization(), 'f', -1, 64)
|
||||
}
|
||||
converted = append(converted, view)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func crackTopStationFromWire(wire *clientpb.CrackTopStation) *clientpb.Crackstation {
|
||||
if wire == nil {
|
||||
return nil
|
||||
}
|
||||
station := &clientpb.Crackstation{
|
||||
ID: wire.GetID(),
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
}
|
||||
if wire.GetStatusAvailable() {
|
||||
station.Status = &clientpb.CrackstationStatus{
|
||||
Name: wire.GetName(),
|
||||
HostUUID: wire.GetHostUUID(),
|
||||
State: wire.GetState(),
|
||||
CurrentCrackJobID: wire.GetCurrentCrackJobID(),
|
||||
IsSyncing: wire.GetIsSyncing(),
|
||||
}
|
||||
}
|
||||
return station
|
||||
}
|
||||
|
||||
func crackTopContextError(scope string, err error) error {
|
||||
message := crackTopSanitizeError(err)
|
||||
if strings.TrimSpace(scope) == "" {
|
||||
return errors.New(message)
|
||||
}
|
||||
return fmt.Errorf("%s: %s", strings.TrimSpace(scope), message)
|
||||
}
|
||||
|
||||
func crackTopWarningText(message string) string {
|
||||
return crackTopSanitizeError(errors.New(message))
|
||||
}
|
||||
|
||||
func crackTopSanitizeError(err error) string {
|
||||
if err == nil {
|
||||
return "unknown error"
|
||||
}
|
||||
message := ansi.Strip(err.Error())
|
||||
message = strings.Map(func(character rune) rune {
|
||||
if unicode.IsSpace(character) {
|
||||
return ' '
|
||||
}
|
||||
if !unicode.IsPrint(character) {
|
||||
return -1
|
||||
}
|
||||
return character
|
||||
}, message)
|
||||
message = strings.Join(strings.Fields(message), " ")
|
||||
if message == "" {
|
||||
return "unknown error"
|
||||
}
|
||||
runes := []rune(message)
|
||||
if len(runes) > crackTopWarningRunes {
|
||||
message = string(runes[:crackTopWarningRunes-1]) + "…"
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type crackTopRPCTestClient struct {
|
||||
calls atomic.Int32
|
||||
call func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error)
|
||||
}
|
||||
|
||||
func (client *crackTopRPCTestClient) CrackTop(ctx context.Context, request *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackTopSnapshot, error) {
|
||||
client.calls.Add(1)
|
||||
if client.call == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return client.call(ctx, request)
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally validates every field in one complete lean-snapshot conversion.
|
||||
func TestLoadCrackTopSnapshotUsesOneRPCAndConvertsLeanSnapshot(t *testing.T) {
|
||||
hashMode := uint32(5600)
|
||||
wire := &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: 1_700_000_123,
|
||||
Jobs: []*clientpb.CrackTopJob{
|
||||
{
|
||||
ID: "job-one",
|
||||
CreatedAt: "2026-09-12T10:00:00Z",
|
||||
CompletedAt: "",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
UpdatedAt: 1_700_000_120,
|
||||
ResultCount: 7,
|
||||
AttackMode: clientpb.CrackAttackMode_BRUTEFORCE,
|
||||
HashType: clientpb.HashType_NET_NTLM_V2,
|
||||
HashMode: &hashMode,
|
||||
Tasks: []*clientpb.CrackTopTask{
|
||||
{
|
||||
ID: "task-live",
|
||||
HostUUID: "worker-one",
|
||||
CreatedAt: 11,
|
||||
StartedAt: 12,
|
||||
CompletedAt: 13,
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
Attempt: 2,
|
||||
LeaseExpiresAt: 1_700_000_500,
|
||||
UpdatedAt: 14,
|
||||
LastHeartbeatAt: 15,
|
||||
ShardSkip: 100,
|
||||
ShardLimit: 200,
|
||||
ProgressCurrent: "125",
|
||||
ProgressTotal: "300",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: false,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{
|
||||
{
|
||||
ID: "gpu-0",
|
||||
Speed: 9_876_543,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 71.5,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 99.25,
|
||||
UtilizationAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "gpu-1", Speed: 123, Temperature: 45, Utilization: 67},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{
|
||||
ID: "task-malformed",
|
||||
HostUUID: "worker-two",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_CRACK,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
ProgressCurrent: "sensitive-raw-data-must-not-be-rebuilt",
|
||||
StatusAvailable: true,
|
||||
StatusParseError: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
{ID: "job-two", Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
},
|
||||
Crackstations: []*clientpb.CrackTopStation{
|
||||
{
|
||||
ID: "station-one",
|
||||
Name: "rig-one",
|
||||
HostUUID: "worker-one",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: "job-one",
|
||||
IsSyncing: false,
|
||||
StatusAvailable: true,
|
||||
},
|
||||
nil,
|
||||
{ID: "station-two", Name: "rig-two", HostUUID: "worker-two"},
|
||||
},
|
||||
}
|
||||
wireBefore := proto.Clone(wire).(*clientpb.CrackTopSnapshot)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, request *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
if ctx == nil {
|
||||
t.Fatal("CrackTop received a nil context")
|
||||
}
|
||||
if request == nil {
|
||||
t.Fatal("CrackTop received a nil request")
|
||||
}
|
||||
return wire, nil
|
||||
},
|
||||
}
|
||||
|
||||
fallback := time.Unix(1_600_000_000, 987_654_321)
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if want := time.Unix(wire.GetObservedAt(), 0); !snapshot.RefreshedAt.Equal(want) {
|
||||
t.Fatalf("RefreshedAt = %s, want server observation time %s", snapshot.RefreshedAt, want)
|
||||
}
|
||||
if !proto.Equal(wire, wireBefore) {
|
||||
t.Fatal("loader mutated the wire snapshot")
|
||||
}
|
||||
|
||||
if len(snapshot.Jobs) != 3 || snapshot.Jobs[1] != nil || snapshot.Jobs[2].GetID() != "job-two" {
|
||||
t.Fatalf("jobs did not preserve wire order and nil entries: %#v", snapshot.Jobs)
|
||||
}
|
||||
job := snapshot.Jobs[0]
|
||||
if job.GetID() != "job-one" || job.GetCreatedAt() != wire.Jobs[0].GetCreatedAt() || job.GetCompletedAt() != wire.Jobs[0].GetCompletedAt() ||
|
||||
job.GetStatus() != clientpb.CrackJobStatus_IN_PROGRESS || job.GetUpdatedAt() != 1_700_000_120 || job.GetResultCount() != 7 {
|
||||
t.Fatalf("converted job metadata = %#v", job)
|
||||
}
|
||||
if job.GetCommand() == nil || job.GetCommand().GetAttackMode() != clientpb.CrackAttackMode_BRUTEFORCE ||
|
||||
job.GetCommand().GetHashType() != clientpb.HashType_NET_NTLM_V2 || job.GetCommand().HashMode == nil || job.GetCommand().GetHashMode() != hashMode {
|
||||
t.Fatalf("converted job command = %#v", job.GetCommand())
|
||||
}
|
||||
if job.GetErr() != "" || job.GetResultFileID() != "" || job.GetKeyspace() != "" || len(job.GetResults()) != 0 ||
|
||||
len(job.GetCommand().GetHashes()) != 0 || len(job.GetCommand().GetPositionalArguments()) != 0 || len(job.GetCommand().GetCredentialIDs()) != 0 {
|
||||
t.Fatalf("lean job conversion populated target or result fields: %#v", job)
|
||||
}
|
||||
if len(job.GetTasks()) != 3 || job.GetTasks()[1] != nil {
|
||||
t.Fatalf("tasks did not preserve wire order and nil entries: %#v", job.GetTasks())
|
||||
}
|
||||
task := job.GetTasks()[0]
|
||||
if task.GetID() != "task-live" || task.GetHostUUID() != "worker-one" || task.GetCreatedAt() != 11 || task.GetStartedAt() != 12 ||
|
||||
task.GetCompletedAt() != 13 || task.GetKind() != clientpb.CrackTaskKind_CRACK_TASK_CRACK ||
|
||||
task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING || task.GetAttempt() != 2 ||
|
||||
task.GetLeaseExpiresAt() != 1_700_000_500 || task.GetUpdatedAt() != 14 || task.GetLastHeartbeatAt() != 15 ||
|
||||
task.GetShardSkip() != 100 || task.GetShardLimit() != 200 {
|
||||
t.Fatalf("converted task metadata = %#v", task)
|
||||
}
|
||||
if task.GetErr() != "" || len(task.GetStdout()) != 0 || len(task.GetStderr()) != 0 || task.GetCommand() != nil ||
|
||||
task.GetLeaseToken() != "" || task.GetKeyspace() != "" || len(task.GetLatestStatusJSON()) != 0 || len(task.GetRecoveredJSON()) != 0 {
|
||||
t.Fatalf("lean task conversion populated execution or raw telemetry fields: %#v", task)
|
||||
}
|
||||
|
||||
metadata, ok := snapshot.taskTelemetry["task-live"]
|
||||
if !ok || metadata.parseErr || metadata.status == nil {
|
||||
t.Fatalf("live task telemetry = %#v, present=%v", metadata, ok)
|
||||
}
|
||||
if !metadata.progressExact {
|
||||
t.Fatal("server-declared exact progress was discarded")
|
||||
}
|
||||
if metadata.status.ProgressCurrent != "125" || metadata.status.ProgressTotal != "300" || len(metadata.status.Devices) != 2 {
|
||||
t.Fatalf("converted status = %#v", metadata.status)
|
||||
}
|
||||
firstDevice := metadata.status.Devices[0]
|
||||
if firstDevice.ID != "gpu-0" || firstDevice.Speed != "9876543" || firstDevice.Temperature != "71.5" || firstDevice.Utilization != "99.25" {
|
||||
t.Fatalf("converted available device telemetry = %#v", firstDevice)
|
||||
}
|
||||
secondDevice := metadata.status.Devices[1]
|
||||
if secondDevice.ID != "gpu-1" || secondDevice.Speed != "" || secondDevice.Temperature != "" || secondDevice.Utilization != "" {
|
||||
t.Fatalf("unavailable device telemetry was treated as available: %#v", secondDevice)
|
||||
}
|
||||
malformed, ok := snapshot.taskTelemetry["task-malformed"]
|
||||
if !ok || !malformed.parseErr || malformed.status != nil {
|
||||
t.Fatalf("malformed task telemetry = %#v, present=%v", malformed, ok)
|
||||
}
|
||||
|
||||
if len(snapshot.Stations) != 3 || snapshot.Stations[1] != nil {
|
||||
t.Fatalf("stations did not preserve wire order and nil entries: %#v", snapshot.Stations)
|
||||
}
|
||||
station := snapshot.Stations[0]
|
||||
if station.GetID() != "station-one" || station.GetName() != "rig-one" || station.GetHostUUID() != "worker-one" || station.GetStatus() == nil ||
|
||||
station.GetStatus().GetName() != "rig-one" || station.GetStatus().GetHostUUID() != "worker-one" ||
|
||||
station.GetStatus().GetState() != clientpb.States_CRACKING || station.GetStatus().GetCurrentCrackJobID() != "job-one" || station.GetStatus().GetIsSyncing() {
|
||||
t.Fatalf("converted station = %#v", station)
|
||||
}
|
||||
if station.GetOperatorName() != "" || station.GetGOOS() != "" || station.GetGOARCH() != "" || station.GetVersion() != "" ||
|
||||
len(station.GetBenchmarks()) != 0 || len(station.GetCUDA()) != 0 || len(station.GetMetal()) != 0 || len(station.GetOpenCL()) != 0 ||
|
||||
len(station.GetHIP()) != 0 || len(station.GetCapabilities()) != 0 {
|
||||
t.Fatalf("lean station conversion populated private inventory fields: %#v", station)
|
||||
}
|
||||
if snapshot.Stations[2].GetStatus() != nil {
|
||||
t.Fatalf("station without available status got a fabricated status: %#v", snapshot.Stations[2])
|
||||
}
|
||||
|
||||
*snapshot.Jobs[0].Command.HashMode = 1000
|
||||
snapshot.Jobs[0].Tasks[0].ID = "changed-task"
|
||||
snapshot.Stations[0].Status.Name = "changed-station"
|
||||
metadata.status.Devices[0].ID = "changed-device"
|
||||
if wire.Jobs[0].GetHashMode() != 5600 || wire.Jobs[0].Tasks[0].GetID() != "task-live" ||
|
||||
wire.Crackstations[0].GetName() != "rig-one" || wire.Jobs[0].Tasks[0].Devices[0].GetID() != "gpu-0" {
|
||||
t.Fatal("converted snapshot aliases mutable wire data")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // This test intentionally covers the full loader-to-dashboard telemetry path.
|
||||
func TestLoadCrackTopSnapshotFeedsDashboardTelemetry(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{
|
||||
ObservedAt: now.Unix(),
|
||||
Jobs: []*clientpb.CrackTopJob{{
|
||||
ID: crackTopTestJobID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
Tasks: []*clientpb.CrackTopTask{{
|
||||
ID: "task-wire",
|
||||
HostUUID: "worker-wire",
|
||||
Kind: clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED,
|
||||
State: clientpb.CrackTaskState_CRACK_TASK_RUNNING,
|
||||
LeaseExpiresAt: now.Add(time.Minute).Unix(),
|
||||
LastHeartbeatAt: now.Unix(),
|
||||
ProgressCurrent: "25",
|
||||
ProgressTotal: "100",
|
||||
StatusAvailable: true,
|
||||
ProgressExact: true,
|
||||
Devices: []*clientpb.CrackTopDevice{{
|
||||
ID: "gpu-0",
|
||||
Speed: 125_000,
|
||||
SpeedAvailable: true,
|
||||
Temperature: 67,
|
||||
TemperatureAvailable: true,
|
||||
Utilization: 98,
|
||||
UtilizationAvailable: true,
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
Crackstations: []*clientpb.CrackTopStation{{
|
||||
ID: "station-wire",
|
||||
Name: "Wire Worker",
|
||||
HostUUID: "worker-wire",
|
||||
State: clientpb.States_CRACKING,
|
||||
CurrentCrackJobID: crackTopTestJobID,
|
||||
StatusAvailable: true,
|
||||
}},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
dashboard := buildCrackTopDashboard(snapshot)
|
||||
if len(dashboard.Jobs) != 1 || !dashboard.Jobs[0].ProgressKnown || dashboard.Jobs[0].Progress != 0.25 {
|
||||
t.Fatalf("job progress from narrow telemetry = %#v", dashboard.Jobs)
|
||||
}
|
||||
if !dashboard.ClusterRateKnown || !dashboard.ClusterComplete || dashboard.ClusterRate != 125_000 {
|
||||
t.Fatalf("cluster telemetry = rate:%d known:%v complete:%v", dashboard.ClusterRate, dashboard.ClusterRateKnown, dashboard.ClusterComplete)
|
||||
}
|
||||
if len(dashboard.Workers) != 1 {
|
||||
t.Fatalf("workers = %#v, want one", dashboard.Workers)
|
||||
}
|
||||
worker := dashboard.Workers[0]
|
||||
if !worker.TelemetryLive || !worker.RateKnown || worker.Rate != 125_000 || worker.Devices != 1 ||
|
||||
!worker.HasTemp || worker.Temperature != 67 || !worker.HasUtil || worker.Utilization != 98 {
|
||||
t.Fatalf("worker telemetry from narrow snapshot = %#v", worker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotUsesCompletionTimeOnlyWithoutObservedAt(t *testing.T) {
|
||||
fallback := time.Unix(1_700_000_000, 123_456_789)
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return &clientpb.CrackTopSnapshot{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 0, fallback)
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if !snapshot.RefreshedAt.Equal(fallback) {
|
||||
t.Fatalf("RefreshedAt = %s, want deterministic completion time %s", snapshot.RefreshedAt, fallback)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesDeadlineAndCancelsCallContext(t *testing.T) {
|
||||
var callContext context.Context
|
||||
var deadline time.Time
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
callContext = ctx
|
||||
var ok bool
|
||||
deadline, ok = ctx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("CrackTop context has no deadline")
|
||||
}
|
||||
return &clientpb.CrackTopSnapshot{ObservedAt: 1}, nil
|
||||
},
|
||||
}
|
||||
|
||||
started := time.Now()
|
||||
timeout := time.Second
|
||||
_, err := loadCrackTopSnapshot(t.Context(), client, timeout, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("loadCrackTopSnapshot: %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
if deadline.Before(started) || deadline.After(started.Add(timeout+100*time.Millisecond)) {
|
||||
t.Fatalf("RPC deadline = %s, want about %s after %s", deadline, timeout, started)
|
||||
}
|
||||
select {
|
||||
case <-callContext.Done():
|
||||
if !errors.Is(callContext.Err(), context.Canceled) {
|
||||
t.Fatalf("completed call context error = %v, want context canceled", callContext.Err())
|
||||
}
|
||||
default:
|
||||
t.Fatal("completed call context was not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotReturnsNilOnTimeout(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, 10*time.Millisecond, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("timed-out load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context deadline exceeded" {
|
||||
t.Fatalf("timed-out load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotPropagatesParentCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(ctx context.Context, _ *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(ctx, client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("canceled load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: context canceled" {
|
||||
t.Fatalf("canceled load error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCrackTopSnapshotSanitizesRPCErrorAndRejectsEmptyResponse(t *testing.T) {
|
||||
t.Run("rpc error", func(t *testing.T) {
|
||||
raw := errors.New("bad\r\n\x1b[31mserver\x1b[0m\tfailure")
|
||||
client := &crackTopRPCTestClient{
|
||||
call: func(context.Context, *commonpb.Empty) (*clientpb.CrackTopSnapshot, error) {
|
||||
return nil, raw
|
||||
},
|
||||
}
|
||||
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("failed load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: bad server failure" {
|
||||
t.Fatalf("sanitized error = %v", err)
|
||||
}
|
||||
if errors.Is(err, raw) {
|
||||
t.Fatal("sanitized error retained the raw RPC error in its unwrap chain")
|
||||
}
|
||||
if strings.ContainsAny(err.Error(), "\r\n\t\x1b") {
|
||||
t.Fatalf("error still contains terminal controls: %q", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil response", func(t *testing.T) {
|
||||
client := &crackTopRPCTestClient{}
|
||||
snapshot, err := loadCrackTopSnapshot(t.Context(), client, time.Second, time.Time{})
|
||||
if snapshot != nil {
|
||||
t.Fatalf("empty load returned snapshot: %#v", snapshot)
|
||||
}
|
||||
if err == nil || err.Error() != "load crack top snapshot: server returned an empty response" {
|
||||
t.Fatalf("empty response error = %v", err)
|
||||
}
|
||||
if client.calls.Load() != 1 {
|
||||
t.Fatalf("CrackTop calls = %d, want 1", client.calls.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
const crackTopMaxProgressTextSize = 148
|
||||
|
||||
type crackTopDashboard struct {
|
||||
Jobs []crackTopJobRow
|
||||
Workers []crackTopWorkerRow
|
||||
ActiveJobs int
|
||||
OnlineWorkers int
|
||||
Unavailable int
|
||||
Recovered uint64
|
||||
ClusterRate uint64
|
||||
ClusterRateKnown bool
|
||||
ClusterComplete bool
|
||||
ExpectedWorkers int
|
||||
ReportingWorkers int
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressJobs int
|
||||
}
|
||||
|
||||
type crackTopJobRow struct {
|
||||
ID string
|
||||
Status clientpb.CrackJobStatus
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Progress float64
|
||||
ProgressKnown bool
|
||||
ProgressComplete bool
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Workers int
|
||||
Reporting int
|
||||
Results uint64
|
||||
Mode string
|
||||
TelemetryErrors int
|
||||
}
|
||||
|
||||
type crackTopWorkerRow struct {
|
||||
ID string
|
||||
Name string
|
||||
State string
|
||||
Online bool
|
||||
Syncing bool
|
||||
JobID string
|
||||
Rate uint64
|
||||
RateKnown bool
|
||||
Devices int
|
||||
Temperature float64
|
||||
HasTemp bool
|
||||
Utilization float64
|
||||
HasUtil bool
|
||||
LastSeen time.Time
|
||||
TelemetryLive bool
|
||||
}
|
||||
|
||||
type crackTopTaskSample struct {
|
||||
jobID string
|
||||
task *clientpb.CrackTask
|
||||
status *crackStatusView
|
||||
statusErr error
|
||||
rate uint64
|
||||
rateKnown bool
|
||||
devices int
|
||||
temperature float64
|
||||
hasTemp bool
|
||||
utilization float64
|
||||
hasUtil bool
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
type crackTopProgressAccumulator struct {
|
||||
completed *big.Rat
|
||||
total *big.Rat
|
||||
}
|
||||
|
||||
func newCrackTopProgressAccumulator() *crackTopProgressAccumulator {
|
||||
return &crackTopProgressAccumulator{completed: new(big.Rat), total: new(big.Rat)}
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) add(fraction, weight *big.Rat) {
|
||||
if a == nil || fraction == nil || weight == nil || weight.Sign() <= 0 {
|
||||
return
|
||||
}
|
||||
clamped := new(big.Rat).Set(fraction)
|
||||
if clamped.Sign() < 0 {
|
||||
clamped.SetInt64(0)
|
||||
}
|
||||
if clamped.Cmp(big.NewRat(1, 1)) > 0 {
|
||||
clamped.SetInt64(1)
|
||||
}
|
||||
a.completed.Add(a.completed, new(big.Rat).Mul(clamped, weight))
|
||||
a.total.Add(a.total, weight)
|
||||
}
|
||||
|
||||
func (a *crackTopProgressAccumulator) value() (float64, bool) {
|
||||
if a == nil || a.total.Sign() <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
fraction, _ := new(big.Rat).Quo(a.completed, a.total).Float64()
|
||||
if math.IsNaN(fraction) || math.IsInf(fraction, 0) {
|
||||
return 0, false
|
||||
}
|
||||
return math.Max(0, math.Min(1, fraction)), true
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Jobs, task telemetry, and connected workers are normalized in one immutable snapshot pass.
|
||||
func buildCrackTopDashboard(snapshot *crackTopSnapshot) crackTopDashboard {
|
||||
dashboard := crackTopDashboard{}
|
||||
if snapshot == nil {
|
||||
return dashboard
|
||||
}
|
||||
|
||||
stations := make(map[string]*clientpb.Crackstation, len(snapshot.Stations))
|
||||
for _, station := range snapshot.Stations {
|
||||
if station == nil {
|
||||
continue
|
||||
}
|
||||
hostID := crackTopStationID(station)
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := stations[hostID]; !exists {
|
||||
stations[hostID] = station
|
||||
}
|
||||
}
|
||||
dashboard.OnlineWorkers = len(stations)
|
||||
|
||||
jobs := make([]*clientpb.CrackJob, 0, len(snapshot.Jobs))
|
||||
for _, job := range snapshot.Jobs {
|
||||
if job != nil && strings.TrimSpace(job.GetID()) != "" {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(jobs, func(i, j int) bool {
|
||||
left := crackJobSortTime(jobs[i])
|
||||
right := crackJobSortTime(jobs[j])
|
||||
if left.Equal(right) {
|
||||
return jobs[i].GetID() < jobs[j].GetID()
|
||||
}
|
||||
return left.After(right)
|
||||
})
|
||||
|
||||
globalProgress := newCrackTopProgressAccumulator()
|
||||
workerSamples := map[string]crackTopTaskSample{}
|
||||
for _, job := range jobs {
|
||||
row, samples, progress := crackTopJobTelemetry(job, stations, snapshot.RefreshedAt, snapshot.taskTelemetry)
|
||||
dashboard.Jobs = append(dashboard.Jobs, row)
|
||||
dashboard.Recovered = saturatingUint64Add(dashboard.Recovered, row.Results)
|
||||
if !crackJobTerminal(job) {
|
||||
dashboard.ActiveJobs++
|
||||
if row.ProgressComplete {
|
||||
dashboard.ProgressJobs++
|
||||
}
|
||||
if progress != nil {
|
||||
globalProgress.completed.Add(globalProgress.completed, progress.completed)
|
||||
globalProgress.total.Add(globalProgress.total, progress.total)
|
||||
}
|
||||
for hostID, sample := range samples {
|
||||
if hostID == "" {
|
||||
continue
|
||||
}
|
||||
if current, ok := workerSamples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], snapshot.RefreshedAt) {
|
||||
workerSamples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dashboard.Progress, dashboard.ProgressKnown = globalProgress.value()
|
||||
dashboard.ProgressKnown = dashboard.ProgressKnown && dashboard.ProgressJobs == dashboard.ActiveJobs
|
||||
if dashboard.ActiveJobs == 0 {
|
||||
dashboard.Progress = 0
|
||||
dashboard.ProgressKnown = false
|
||||
}
|
||||
|
||||
workerIDs := make(map[string]struct{}, len(stations)+len(workerSamples))
|
||||
for hostID := range stations {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerSamples {
|
||||
workerIDs[hostID] = struct{}{}
|
||||
}
|
||||
for hostID := range workerIDs {
|
||||
station := stations[hostID]
|
||||
sample, hasSample := workerSamples[hostID]
|
||||
worker := crackTopWorkerRow{ID: hostID, Online: station != nil}
|
||||
if station != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetName())
|
||||
if worker.Name == "" && station.GetStatus() != nil {
|
||||
worker.Name = crackTopSafeCell(station.GetStatus().GetName())
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
worker.State = status.GetState().String()
|
||||
worker.Syncing = status.GetIsSyncing()
|
||||
worker.JobID = strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
}
|
||||
}
|
||||
if worker.Name == "" {
|
||||
worker.Name = crackTopShortID(hostID)
|
||||
}
|
||||
if worker.State == "" {
|
||||
if worker.Online {
|
||||
worker.State = "ONLINE"
|
||||
} else {
|
||||
worker.State = "OFFLINE"
|
||||
}
|
||||
}
|
||||
if crackTopStationExpectsTelemetry(station) {
|
||||
dashboard.ExpectedWorkers++
|
||||
}
|
||||
if hasSample {
|
||||
if worker.JobID == "" && !worker.Online {
|
||||
worker.JobID = sample.jobID
|
||||
}
|
||||
worker.LastSeen = sample.lastSeen
|
||||
if crackTopTaskSampleIsLive(sample, station, snapshot.RefreshedAt) {
|
||||
worker.TelemetryLive = true
|
||||
worker.Rate = sample.rate
|
||||
worker.RateKnown = sample.rateKnown
|
||||
worker.Devices = sample.devices
|
||||
worker.Temperature = sample.temperature
|
||||
worker.HasTemp = sample.hasTemp
|
||||
worker.Utilization = sample.utilization
|
||||
worker.HasUtil = sample.hasUtil
|
||||
}
|
||||
}
|
||||
if !worker.Online {
|
||||
dashboard.Unavailable++
|
||||
}
|
||||
if worker.RateKnown {
|
||||
dashboard.ClusterRate = saturatingUint64Add(dashboard.ClusterRate, worker.Rate)
|
||||
dashboard.ClusterRateKnown = true
|
||||
dashboard.ReportingWorkers++
|
||||
}
|
||||
dashboard.Workers = append(dashboard.Workers, worker)
|
||||
}
|
||||
dashboard.ClusterComplete = dashboard.ReportingWorkers == dashboard.ExpectedWorkers
|
||||
dashboard.ClusterRateKnown = dashboard.ReportingWorkers > 0 || dashboard.ClusterComplete
|
||||
|
||||
sort.SliceStable(dashboard.Workers, func(i, j int) bool {
|
||||
left, right := dashboard.Workers[i], dashboard.Workers[j]
|
||||
if left.Online != right.Online {
|
||||
return left.Online
|
||||
}
|
||||
if left.RateKnown != right.RateKnown {
|
||||
return left.RateKnown
|
||||
}
|
||||
if left.Rate != right.Rate {
|
||||
return left.Rate > right.Rate
|
||||
}
|
||||
leftName := strings.ToLower(left.Name)
|
||||
rightName := strings.ToLower(right.Name)
|
||||
if leftName != rightName {
|
||||
return leftName < rightName
|
||||
}
|
||||
return left.ID < right.ID
|
||||
})
|
||||
return dashboard
|
||||
}
|
||||
|
||||
//nolint:gocyclo // Job telemetry combines task state, liveness, progress, and device metrics in one aggregation pass.
|
||||
func crackTopJobTelemetry(
|
||||
job *clientpb.CrackJob,
|
||||
stations map[string]*clientpb.Crackstation,
|
||||
now time.Time,
|
||||
taskTelemetry map[string]crackTopTaskTelemetry,
|
||||
) (crackTopJobRow, map[string]crackTopTaskSample, *crackTopProgressAccumulator) {
|
||||
row := crackTopJobRow{
|
||||
ID: strings.TrimSpace(job.GetID()),
|
||||
Status: job.GetStatus(),
|
||||
CreatedAt: crackTopParseTime(job.GetCreatedAt()),
|
||||
UpdatedAt: crackTopParseUnix(job.GetUpdatedAt()),
|
||||
Results: job.GetResultCount(),
|
||||
Mode: crackTopJobMode(job),
|
||||
}
|
||||
progress := newCrackTopProgressAccumulator()
|
||||
samples := map[string]crackTopTaskSample{}
|
||||
workers := map[string]struct{}{}
|
||||
workTasks := 0
|
||||
knownProgressTasks := 0
|
||||
allProgressWeightsKnown := true
|
||||
|
||||
for _, task := range crackJobTasks(job) {
|
||||
if !crackTopCrackTask(task, taskTelemetry) {
|
||||
continue
|
||||
}
|
||||
workTasks++
|
||||
hostID := strings.TrimSpace(task.GetHostUUID())
|
||||
if hostID != "" {
|
||||
workers[hostID] = struct{}{}
|
||||
}
|
||||
status, statusErr := crackTaskStatus(task)
|
||||
progressExact := task.GetShardSkip() == 0
|
||||
if metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]; ok {
|
||||
status = metadata.status
|
||||
statusErr = nil
|
||||
progressExact = metadata.progressExact || task.GetShardSkip() == 0
|
||||
if metadata.parseErr {
|
||||
statusErr = errors.New("crack status telemetry could not be parsed")
|
||||
}
|
||||
}
|
||||
if statusErr != nil {
|
||||
row.TelemetryErrors++
|
||||
}
|
||||
fraction, weight, known := crackTopTaskProgress(task, status, progressExact)
|
||||
if known {
|
||||
progress.add(fraction, weight)
|
||||
knownProgressTasks++
|
||||
} else if weight := crackTopAssignedShardWeight(task); weight != nil {
|
||||
// Unknown work may already be complete, so zero is only a lower
|
||||
// bound. Including its weight prevents known small shards from
|
||||
// overstating the job's completion percentage.
|
||||
progress.add(new(big.Rat), weight)
|
||||
} else {
|
||||
allProgressWeightsKnown = false
|
||||
}
|
||||
sample := crackTopTaskSample{
|
||||
jobID: row.ID,
|
||||
task: task,
|
||||
status: status,
|
||||
statusErr: statusErr,
|
||||
lastSeen: crackTopTaskTime(task),
|
||||
}
|
||||
if status != nil {
|
||||
sample.rate, sample.rateKnown = crackTopStatusRate(status)
|
||||
sample.devices = len(status.Devices)
|
||||
sample.temperature, sample.hasTemp = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Temperature })
|
||||
sample.utilization, sample.hasUtil = crackTopStatusMaximum(status, func(device crackDeviceView) string { return device.Utilization })
|
||||
}
|
||||
if hostID != "" {
|
||||
if current, ok := samples[hostID]; !ok || crackTopSamplePreferred(sample, current, stations[hostID], now) {
|
||||
samples[hostID] = sample
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row.Workers = len(workers)
|
||||
row.Progress, row.ProgressKnown = progress.value()
|
||||
row.ProgressKnown = row.ProgressKnown && workTasks > 0 && allProgressWeightsKnown
|
||||
row.ProgressComplete = row.ProgressKnown && knownProgressTasks == workTasks
|
||||
if job.GetStatus() == clientpb.CrackJobStatus_COMPLETED {
|
||||
row.Progress = 1
|
||||
row.ProgressKnown = true
|
||||
row.ProgressComplete = true
|
||||
}
|
||||
for hostID, sample := range samples {
|
||||
if crackTopTaskSampleIsLive(sample, stations[hostID], now) && sample.rateKnown {
|
||||
row.Rate = saturatingUint64Add(row.Rate, sample.rate)
|
||||
row.RateKnown = true
|
||||
row.Reporting++
|
||||
}
|
||||
}
|
||||
return row, samples, progress
|
||||
}
|
||||
|
||||
func crackTopTaskProgress(task *clientpb.CrackTask, status *crackStatusView, progressExact bool) (*big.Rat, *big.Rat, bool) {
|
||||
if task == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
weight := crackTopAssignedShardWeight(task)
|
||||
if weight == nil {
|
||||
weight = new(big.Rat)
|
||||
}
|
||||
state := task.GetState()
|
||||
if state == clientpb.CrackTaskState_CRACK_TASK_COMPLETED {
|
||||
if weight.Sign() <= 0 && status != nil {
|
||||
_, total, ok := crackTopProgressPair(status)
|
||||
if ok {
|
||||
weight.Set(total)
|
||||
}
|
||||
}
|
||||
if weight.Sign() > 0 {
|
||||
return big.NewRat(1, 1), weight, true
|
||||
}
|
||||
}
|
||||
|
||||
current, total, ok := crackTopProgressPair(status)
|
||||
if ok && (task.GetShardSkip() == 0 || progressExact) {
|
||||
fraction := new(big.Rat).Quo(current, total)
|
||||
if weight.Sign() <= 0 {
|
||||
weight.Set(total)
|
||||
}
|
||||
return fraction, weight, weight.Sign() > 0
|
||||
}
|
||||
if weight.Sign() > 0 && (state == clientpb.CrackTaskState_CRACK_TASK_QUEUED || state == clientpb.CrackTaskState_CRACK_TASK_LEASED) {
|
||||
return new(big.Rat), weight, true
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func crackTopAssignedShardWeight(task *clientpb.CrackTask) *big.Rat {
|
||||
if task == nil || task.GetShardLimit() == 0 {
|
||||
return nil
|
||||
}
|
||||
return new(big.Rat).SetInt(new(big.Int).SetUint64(task.GetShardLimit()))
|
||||
}
|
||||
|
||||
func crackTopProgressPair(status *crackStatusView) (*big.Rat, *big.Rat, bool) {
|
||||
if status == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
current, currentOK := crackTopRat(status.ProgressCurrent)
|
||||
total, totalOK := crackTopRat(status.ProgressTotal)
|
||||
if !currentOK || !totalOK || total.Sign() <= 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return current, total, true
|
||||
}
|
||||
|
||||
func crackTopRat(value string) (*big.Rat, bool) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || len(value) > crackTopMaxProgressTextSize {
|
||||
return nil, false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character < '0' || character > '9' {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
result := new(big.Rat)
|
||||
if _, ok := result.SetString(value); !ok {
|
||||
return nil, false
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func crackTopStatusRate(status *crackStatusView) (uint64, bool) {
|
||||
if status == nil {
|
||||
return 0, false
|
||||
}
|
||||
var total uint64
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
rate, err := strconv.ParseUint(strings.TrimSpace(device.Speed), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
total = saturatingUint64Add(total, rate)
|
||||
}
|
||||
return total, found
|
||||
}
|
||||
|
||||
func crackTopStatusMaximum(status *crackStatusView, value func(crackDeviceView) string) (float64, bool) {
|
||||
if status == nil || value == nil {
|
||||
return 0, false
|
||||
}
|
||||
maximum := 0.0
|
||||
found := false
|
||||
for _, device := range status.Devices {
|
||||
number, err := strconv.ParseFloat(strings.TrimSpace(value(device)), 64)
|
||||
if err != nil || number < 0 || math.IsNaN(number) || math.IsInf(number, 0) {
|
||||
continue
|
||||
}
|
||||
if !found || number > maximum {
|
||||
maximum = number
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return maximum, found
|
||||
}
|
||||
|
||||
func crackTopTaskSampleIsLive(sample crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
if station == nil || sample.task == nil || sample.statusErr != nil || sample.task.GetState() != clientpb.CrackTaskState_CRACK_TASK_RUNNING {
|
||||
return false
|
||||
}
|
||||
leaseExpiresAt := sample.task.GetLeaseExpiresAt()
|
||||
if now.IsZero() || leaseExpiresAt <= now.Unix() {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
return false
|
||||
}
|
||||
if status.GetIsSyncing() || status.GetState() != clientpb.States_CRACKING {
|
||||
return false
|
||||
}
|
||||
currentJobID := strings.TrimSpace(status.GetCurrentCrackJobID())
|
||||
return currentJobID != "" && currentJobID == sample.jobID
|
||||
}
|
||||
|
||||
func crackTopStationExpectsTelemetry(station *clientpb.Crackstation) bool {
|
||||
if station == nil {
|
||||
return false
|
||||
}
|
||||
status := station.GetStatus()
|
||||
if status == nil {
|
||||
// The station is connected, but without status we cannot prove it is
|
||||
// idle. Keep the cluster total unknown instead of reporting a false zero.
|
||||
return true
|
||||
}
|
||||
if status.GetIsSyncing() {
|
||||
return false
|
||||
}
|
||||
return status.GetState() == clientpb.States_CRACKING
|
||||
}
|
||||
|
||||
func crackTopSamplePreferred(left, right crackTopTaskSample, station *clientpb.Crackstation, now time.Time) bool {
|
||||
leftLive := crackTopTaskSampleIsLive(left, station, now)
|
||||
rightLive := crackTopTaskSampleIsLive(right, station, now)
|
||||
if leftLive != rightLive {
|
||||
return leftLive
|
||||
}
|
||||
return crackTopSampleNewer(left, right)
|
||||
}
|
||||
|
||||
func crackTopSampleNewer(left, right crackTopTaskSample) bool {
|
||||
if !left.lastSeen.Equal(right.lastSeen) {
|
||||
return left.lastSeen.After(right.lastSeen)
|
||||
}
|
||||
leftRunning := left.task != nil && left.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
rightRunning := right.task != nil && right.task.GetState() == clientpb.CrackTaskState_CRACK_TASK_RUNNING
|
||||
if leftRunning != rightRunning {
|
||||
return leftRunning
|
||||
}
|
||||
if left.task == nil || right.task == nil {
|
||||
return left.task != nil
|
||||
}
|
||||
return left.task.GetAttempt() > right.task.GetAttempt()
|
||||
}
|
||||
|
||||
func crackTopCrackTask(task *clientpb.CrackTask, taskTelemetry map[string]crackTopTaskTelemetry) bool {
|
||||
if task == nil {
|
||||
return false
|
||||
}
|
||||
switch task.GetKind() {
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_CRACK:
|
||||
return true
|
||||
case clientpb.CrackTaskKind_CRACK_TASK_UNSPECIFIED:
|
||||
if task.GetShardLimit() != 0 || len(task.GetLatestStatusJSON()) != 0 {
|
||||
return true
|
||||
}
|
||||
metadata, ok := taskTelemetry[strings.TrimSpace(task.GetID())]
|
||||
return ok && (metadata.status != nil || metadata.parseErr)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func crackTopStationID(station *clientpb.Crackstation) string {
|
||||
if station == nil {
|
||||
return ""
|
||||
}
|
||||
if hostID := strings.TrimSpace(station.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
if status := station.GetStatus(); status != nil {
|
||||
if hostID := strings.TrimSpace(status.GetHostUUID()); hostID != "" {
|
||||
return hostID
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(station.GetID())
|
||||
}
|
||||
|
||||
func crackTopTaskTime(task *clientpb.CrackTask) time.Time {
|
||||
if task == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
for _, value := range []int64{task.GetLastHeartbeatAt(), task.GetUpdatedAt(), task.GetStartedAt(), task.GetCreatedAt()} {
|
||||
if value > 0 {
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func crackTopParseTime(value string) time.Time {
|
||||
parsed, _ := time.Parse(time.RFC3339, strings.TrimSpace(value))
|
||||
return parsed
|
||||
}
|
||||
|
||||
func crackTopParseUnix(value int64) time.Time {
|
||||
if value <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(value, 0)
|
||||
}
|
||||
|
||||
func crackTopJobMode(job *clientpb.CrackJob) string {
|
||||
if job == nil || job.GetCommand() == nil {
|
||||
return ""
|
||||
}
|
||||
mode := int32(job.GetCommand().GetHashType())
|
||||
if job.GetCommand().HashMode != nil {
|
||||
mode = int32(job.GetCommand().GetHashMode())
|
||||
}
|
||||
if name, ok := hashcatHashTypeName(mode); ok {
|
||||
return name
|
||||
}
|
||||
return "mode " + strconv.FormatInt(int64(mode), 10)
|
||||
}
|
||||
|
||||
func crackTopShortID(value string) string {
|
||||
value = crackTopSafeCell(value)
|
||||
if value == "" {
|
||||
return "unknown"
|
||||
}
|
||||
if index := strings.IndexByte(value, '-'); index > 0 {
|
||||
value = value[:index]
|
||||
}
|
||||
runes := []rune(value)
|
||||
if len(runes) > 8 {
|
||||
value = string(runes[:8])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func crackTopSafeCell(value string) string {
|
||||
value = ansi.Strip(value)
|
||||
value = strings.Map(func(r rune) rune {
|
||||
if !unicode.IsPrint(r) {
|
||||
return -1
|
||||
}
|
||||
if unicode.IsSpace(r) && r != ' ' {
|
||||
return ' '
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
value = strings.TrimSpace(value)
|
||||
const maximumRunes = 96
|
||||
runes := []rune(value)
|
||||
if len(runes) > maximumRunes {
|
||||
value = string(runes[:maximumRunes-1]) + "…"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func saturatingUint64Add(left, right uint64) uint64 {
|
||||
if math.MaxUint64-left < right {
|
||||
return math.MaxUint64
|
||||
}
|
||||
return left + right
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+279
-46
@@ -22,6 +22,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -30,10 +32,75 @@ import (
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
)
|
||||
|
||||
// CrackCmd - GPU password cracking interface
|
||||
func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
invocation, err := classifyCrackInvocation(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationBackendInfo {
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
return
|
||||
}
|
||||
printCrackersWithBackendInfoLevel(crackers, con, false, invocation.backendInfoLevel)
|
||||
return
|
||||
}
|
||||
|
||||
if invocation.mode == crackInvocationJob || invocation.isSynchronousQuery() {
|
||||
crackCmd, err := buildCrackCommand(cmd, args)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
if err := resolveManagedCrackFileReferences(ctx, crackCmd, func(ctx context.Context, request *clientpb.CrackFile) (*clientpb.CrackFiles, error) {
|
||||
return con.Rpc.CrackFilesList(ctx, request)
|
||||
}); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := con.Rpc.Crack(ctx, crackCmd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if invocation.isSynchronousQuery() {
|
||||
value, stderr, err := crackQueryResponse(resp, invocation)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
printCrackQueryStderr(con, stderr)
|
||||
printCrackQueryValue(con, value)
|
||||
return
|
||||
}
|
||||
if resp == nil || resp.Job == nil {
|
||||
con.PrintInfof("Crack request submitted\n")
|
||||
return
|
||||
}
|
||||
con.PrintInfof("Crack job %s created (status: %s)\n", resp.Job.ID, resp.Job.Status.String())
|
||||
if resp.Job.Err != "" {
|
||||
con.PrintErrorf("Crack job error: %s\n", resp.Job.Err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !AreCrackersOnline(con) {
|
||||
PrintNoCrackstations(con)
|
||||
} else {
|
||||
@@ -57,20 +124,139 @@ func CrackCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackKeyspace(response *clientpb.CrackResponse) (string, error) {
|
||||
return crackQueryResponseValue(response, crackInvocation{mode: crackInvocationKeyspace})
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) isSynchronousQuery() bool {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates, crackInvocationLookup, crackInvocationIdentify, crackInvocationHashInfo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (invocation crackInvocation) queryMode() clientpb.CrackQueryMode {
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_KEYSPACE
|
||||
case crackInvocationTotalCandidates:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_TOTAL_CANDIDATES
|
||||
case crackInvocationLookup:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_LOOKUP
|
||||
case crackInvocationIdentify:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_IDENTIFY
|
||||
case crackInvocationHashInfo:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_HASH_INFO
|
||||
default:
|
||||
return clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func crackQueryResponseValue(response *clientpb.CrackResponse, invocation crackInvocation) (string, error) {
|
||||
value, _, err := crackQueryResponse(response, invocation)
|
||||
return value, err
|
||||
}
|
||||
|
||||
func crackQueryResponse(response *clientpb.CrackResponse, invocation crackInvocation) (string, string, error) {
|
||||
if response == nil {
|
||||
return "", "", fmt.Errorf("server returned an empty %s response", crackInvocationName(invocation.mode))
|
||||
}
|
||||
expectedMode := invocation.queryMode()
|
||||
if expectedMode == clientpb.CrackQueryMode_CRACK_QUERY_UNSPECIFIED {
|
||||
return "", "", fmt.Errorf("invalid synchronous crack query mode")
|
||||
}
|
||||
query := response.GetQuery()
|
||||
if query == nil {
|
||||
// Servers predating typed query results returned only Keyspace. Do not use
|
||||
// that legacy field for a selected station because it cannot prove which
|
||||
// station produced the result.
|
||||
if invocation.mode == crackInvocationKeyspace && invocation.crackstation == "" {
|
||||
value, err := canonicalCrackDecimal(response.GetKeyspace(), "keyspace")
|
||||
return value, "", err
|
||||
}
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
if query.GetMode() != expectedMode {
|
||||
return "", "", fmt.Errorf("server returned %s for %s query", query.GetMode().String(), crackInvocationName(invocation.mode))
|
||||
}
|
||||
value := query.GetValue()
|
||||
switch invocation.mode {
|
||||
case crackInvocationKeyspace, crackInvocationTotalCandidates:
|
||||
value, err := canonicalCrackDecimal(value, crackInvocationName(invocation.mode))
|
||||
return value, query.GetStderr(), err
|
||||
default:
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "", "", fmt.Errorf("server returned an empty %s query result", crackInvocationName(invocation.mode))
|
||||
}
|
||||
return value, query.GetStderr(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalCrackDecimal(value string, name string) (string, error) {
|
||||
raw := strings.TrimSpace(value)
|
||||
keyspace, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("server returned invalid %s %q", name, value)
|
||||
}
|
||||
return strconv.FormatUint(keyspace, 10), nil
|
||||
}
|
||||
|
||||
func printCrackQueryValue(con *console.SliverClient, value string) {
|
||||
con.Printf("%s", value)
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
con.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
func printCrackQueryStderr(con *console.SliverClient, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(value, "\n") {
|
||||
value += "\n"
|
||||
}
|
||||
con.PrintWarnf("%s", value)
|
||||
}
|
||||
|
||||
// CrackStationsCmd - Manage GPU cracking stations
|
||||
func CrackStationsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
crackers, err := con.Rpc.Crackstations(context.Background(), &commonpb.Empty{})
|
||||
ctx, cancel := crackCommandContext(cmd.Context(), cmd)
|
||||
defer cancel()
|
||||
crackers, err := connectedCrackstations(ctx, con.Rpc)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if len(crackers.Crackstations) == 0 {
|
||||
showBenchmarks, _ := cmd.Flags().GetBool("show-benchmarks")
|
||||
if len(crackers) == 0 {
|
||||
PrintNoCrackstations(con)
|
||||
} else {
|
||||
PrintCrackers(crackers.Crackstations, con)
|
||||
PrintCrackers(crackers, con, showBenchmarks)
|
||||
}
|
||||
}
|
||||
|
||||
func connectedCrackstations(ctx context.Context, rpc rpcpb.SliverRPCClient) ([]*clientpb.Crackstation, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
response, err := rpc.Crackstations(ctx, &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response == nil {
|
||||
return nil, nil
|
||||
}
|
||||
crackers := make([]*clientpb.Crackstation, 0, len(response.Crackstations))
|
||||
for _, cracker := range response.Crackstations {
|
||||
if cracker != nil {
|
||||
crackers = append(crackers, cracker)
|
||||
}
|
||||
}
|
||||
return crackers, nil
|
||||
}
|
||||
|
||||
func PrintNoCrackstations(con *console.SliverClient) {
|
||||
con.PrintInfof("No crackstations connected to server\n")
|
||||
}
|
||||
@@ -83,66 +269,113 @@ func AreCrackersOnline(con *console.SliverClient) bool {
|
||||
return len(crackers.Crackstations) > 0
|
||||
}
|
||||
|
||||
func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient) {
|
||||
sort.Slice(crackers, func(i, j int) bool {
|
||||
return crackers[i].Name < crackers[j].Name
|
||||
func PrintCrackers(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool) {
|
||||
printCrackersWithBackendInfoLevel(crackers, con, showBenchmarks, 1)
|
||||
}
|
||||
|
||||
func printCrackersWithBackendInfoLevel(crackers []*clientpb.Crackstation, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
ordered := make([]*clientpb.Crackstation, 0, len(crackers))
|
||||
for _, cracker := range crackers {
|
||||
if cracker != nil {
|
||||
ordered = append(ordered, cracker)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
if ordered[i].Name == ordered[j].Name {
|
||||
return ordered[i].HostUUID < ordered[j].HostUUID
|
||||
}
|
||||
return ordered[i].Name < ordered[j].Name
|
||||
})
|
||||
for index, cracker := range crackers {
|
||||
printCracker(cracker, index, con)
|
||||
if index < len(crackers)-1 {
|
||||
for index, cracker := range ordered {
|
||||
printCracker(cracker, index, con, showBenchmarks, backendInfoLevel)
|
||||
if index < len(ordered)-1 {
|
||||
con.Println()
|
||||
con.Println()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient) {
|
||||
func printCracker(cracker *clientpb.Crackstation, index int, con *console.SliverClient, showBenchmarks bool, backendInfoLevel uint32) {
|
||||
con.Printf("%s\n", renderCracker(cracker, index, settings.GetTableStyle(con), backendInfoLevel))
|
||||
if showBenchmarks {
|
||||
con.Println()
|
||||
printBenchmarks(cracker, con)
|
||||
}
|
||||
}
|
||||
|
||||
func renderCracker(cracker *clientpb.Crackstation, index int, style table.Style, backendInfoLevel uint32) string {
|
||||
if cracker == nil {
|
||||
return ""
|
||||
}
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + console.Orange + fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, cracker.Name, cracker.OperatorName) + console.Normal + "\n")
|
||||
tw.SetStyle(style)
|
||||
tw.SetTitle(console.StyleBoldOrange.Render(fmt.Sprintf(">>> Crackstation %02d - %s (%s)", index+1, safeCrackCell(cracker.Name), safeCrackCell(cracker.OperatorName))) + "\n")
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "Operating System" + console.Normal, fmt.Sprintf("%s/%s", cracker.GOOS, cracker.GOARCH)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Hashcat Version" + console.Normal, cracker.HashcatVersion})
|
||||
if 0 < len(cracker.CUDA) {
|
||||
for _, cuda := range cracker.CUDA {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "CUDA Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, cuda.Name, cuda.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", cuda.MemoryFree, cuda.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", cuda.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", cuda.Processors)})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Operating System"), fmt.Sprintf("%s/%s", valueOrDash(cracker.GOOS), valueOrDash(cracker.GOARCH))})
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Hashcat Version"), valueOrDash(cracker.HashcatVersion)})
|
||||
if backendInfoLevel >= 2 {
|
||||
appendCrackstationDetailRows(tw, cracker)
|
||||
}
|
||||
for _, cuda := range cracker.CUDA {
|
||||
appendCUDABackendRows(tw, cuda, backendInfoLevel)
|
||||
}
|
||||
for _, hip := range cracker.HIP {
|
||||
appendHIPBackendRows(tw, hip, backendInfoLevel)
|
||||
}
|
||||
for _, metal := range cracker.Metal {
|
||||
appendMetalBackendRows(tw, metal, backendInfoLevel)
|
||||
}
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
appendOpenCLBackendRows(tw, openCL, backendInfoLevel)
|
||||
}
|
||||
if !hasReportedBackendDevice(cracker) {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.StyleBold.Render("Backend Devices"), "No backend devices reported"})
|
||||
}
|
||||
return tw.Render()
|
||||
}
|
||||
|
||||
func formatCUDADevice(device *clientpb.CUDABackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetCUDAVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatHIPDevice(device *clientpb.HIPBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetHIPVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatMetalDevice(device *clientpb.MetalBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetMetalVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatOpenCLDevice(device *clientpb.OpenCLBackendInfo) string {
|
||||
return formatBackendDevice(device.GetName(), device.GetOpenCLVersion(), device.GetVersion())
|
||||
}
|
||||
|
||||
func formatBackendDevice(name string, versions ...string) string {
|
||||
for _, version := range versions {
|
||||
if version != "" {
|
||||
return fmt.Sprintf("%s (%s)", name, version)
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.Metal) {
|
||||
for _, metal := range cracker.Metal {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "Metal Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+"%s (%s)"+console.Normal, metal.Name, metal.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", metal.MemoryFree, metal.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", metal.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", metal.Processors)})
|
||||
}
|
||||
}
|
||||
if 0 < len(cracker.OpenCL) {
|
||||
for _, openCL := range cracker.OpenCL {
|
||||
tw.AppendSeparator()
|
||||
tw.AppendRow(table.Row{console.Bold + "OpenCL Device" + console.Normal, fmt.Sprintf(console.Bold+console.Green+console.Bold+"%s (%s)"+console.Normal, openCL.Name, openCL.Version)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Memory" + console.Normal, fmt.Sprintf("%s free of %s", openCL.MemoryFree, openCL.MemoryTotal)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Clock" + console.Normal, fmt.Sprintf("%d", openCL.Clock)})
|
||||
tw.AppendRow(table.Row{console.Bold + "Processors" + console.Normal, fmt.Sprintf("%d", openCL.Processors)})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
con.Println()
|
||||
printBenchmarks(cracker, con)
|
||||
return name
|
||||
}
|
||||
|
||||
func printBenchmarks(cracker *clientpb.Crackstation, con *console.SliverClient) {
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
tw.SetTitle(console.Bold + "Benchmarks" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Benchmarks"))
|
||||
tw.SortBy([]table.SortBy{{Name: "Hash Type"}})
|
||||
tw.AppendHeader(table.Row{"Hash Type", "Rate (H/s)"})
|
||||
for hashType, speed := range cracker.Benchmarks {
|
||||
tw.AppendRow(table.Row{clientpb.HashType(hashType), fmt.Sprintf("%d", speed)})
|
||||
tw.AppendHeader(table.Row{"Hash Type", "Rate"})
|
||||
if len(cracker.Benchmarks) == 0 {
|
||||
tw.AppendRow(table.Row{"No benchmarks reported", "-"})
|
||||
} else {
|
||||
for hashType, speed := range cracker.Benchmarks {
|
||||
name, ok := hashcatHashTypeName(hashType)
|
||||
if !ok {
|
||||
name = clientpb.HashType(hashType).String()
|
||||
}
|
||||
tw.AppendRow(table.Row{name, humanizeHashRate(speed)})
|
||||
}
|
||||
}
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
func TestFormatBackendDevice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
device string
|
||||
versions []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "specific runtime version wins",
|
||||
device: "Apple M5",
|
||||
versions: []string{"Metal 410.7", "legacy-generic"},
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy generic fallback",
|
||||
device: "NVIDIA GPU",
|
||||
versions: []string{"", "13.0"},
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "no empty version suffix",
|
||||
device: "AMD GPU",
|
||||
versions: []string{"", ""},
|
||||
want: "AMD GPU",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := formatBackendDevice(test.device, test.versions...); got != test.want {
|
||||
t.Fatalf("formatBackendDevice() = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendSpecificDisplayVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "CUDA version",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", CUDAVersion: "13.0", Version: "legacy",
|
||||
}),
|
||||
want: "NVIDIA GPU (13.0)",
|
||||
},
|
||||
{
|
||||
name: "HIP version",
|
||||
got: formatHIPDevice(&clientpb.HIPBackendInfo{
|
||||
Name: "AMD GPU", HIPVersion: "7.0.51831", Version: "legacy",
|
||||
}),
|
||||
want: "AMD GPU (7.0.51831)",
|
||||
},
|
||||
{
|
||||
name: "Metal version",
|
||||
got: formatMetalDevice(&clientpb.MetalBackendInfo{
|
||||
Name: "Apple M5", MetalVersion: "Metal 410.7", Version: "legacy",
|
||||
}),
|
||||
want: "Apple M5 (Metal 410.7)",
|
||||
},
|
||||
{
|
||||
name: "legacy CUDA fallback",
|
||||
got: formatCUDADevice(&clientpb.CUDABackendInfo{
|
||||
Name: "NVIDIA GPU", Version: "12.9",
|
||||
}),
|
||||
want: "NVIDIA GPU (12.9)",
|
||||
},
|
||||
{
|
||||
name: "OpenCL version",
|
||||
got: formatOpenCLDevice(&clientpb.OpenCLBackendInfo{
|
||||
Name: "Portable GPU", OpenCLVersion: "OpenCL 3.0", Version: "legacy",
|
||||
}),
|
||||
want: "Portable GPU (OpenCL 3.0)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if test.got != test.want {
|
||||
t.Fatalf("backend display = %q, want %q", test.got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelTwo(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
OperatorName: "operator",
|
||||
HostUUID: "host-uuid",
|
||||
Version: "station-v1",
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
HashcatVersion: "v7.1.2",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "CUDA GPU", CUDAVersion: "CUDA 13", Version: "device-v1", Type: "GPU",
|
||||
Vendor: "NVIDIA", VendorID: 4318, BackendDeviceID: 1, BackendDeviceAlias: &zero,
|
||||
PreferredThreadSize: &zero, MemoryUnified: &falseValue, LocalMemory: "64 KB",
|
||||
CacheSize: "8 MB", PCIAddress: "01:00.0", MemoryFree: "8 GB", MemoryTotal: "12 GB",
|
||||
}},
|
||||
HIP: []*clientpb.HIPBackendInfo{{Name: "HIP GPU", HIPVersion: "HIP 7", BackendDeviceID: 2}},
|
||||
Metal: []*clientpb.MetalBackendInfo{{
|
||||
Name: "Metal GPU", MetalVersion: "Metal 410", BackendDeviceID: 3,
|
||||
RegistryID: &zero, GPUHeadless: &falseValue, GPULowPower: &falseValue, GPURemovable: &falseValue,
|
||||
PhysicalLocation: "built-in", MaxTXRate: "100", MemoryAllocPerBlock: "4 GB",
|
||||
}},
|
||||
OpenCL: []*clientpb.OpenCLBackendInfo{{
|
||||
Name: "OpenCL GPU", OpenCLVersion: "OpenCL 3.0", OpenCLDriverVersion: "driver-v1",
|
||||
BackendDeviceID: 4, PlatformVendor: "Portable", PlatformName: "Platform",
|
||||
PlatformVersion: "platform-v1", MemoryAllocPerBlock: "2 GB",
|
||||
}},
|
||||
}
|
||||
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 2)
|
||||
for _, expected := range []string{
|
||||
"station", "operator", "host-uuid", "station-v1", "linux/amd64", "v7.1.2",
|
||||
"CUDA GPU", "HIP GPU", "Metal GPU", "OpenCL GPU", "OpenCL 3.0", "driver-v1",
|
||||
"Backend Device ID", "Backend Device Alias", "Preferred Thread Size", "Unified Memory",
|
||||
"NVIDIA", "4318", "device-v1", "64 KB", "8 MB", "01:00.0",
|
||||
"Registry ID", "Headless", "Low Power", "Removable", "built-in", "100",
|
||||
"OpenCL Platform ID", "Portable", "Platform", "platform-v1", "2 GB",
|
||||
} {
|
||||
if !strings.Contains(output, expected) {
|
||||
t.Errorf("level-2 backend info does not contain %q:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerBackendInfoLevelOneOmitsDetails(t *testing.T) {
|
||||
station := &clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
Name: "GPU", BackendDeviceID: 7, PCIAddress: "01:00.0",
|
||||
}},
|
||||
}
|
||||
output := renderCracker(station, 0, settings.SliverDefault, 1)
|
||||
for _, unexpected := range []string{"Host UUID", "Backend Device ID", "PCI Address"} {
|
||||
if strings.Contains(output, unexpected) {
|
||||
t.Errorf("level-1 backend info unexpectedly contains %q:\n%s", unexpected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerPreservesOptionalPresence(t *testing.T) {
|
||||
zero := uint32(0)
|
||||
falseValue := false
|
||||
withPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{
|
||||
BackendDeviceAlias: &zero,
|
||||
MemoryUnified: &falseValue,
|
||||
}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if !strings.Contains(withPresence, "Backend Device Alias") || !strings.Contains(withPresence, "Unified Memory") {
|
||||
t.Fatalf("present zero/false optional fields were omitted:\n%s", withPresence)
|
||||
}
|
||||
|
||||
withoutPresence := renderCracker(&clientpb.Crackstation{
|
||||
CUDA: []*clientpb.CUDABackendInfo{{}},
|
||||
}, 0, settings.SliverDefault, 2)
|
||||
if strings.Contains(withoutPresence, "Backend Device Alias") || strings.Contains(withoutPresence, "Unified Memory") {
|
||||
t.Fatalf("absent optional fields were rendered:\n%s", withoutPresence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCrackerReportsNoBackendDevices(t *testing.T) {
|
||||
output := renderCracker(&clientpb.Crackstation{
|
||||
Name: "station",
|
||||
CUDA: []*clientpb.CUDABackendInfo{nil},
|
||||
}, 0, settings.SliverDefault, 1)
|
||||
if !strings.Contains(output, "No backend devices reported") {
|
||||
t.Fatalf("missing no-device message:\n%s", output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package crack
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Hashcat mode names sourced from client/credentials/EXAMPLES.md.
|
||||
var hashcatHashTypes = map[int32]string{
|
||||
0: "MD5",
|
||||
10: "md5($pass.$salt)",
|
||||
11: "Joomla < 2.5.18",
|
||||
12: "PostgreSQL",
|
||||
20: "md5($salt.$pass)",
|
||||
21: "osCommerce, xt:Commerce",
|
||||
22: "Juniper NetScreen/SSG (ScreenOS)",
|
||||
23: "Skype",
|
||||
24: "SolarWinds Serv-U",
|
||||
30: "md5(utf16le($pass).$salt)",
|
||||
40: "md5($salt.utf16le($pass))",
|
||||
50: "HMAC-MD5 (key = $pass)",
|
||||
60: "HMAC-MD5 (key = $salt)",
|
||||
70: "md5(utf16le($pass))",
|
||||
100: "SHA1",
|
||||
101: "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
110: "sha1($pass.$salt)",
|
||||
111: "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
112: "Oracle S: Type (Oracle 11+)",
|
||||
120: "sha1($salt.$pass)",
|
||||
121: "SMF (Simple Machines Forum) > v1.1",
|
||||
122: "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
123: "EPi",
|
||||
124: "Django (SHA-1)",
|
||||
125: "ArubaOS",
|
||||
130: "sha1(utf16le($pass).$salt)",
|
||||
131: "MSSQL (2000)",
|
||||
132: "MSSQL (2005)",
|
||||
133: "PeopleSoft",
|
||||
140: "sha1($salt.utf16le($pass))",
|
||||
141: "Episerver 6.x < .NET 4",
|
||||
150: "HMAC-SHA1 (key = $pass)",
|
||||
160: "HMAC-SHA1 (key = $salt)",
|
||||
170: "sha1(utf16le($pass))",
|
||||
190: "sha1(LinkedIn)",
|
||||
200: "MySQL323",
|
||||
300: "MySQL4.1/MySQL5",
|
||||
400: "phpass, phpBB3 (MD5)",
|
||||
500: "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
501: "Juniper IVE",
|
||||
600: "BLAKE2b-512",
|
||||
900: "MD4",
|
||||
1000: "NTLM",
|
||||
1100: "Domain Cached Credentials (DCC), MS Cache",
|
||||
1300: "SHA2-224",
|
||||
1400: "SHA2-256",
|
||||
1410: "sha256($pass.$salt)",
|
||||
1411: "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
1420: "sha256($salt.$pass)",
|
||||
1421: "hMailServer",
|
||||
1430: "sha256(utf16le($pass).$salt)",
|
||||
1431: "base64(sha256(unicode($pass)))",
|
||||
1440: "sha256($salt.utf16le($pass))",
|
||||
1441: "Episerver 6.x >= .NET 4",
|
||||
1450: "HMAC-SHA256 (key = $pass)",
|
||||
1460: "HMAC-SHA256 (key = $salt)",
|
||||
1470: "sha256(utf16le($pass))",
|
||||
1500: "descrypt, DES (Unix), Traditional DES",
|
||||
1600: "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
1700: "SHA2-512",
|
||||
1710: "sha512($pass.$salt)",
|
||||
1711: "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
1720: "sha512($salt.$pass)",
|
||||
1722: "macOS v10.7",
|
||||
1730: "sha512(utf16le($pass).$salt)",
|
||||
1731: "MSSQL (2012, 2014)",
|
||||
1740: "sha512($salt.utf16le($pass))",
|
||||
1750: "HMAC-SHA512 (key = $pass)",
|
||||
1760: "HMAC-SHA512 (key = $salt)",
|
||||
1770: "sha512(utf16le($pass))",
|
||||
1800: "sha512crypt $6$, SHA512 (Unix)",
|
||||
2000: "STDOUT",
|
||||
2100: "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
2400: "Cisco-PIX MD5",
|
||||
2410: "Cisco-ASA MD5",
|
||||
2500: "WPA-EAPOL-PBKDF2",
|
||||
2501: "WPA-EAPOL-PMK",
|
||||
2600: "md5(md5($pass))",
|
||||
2611: "vBulletin < v3.8.5",
|
||||
2612: "PHPS",
|
||||
2711: "vBulletin >= v3.8.5",
|
||||
2811: "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
3000: "LM",
|
||||
3100: "Oracle H: Type (Oracle 7+)",
|
||||
3200: "bcrypt $2\\*$, Blowfish (Unix)",
|
||||
3300: "MD5(Sun)",
|
||||
3500: "md5(md5(md5($pass)))",
|
||||
3610: "md5(md5($salt).$pass )",
|
||||
3710: "md5($salt.md5($pass))",
|
||||
3711: "MediaWiki B type",
|
||||
3720: "md5($pass.md5($salt))",
|
||||
3721: "WebEdition CMS",
|
||||
3800: "md5($salt.$pass.$salt)",
|
||||
3910: "md5(md5($pass).md5($salt))",
|
||||
4010: "md5($salt.md5($salt.$pass))",
|
||||
4110: "md5($salt.md5($pass.$salt))",
|
||||
4210: "md5($username.0.$pass)",
|
||||
4300: "md5(strtoupper(md5($pass)))",
|
||||
4400: "md5(sha1($pass))",
|
||||
4500: "sha1(sha1($pass))",
|
||||
4510: "sha1(sha1($pass).$salt)",
|
||||
4520: "sha1($salt.sha1($pass))",
|
||||
4521: "Redmine",
|
||||
4522: "PunBB",
|
||||
4600: "sha1(sha1(sha1($pass)))",
|
||||
4700: "sha1(md5($pass))",
|
||||
4710: "sha1(md5($pass).$salt)",
|
||||
4711: "Huawei sha1(md5($pass).$salt)",
|
||||
4800: "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
4900: "sha1($salt.$pass.$salt)",
|
||||
5000: "sha1(sha1($salt.$pass.$salt))",
|
||||
5100: "Half MD5",
|
||||
5200: "Password Safe v3",
|
||||
5300: "IKE-PSK MD5",
|
||||
5400: "IKE-PSK SHA1",
|
||||
5500: "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
5600: "NetNTLMv2",
|
||||
5700: "Cisco-IOS type 4 (SHA256)",
|
||||
5800: "Samsung Android Password/PIN",
|
||||
6000: "RIPEMD-160",
|
||||
6100: "Whirlpool",
|
||||
6211: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
6212: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
6213: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
6221: "TrueCrypt 5.0+ SHA512 + AES / TrueCrypt 5.0+ SHA512 + Serpent / TrueCrypt 5.0+ SHA512 + Twofish",
|
||||
6222: "TrueCrypt 5.0+ SHA512 + AES-Twofish / TrueCrypt 5.0+ SHA512 + Serpent-AES / TrueCrypt 5.0+ SHA512 + Twofish-Serpent",
|
||||
6223: "TrueCrypt 5.0+ SHA512 + AES-Twofish-Serpent / TrueCrypt 5.0+ SHA512 + Serpent-Twofish-AES",
|
||||
6231: "TrueCrypt 5.0+ Whirlpool + AES / TrueCrypt 5.0+ Whirlpool + Serpent / TrueCrypt 5.0+ Whirlpool + Twofish",
|
||||
6232: "TrueCrypt 5.0+ Whirlpool + AES-Twofish / TrueCrypt 5.0+ Whirlpool + Serpent-AES / TrueCrypt 5.0+ Whirlpool + Twofish-Serpent",
|
||||
6233: "TrueCrypt 5.0+ Whirlpool + AES-Twofish-Serpent / TrueCrypt 5.0+ Whirlpool + Serpent-Twofish-AES",
|
||||
6241: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish + boot",
|
||||
6242: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-AES + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent + boot",
|
||||
6243: "TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + AES-Twofish-Serpent + boot / TrueCrypt 5.0+ PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES + boot",
|
||||
6300: "AIX {smd5}",
|
||||
6400: "AIX {ssha256}",
|
||||
6500: "AIX {ssha512}",
|
||||
6600: "1Password, agilekeychain",
|
||||
6700: "AIX {ssha1}",
|
||||
6800: "LastPass + LastPass sniffed",
|
||||
6900: "GOST R 34.11-94",
|
||||
7000: "FortiGate (FortiOS)",
|
||||
7100: "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
7200: "GRUB 2",
|
||||
7300: "IPMI2 RAKP HMAC-SHA1",
|
||||
7400: "sha256crypt $5$, SHA256 (Unix)",
|
||||
7401: "MySQL $A$ (sha256crypt)",
|
||||
7500: "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
7700: "SAP CODVN B (BCODE)",
|
||||
7701: "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
7800: "SAP CODVN F/G (PASSCODE)",
|
||||
7801: "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
7900: "Drupal7",
|
||||
8000: "Sybase ASE",
|
||||
8100: "Citrix NetScaler (SHA1)",
|
||||
8200: "1Password, cloudkeychain",
|
||||
8300: "DNSSEC (NSEC3)",
|
||||
8400: "WBB3 (Woltlab Burning Board)",
|
||||
8500: "RACF",
|
||||
8600: "Lotus Notes/Domino 5",
|
||||
8700: "Lotus Notes/Domino 6",
|
||||
8800: "Android FDE <= 4.3",
|
||||
8900: "scrypt",
|
||||
9000: "Password Safe v2",
|
||||
9100: "Lotus Notes/Domino 8",
|
||||
9200: "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
9300: "Cisco-IOS $9$ (scrypt)",
|
||||
9400: "MS Office 2007",
|
||||
9500: "MS Office 2010",
|
||||
9600: "MS Office 2013",
|
||||
9700: "MS Office <= 2003 MD5 + RC4, oldoffice$0, oldoffice$1",
|
||||
9710: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
9720: "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
9800: "MS Office <= 2003 SHA1 + RC4, oldoffice$3, oldoffice$4",
|
||||
9810: "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
9820: "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
9900: "Radmin2",
|
||||
10000: "Django (PBKDF2-SHA256)",
|
||||
10100: "SipHash",
|
||||
10200: "CRAM-MD5",
|
||||
10300: "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
10400: "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
10410: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
10420: "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
10500: "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
10600: "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
10700: "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
10800: "SHA2-384",
|
||||
10810: "sha384($pass.$salt)",
|
||||
10820: "sha384($salt.$pass)",
|
||||
10830: "sha384(utf16le($pass).$salt)",
|
||||
10840: "sha384($salt.utf16le($pass))",
|
||||
10870: "sha384(utf16le($pass))",
|
||||
10900: "PBKDF2-HMAC-SHA256",
|
||||
10901: "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
11000: "PrestaShop",
|
||||
11100: "PostgreSQL CRAM (MD5)",
|
||||
11200: "MySQL CRAM (SHA1)",
|
||||
11300: "Bitcoin/Litecoin wallet.dat",
|
||||
11400: "SIP digest authentication (MD5)",
|
||||
11500: "CRC32",
|
||||
11600: "7-Zip",
|
||||
11700: "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
11750: "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
11760: "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
11800: "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
11850: "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
11860: "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
11900: "PBKDF2-HMAC-MD5",
|
||||
12000: "PBKDF2-HMAC-SHA1",
|
||||
12001: "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
12100: "PBKDF2-HMAC-SHA512",
|
||||
12200: "eCryptfs",
|
||||
12300: "Oracle T: Type (Oracle 12+)",
|
||||
12400: "BSDi Crypt, Extended DES",
|
||||
12500: "RAR3-hp",
|
||||
12600: "ColdFusion 10+",
|
||||
12700: "Blockchain, My Wallet",
|
||||
12800: "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
12900: "Android FDE (Samsung DEK)",
|
||||
13000: "RAR5",
|
||||
13100: "Kerberos 5, etype 23, TGS-REP",
|
||||
13200: "AxCrypt 1",
|
||||
13300: "AxCrypt 1 in-memory SHA1",
|
||||
13400: "KeePass 1 AES / without keyfile / KeePass 2 AES / without keyfile / KeePass 1 Twofish / with keyfile / Keepass 2 AES / with keyfile",
|
||||
13500: "PeopleSoft PS_TOKEN",
|
||||
13600: "WinZip",
|
||||
13711: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish",
|
||||
13712: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + AES-Twofish / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-AES / VeraCrypt PBKDF2-HMAC-RIPEMD160 + Twofish-Serpent",
|
||||
13713: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + Serpent-Twofish-AES",
|
||||
13721: "VeraCrypt PBKDF2-HMAC-SHA512 + AES / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish",
|
||||
13722: "VeraCrypt PBKDF2-HMAC-SHA512 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA512 + Twofish-Serpent",
|
||||
13723: "VeraCrypt PBKDF2-HMAC-SHA512 + Serpent-Twofish-AES",
|
||||
13731: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish",
|
||||
13732: "VeraCrypt PBKDF2-HMAC-Whirlpool + AES-Twofish / VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-AES / VeraCrypt PBKDF2-HMAC-Whirlpool + Twofish-Serpent",
|
||||
13733: "VeraCrypt PBKDF2-HMAC-Whirlpool + Serpent-Twofish-AES",
|
||||
13741: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES",
|
||||
13742: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish",
|
||||
13743: "VeraCrypt PBKDF2-HMAC-RIPEMD160 + boot-mode + AES-Twofish-Serpent",
|
||||
13751: "VeraCrypt PBKDF2-HMAC-SHA256 + AES / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish",
|
||||
13752: "VeraCrypt PBKDF2-HMAC-SHA256 + AES-Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-AES / VeraCrypt PBKDF2-HMAC-SHA256 + Twofish-Serpent",
|
||||
13753: "VeraCrypt PBKDF2-HMAC-SHA256 + Serpent-Twofish-AES",
|
||||
13761: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Twofish / VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + PIM + AES",
|
||||
13762: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-AES",
|
||||
13763: "VeraCrypt PBKDF2-HMAC-SHA256 + boot-mode + Serpent-Twofish-AES",
|
||||
13771: "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
13772: "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
13773: "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
13800: "Windows Phone 8+ PIN/password",
|
||||
13900: "OpenCart",
|
||||
14000: "DES (PT = $salt, key = $pass)",
|
||||
14100: "3DES (PT = $salt, key = $pass)",
|
||||
14400: "sha1(CX)",
|
||||
14500: "Linux Kernel Crypto API (2.4)",
|
||||
14600: "LUKS",
|
||||
14700: "iTunes backup < 10.0",
|
||||
14800: "iTunes backup >= 10.0",
|
||||
14900: "Skip32 (PT = $salt, key = $pass)",
|
||||
15000: "FileZilla Server >= 0.9.55",
|
||||
15100: "Juniper/NetBSD sha1crypt",
|
||||
15200: "Blockchain, My Wallet, V2",
|
||||
15300: "DPAPI masterkey file v1 + local context",
|
||||
15400: "ChaCha20",
|
||||
15500: "JKS Java Key Store Private Keys (SHA1)",
|
||||
15600: "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
15700: "Ethereum Wallet, SCRYPT",
|
||||
15900: "DPAPI masterkey file v2 + Active Directory domain context",
|
||||
16000: "Tripcode",
|
||||
16100: "TACACS+",
|
||||
16200: "Apple Secure Notes",
|
||||
16300: "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
16400: "CRAM-MD5 Dovecot",
|
||||
16500: "JWT (JSON Web Token)",
|
||||
16600: "Electrum Wallet (Salt-Type 1-3)",
|
||||
16700: "FileVault 2",
|
||||
16800: "WPA-PMKID-PBKDF2",
|
||||
16801: "WPA-PMKID-PMK",
|
||||
16900: "Ansible Vault",
|
||||
17200: "PKZIP (Compressed)",
|
||||
17210: "PKZIP (Uncompressed)",
|
||||
17220: "PKZIP (Compressed Multi-File)",
|
||||
17225: "PKZIP (Mixed Multi-File)",
|
||||
17230: "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
17300: "SHA3-224",
|
||||
17400: "SHA3-256",
|
||||
17500: "SHA3-384",
|
||||
17600: "SHA3-512",
|
||||
17700: "Keccak-224",
|
||||
17800: "Keccak-256",
|
||||
17900: "Keccak-384",
|
||||
18000: "Keccak-512",
|
||||
18100: "TOTP (HMAC-SHA1)",
|
||||
18200: "Kerberos 5, etype 23, AS-REP",
|
||||
18300: "Apple File System (APFS)",
|
||||
18400: "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
18500: "sha1(md5(md5($pass)))",
|
||||
18600: "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
18700: "Java Object hashCode()",
|
||||
18800: "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
18900: "Android Backup",
|
||||
19000: "QNX /etc/shadow (MD5)",
|
||||
19100: "QNX /etc/shadow (SHA256)",
|
||||
19200: "QNX /etc/shadow (SHA512)",
|
||||
19300: "sha1($salt1.$pass.$salt2)",
|
||||
19500: "Ruby on Rails Restful-Authentication",
|
||||
19600: "Kerberos 5, etype 17, TGS-REP (AES128-CTS-HMAC-SHA1-96)",
|
||||
19700: "Kerberos 5, etype 18, TGS-REP (AES256-CTS-HMAC-SHA1-96)",
|
||||
19800: "Kerberos 5, etype 17, Pre-Auth",
|
||||
19900: "Kerberos 5, etype 18, Pre-Auth",
|
||||
20011: "DiskCryptor SHA512 + XTS 512 bit (AES) / DiskCryptor SHA512 + XTS 512 bit (Twofish) / DiskCryptor SHA512 + XTS 512 bit (Serpent)",
|
||||
20012: "DiskCryptor SHA512 + XTS 1024 bit (AES-Twofish) / DiskCryptor SHA512 + XTS 1024 bit (Twofish-Serpent) / DiskCryptor SHA512 + XTS 1024 bit (Serpent-AES)",
|
||||
20013: "DiskCryptor SHA512 + XTS 1536 bit (AES-Twofish-Serpent)",
|
||||
20200: "Python passlib pbkdf2-sha512",
|
||||
20300: "Python passlib pbkdf2-sha256",
|
||||
20400: "Python passlib pbkdf2-sha1",
|
||||
20500: "PKZIP Master Key",
|
||||
20510: "PKZIP Master Key (6 byte optimization)",
|
||||
20600: "Oracle Transportation Management (SHA256)",
|
||||
20710: "sha256(sha256($pass).$salt)",
|
||||
20711: "AuthMe sha256",
|
||||
20720: "sha256($salt.sha256($pass))",
|
||||
20800: "sha256(md5($pass))",
|
||||
20900: "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
21000: "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
21100: "sha1(md5($pass.$salt))",
|
||||
21200: "md5(sha1($salt).md5($pass))",
|
||||
21300: "md5($salt.sha1($salt.$pass))",
|
||||
21400: "sha256(sha256_bin($pass))",
|
||||
21500: "SolarWinds Orion",
|
||||
21501: "SolarWinds Orion v2",
|
||||
21600: "Web2py pbkdf2-sha512",
|
||||
21700: "Electrum Wallet (Salt-Type 4)",
|
||||
21800: "Electrum Wallet (Salt-Type 5)",
|
||||
22000: "WPA-PBKDF2-PMKID+EAPOL",
|
||||
22001: "WPA-PMK-PMKID+EAPOL",
|
||||
22100: "BitLocker",
|
||||
22200: "Citrix NetScaler (SHA512)",
|
||||
22300: "sha256($salt.$pass.$salt)",
|
||||
22301: "Telegram Mobile App Passcode (SHA256)",
|
||||
22400: "AES Crypt (SHA256)",
|
||||
22500: "MultiBit Classic .key (MD5)",
|
||||
22600: "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
22700: "MultiBit HD (scrypt)",
|
||||
22911: "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
22921: "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
22931: "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
22941: "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
22951: "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
23001: "SecureZIP AES-128",
|
||||
23002: "SecureZIP AES-192",
|
||||
23003: "SecureZIP AES-256",
|
||||
23100: "Apple Keychain",
|
||||
23200: "XMPP SCRAM PBKDF2-SHA1",
|
||||
23300: "Apple iWork",
|
||||
23400: "Bitwarden",
|
||||
23500: "AxCrypt 2 AES-128",
|
||||
23600: "AxCrypt 2 AES-256",
|
||||
23700: "RAR3-p (Uncompressed)",
|
||||
23800: "RAR3-p (Compressed)",
|
||||
23900: "BestCrypt v3 Volume Encryption",
|
||||
24100: "MongoDB ServerKey SCRAM-SHA-1",
|
||||
24200: "MongoDB ServerKey SCRAM-SHA-256",
|
||||
24300: "sha1($salt.sha1($pass.$salt))",
|
||||
24410: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
24420: "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
24500: "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
24600: "SQLCipher",
|
||||
24700: "Stuffit5",
|
||||
24800: "Umbraco HMAC-SHA1",
|
||||
24900: "Dahua Authentication MD5",
|
||||
25300: "MS Office 2016 - SheetProtection",
|
||||
25400: "PDF 1.4 - 1.6 (Acrobat 5 - 8) - edit password",
|
||||
25500: "Stargazer Stellar Wallet XLM",
|
||||
25600: "bcrypt(md5($pass)) / bcryptmd5",
|
||||
25700: "MurmurHash",
|
||||
25800: "bcrypt(sha1($pass)) / bcryptsha1",
|
||||
25900: "KNX IP Secure - Device Authentication Code",
|
||||
26000: "Mozilla key3.db",
|
||||
26100: "Mozilla key4.db",
|
||||
26200: "OpenEdge Progress Encode",
|
||||
26300: "FortiGate256 (FortiOS256)",
|
||||
26401: "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26402: "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26403: "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
26500: "iPhone passcode (UID key + System Keybag)",
|
||||
26600: "MetaMask Wallet",
|
||||
99999: "Plaintext",
|
||||
}
|
||||
|
||||
func hashcatHashTypeName(mode int32) (string, bool) {
|
||||
name, ok := hashcatHashTypes[mode]
|
||||
return name, ok
|
||||
}
|
||||
|
||||
func humanizeHashRate(rate uint64) string {
|
||||
const unit = 1000
|
||||
if rate < unit {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
value := float64(rate)
|
||||
exp := 0
|
||||
for value >= unit && exp < 5 {
|
||||
value /= unit
|
||||
exp++
|
||||
}
|
||||
suffixes := []string{"kH/s", "MH/s", "GH/s", "TH/s", "PH/s"}
|
||||
if exp == 0 {
|
||||
return fmt.Sprintf("%d H/s", rate)
|
||||
}
|
||||
if exp > len(suffixes) {
|
||||
exp = len(suffixes)
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", value, suffixes[exp-1])
|
||||
}
|
||||
@@ -3,13 +3,95 @@ package crack
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type crackJobCompletionEntry struct {
|
||||
id string
|
||||
description string
|
||||
}
|
||||
|
||||
// CrackJobIDCompleter completes durable crack job IDs.
|
||||
//
|
||||
//nolint:revive // Keep the established exported completer name for compatibility.
|
||||
func CrackJobIDCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(_ carapace.Context) carapace.Action {
|
||||
entries, err := crackJobCompletionEntries(con)
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack jobs: %s", err.Error())
|
||||
}
|
||||
|
||||
values := make([]string, 0, len(entries)*2)
|
||||
for _, entry := range entries {
|
||||
values = append(values, entry.id, entry.description)
|
||||
}
|
||||
return carapace.ActionValuesDescribed(values...).Tag("crack jobs")
|
||||
})
|
||||
}
|
||||
|
||||
func registerCrackJobIDCompletion(cmd *cobra.Command, con *console.SliverClient) {
|
||||
if cmd == nil || cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
entries, err := crackJobCompletionEntries(con)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
values := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.id, toComplete) {
|
||||
values = append(values, fmt.Sprintf("%s\t%s", entry.id, entry.description))
|
||||
}
|
||||
}
|
||||
return values, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
func crackJobCompletionEntries(con *console.SliverClient) ([]crackJobCompletionEntry, error) {
|
||||
if con == nil || con.Rpc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
jobs, err := con.Rpc.CrackJobs(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if jobs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entries := make([]crackJobCompletionEntry, 0, len(jobs.GetJobs()))
|
||||
for _, job := range jobs.GetJobs() {
|
||||
if job == nil || strings.TrimSpace(job.GetID()) == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, crackJobCompletionEntry{
|
||||
id: job.GetID(),
|
||||
description: fmt.Sprintf(
|
||||
"%s, created %s, keyspace %s, %d task(s), %d result(s)",
|
||||
safeCrackCell(job.GetStatus().String()),
|
||||
formatCrackTime(job.GetCreatedAt()),
|
||||
valueOrDash(job.GetKeyspace()),
|
||||
len(job.GetTasks()),
|
||||
job.GetResultCount(),
|
||||
),
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
@@ -25,7 +107,7 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
}
|
||||
|
||||
results = append(results, file.Name)
|
||||
desc := fmt.Sprintf("%s (%s chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
||||
desc := fmt.Sprintf("%s (%d chunks, chunk size: %s)", byteCountSI(file.UncompressedSize), len(file.Chunks), byteCountSI(file.ChunkSize))
|
||||
results = append(results, desc)
|
||||
}
|
||||
|
||||
@@ -35,14 +117,14 @@ func CrackHcstat2Completer(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
wordlists, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_WORDLIST})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range wordlists.Files {
|
||||
if file.Type != clientpb.CrackFileType_WORDLIST {
|
||||
continue
|
||||
}
|
||||
@@ -59,14 +141,14 @@ func CrackWordlistCompleter(con *console.SliverClient) carapace.Action {
|
||||
|
||||
func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
|
||||
hcstat2, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_MARKOV_HCSTAT2})
|
||||
rules, err := con.Rpc.CrackFilesList(context.Background(), &clientpb.CrackFile{Type: clientpb.CrackFileType_RULES})
|
||||
if err != nil {
|
||||
return carapace.ActionMessage("failed to fetch crack files: %s", err.Error())
|
||||
}
|
||||
|
||||
results := make([]string, 0)
|
||||
|
||||
for _, file := range hcstat2.Files {
|
||||
for _, file := range rules.Files {
|
||||
if file.Type != clientpb.CrackFileType_RULES {
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +159,7 @@ func CrackRulesCompleter(con *console.SliverClient) carapace.Action {
|
||||
results = append(results, desc)
|
||||
}
|
||||
|
||||
return carapace.ActionValuesDescribed(results...).Tag("wordlists")
|
||||
return carapace.ActionValuesDescribed(results...).Tag("rules")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type crackFilesListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
requests []clientpb.CrackFileType
|
||||
}
|
||||
|
||||
type crackJobsListCapture struct {
|
||||
rpcpb.SliverRPCClient
|
||||
jobs *clientpb.CrackJobs
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (capture *crackJobsListCapture) CrackJobs(_ context.Context, _ *commonpb.Empty, _ ...grpc.CallOption) (*clientpb.CrackJobs, error) {
|
||||
capture.calls++
|
||||
return capture.jobs, capture.err
|
||||
}
|
||||
|
||||
func (capture *crackFilesListCapture) CrackFilesList(_ context.Context, request *clientpb.CrackFile, _ ...grpc.CallOption) (*clientpb.CrackFiles, error) {
|
||||
capture.requests = append(capture.requests, request.GetType())
|
||||
return &clientpb.CrackFiles{}, nil
|
||||
}
|
||||
|
||||
func TestCrackFileCompletersRequestTheirOwnFileType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want clientpb.CrackFileType
|
||||
action func(*console.SliverClient) carapace.Action
|
||||
}{
|
||||
{name: "wordlists", want: clientpb.CrackFileType_WORDLIST, action: CrackWordlistCompleter},
|
||||
{name: "rules", want: clientpb.CrackFileType_RULES, action: CrackRulesCompleter},
|
||||
{name: "hcstat2", want: clientpb.CrackFileType_MARKOV_HCSTAT2, action: CrackHcstat2Completer},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
capture := &crackFilesListCapture{}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = test.action(client).Invoke(carapace.NewContext(""))
|
||||
if len(capture.requests) != 1 || capture.requests[0] != test.want {
|
||||
t.Fatalf("completion requests = %v, want [%s]", capture.requests, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompleterRequestsDurableJobs(t *testing.T) {
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{{
|
||||
ID: "12345678-1234-1234-1234-123456789abc",
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
}}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
_ = CrackJobIDCompleter(client).Invoke(carapace.NewContext(""))
|
||||
if capture.calls != 1 {
|
||||
t.Fatalf("CrackJobs calls = %d, want 1", capture.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCobraCompletionUsesFullIDsAndFiltersPrefix(t *testing.T) {
|
||||
firstID := "12345678-1234-1234-1234-123456789abc"
|
||||
secondID := "abcdefab-cdef-cdef-cdef-abcdefabcdef"
|
||||
capture := &crackJobsListCapture{jobs: &clientpb.CrackJobs{Jobs: []*clientpb.CrackJob{
|
||||
{
|
||||
ID: firstID,
|
||||
Status: clientpb.CrackJobStatus_IN_PROGRESS,
|
||||
CreatedAt: "2026-09-11T12:00:00Z",
|
||||
Keyspace: "1000",
|
||||
Tasks: []*clientpb.CrackTask{{ID: "task-id"}},
|
||||
ResultCount: 2,
|
||||
},
|
||||
{ID: secondID, Status: clientpb.CrackJobStatus_COMPLETED},
|
||||
nil,
|
||||
{},
|
||||
}}}
|
||||
client := &console.SliverClient{Rpc: capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
|
||||
values, directive := job.ValidArgsFunction(job, nil, "1234")
|
||||
if directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion directive = %v, want no-file-completion", directive)
|
||||
}
|
||||
if len(values) != 1 {
|
||||
t.Fatalf("completion values = %#v, want one filtered result", values)
|
||||
}
|
||||
if !strings.HasPrefix(values[0], firstID+"\t") {
|
||||
t.Fatalf("completion = %q, want full ID %q", values[0], firstID)
|
||||
}
|
||||
for _, want := range []string{"IN_PROGRESS", "2026-09-11T12:00:00Z", "keyspace 1000", "1 task(s)", "2 result(s)"} {
|
||||
if !strings.Contains(values[0], want) {
|
||||
t.Errorf("completion %q does not contain %q", values[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
values, directive = job.ValidArgsFunction(job, []string{firstID}, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("second positional completion = %#v, %v", values, directive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrackJobCompletionHandlesRPCFailuresAndNilData(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
capture *crackJobsListCapture
|
||||
}{
|
||||
{name: "rpc error", capture: &crackJobsListCapture{err: errors.New("rpc failed")}},
|
||||
{name: "nil response", capture: &crackJobsListCapture{}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := &console.SliverClient{Rpc: test.capture}
|
||||
root := Commands(client)[0]
|
||||
job, _, err := root.Find([]string{"job"})
|
||||
if err != nil {
|
||||
t.Fatalf("find crack job: %v", err)
|
||||
}
|
||||
values, directive := job.ValidArgsFunction(job, nil, "")
|
||||
if len(values) != 0 || directive != cobra.ShellCompDirectiveNoFileComp {
|
||||
t.Fatalf("completion = %#v, %v", values, directive)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package creds
|
||||
|
||||
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"
|
||||
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
|
||||
})
|
||||
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
|
||||
credsAddCmd.AddCommand(credsAddFileCmd)
|
||||
|
||||
credsRmCmd := &cobra.Command{
|
||||
|
||||
@@ -61,9 +61,9 @@ func printCollection(collection string, creds []*clientpb.Credential, con *conso
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(settings.GetTableStyle(con))
|
||||
if collection != "" {
|
||||
tw.SetTitle(console.Bold + collection + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render(collection))
|
||||
} else {
|
||||
tw.SetTitle(console.Bold + "Default Collection" + console.Normal)
|
||||
tw.SetTitle(console.StyleBold.Render("Default Collection"))
|
||||
}
|
||||
tw.AppendHeader(table.Row{
|
||||
"ID",
|
||||
|
||||
+598
-139
@@ -1,143 +1,602 @@
|
||||
package creds
|
||||
|
||||
// List of all available hash-types, for completions.
|
||||
// Unfortunately this list has to be maintained manually,
|
||||
// until someone devises a way to do so automatically.
|
||||
// hashTypes maps protobuf enum identifiers to the exact human-readable
|
||||
// names reported by the bundled Hashcat v7.1.2-armory.1 modules at commit
|
||||
// deadd52de22e2753e9caaff6d1b92e5a22a35626.
|
||||
var hashTypes = map[string]string{
|
||||
"MD5": "MD5", // Enum 0 in proto file.
|
||||
|
||||
"MD4": "MD4",
|
||||
"SHA1": "SHA1",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"SIPHASH": "SipHash",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
|
||||
// ... TODO fill in the rest ...,
|
||||
|
||||
"CRC32": "CRC32",
|
||||
"CRC32C": "CRC32B",
|
||||
"CRC64Jones": "CRC64-Jones",
|
||||
"JAVA_OBJECT": "Java object",
|
||||
"MURMUR": "MurmurHash",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"SCRYPT": "scrypt",
|
||||
"PHPASS": "phpass",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96 / HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"RACF": "Resource Access Control Facility",
|
||||
"AIX_SMD5": "AIX MD5",
|
||||
"AIX_SSHA1": "AIX SHA-1",
|
||||
"AIX_SSHA256": "AIX SHA-256",
|
||||
"AIX_SSHA512": "AIX SHA-512",
|
||||
"LM": "LM",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"NTLM": "NTLM",
|
||||
"RADMIN2": "Radmin2",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/Password",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"CISCO_PIX_MD5": "Cisco-Pix MD5",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
|
||||
// ...
|
||||
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
|
||||
// ...
|
||||
|
||||
// INVALID = 9999; // Invalid hash type
|
||||
|
||||
// Out of order
|
||||
"BCRYPT_UNIX": "bcrypt",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"MD5": "MD5",
|
||||
"MD5_PW_SALT": "md5($pass.$salt)",
|
||||
"JOOMLA_2_5_18": "Joomla < 2.5.18",
|
||||
"POSTGRESQL": "PostgreSQL",
|
||||
"MD5_SALT_PW": "md5($salt.$pass)",
|
||||
"OSCOMMERCE_XT_COMMERCE": "osCommerce, xt:Commerce",
|
||||
"JUNIPER_NETSCREEN_SSG_SCREENOS": "Juniper NetScreen/SSG (ScreenOS)",
|
||||
"SKYPE": "Skype",
|
||||
"SOLARWINDS_SERV_U": "SolarWinds Serv-U",
|
||||
"MD5_UTF16LE_PASS_SALT": "md5(utf16le($pass).$salt)",
|
||||
"MD5_SALT_UTF16LE_PASS": "md5($salt.utf16le($pass))",
|
||||
"HMAC_MD5_KEY_PASS": "HMAC-MD5 (key = $pass)",
|
||||
"HMAC_MD5_KEY_SALT": "HMAC-MD5 (key = $salt)",
|
||||
"MD5_UTF16LE": "md5(utf16le($pass))",
|
||||
"SHA1": "SHA1",
|
||||
"NSLDAP_SHA_1_BASE64_NETSCAPE_LDAP_SHA": "nsldap, SHA-1(Base64), Netscape LDAP SHA",
|
||||
"SHA1_PASS_SALT": "sha1($pass.$salt)",
|
||||
"NSLDAPS_SSHA_1_BASE64_NETSCAPE_LDAP_SSHA": "nsldaps, SSHA-1(Base64), Netscape LDAP SSHA",
|
||||
"ORACLE_S_TYPE_ORACLE_11": "Oracle S: Type (Oracle 11+)",
|
||||
"SHA1_SALT_PASS": "sha1($salt.$pass)",
|
||||
"SMF_SIMPLE_MACHINES_FORUM_V1_1": "SMF (Simple Machines Forum) > v1.1",
|
||||
"MACOS_V10_4_MACOS_V10_5_MACOS_V10_6": "macOS v10.4, macOS v10.5, macOS v10.6",
|
||||
"DJANGO_SHA_1": "Django (SHA-1)",
|
||||
"ARUBAOS": "ArubaOS",
|
||||
"SHA1_UTF16LE_PASS_SALT": "sha1(utf16le($pass).$salt)",
|
||||
"MSSQL_2000": "MSSQL (2000)",
|
||||
"MSSQL_2005": "MSSQL (2005)",
|
||||
"PEOPLESOFT": "PeopleSoft",
|
||||
"SHA1_SALT_UTF16LE_PASS": "sha1($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_141": "Episerver 6.x < .NET 4",
|
||||
"HMAC_SHA1_KEY_PASS": "HMAC-SHA1 (key = $pass)",
|
||||
"HMAC_SHA1_KEY_SALT": "HMAC-SHA1 (key = $salt)",
|
||||
"SHA1_UTF16LE": "sha1(utf16le($pass))",
|
||||
"MYSQL323": "MySQL323",
|
||||
"MYSQL4_1_MYSQL5": "MySQL4.1/MySQL5",
|
||||
"PHPASS": "phpass",
|
||||
"MD5CRYPT_MD5_UNIX_CISCO_IOS_1_MD5": "md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)",
|
||||
"JUNIPER_IVE": "Juniper IVE",
|
||||
"BLAKE2B_256": "BLAKE2b-512",
|
||||
"BLAKE2B_512_PW_SALT": "BLAKE2b-512($pass.$salt)",
|
||||
"BLAKE2B_512_SALT_PW": "BLAKE2b-512($salt.$pass)",
|
||||
"MD4": "MD4",
|
||||
"NTLM": "NTLM",
|
||||
"DCC": "Domain Cached Credentials (DCC), MS Cache",
|
||||
"SHA2_224": "SHA2-224",
|
||||
"SHA224_PASS_SALT": "sha224($pass.$salt)",
|
||||
"SHA224_SALT_PASS": "sha224($salt.$pass)",
|
||||
"SHA2_256": "SHA2-256",
|
||||
"SHA256_PASS_SALT": "sha256($pass.$salt)",
|
||||
"SSHA_256_BASE64_LDAP_SSHA256": "SSHA-256(Base64), LDAP {SSHA256}",
|
||||
"SHA256_SALT_PASS": "sha256($salt.$pass)",
|
||||
"HMAILSERVER": "hMailServer",
|
||||
"SHA256_UTF16LE_PASS_SALT": "sha256(utf16le($pass).$salt)",
|
||||
"SHA256_SALT_UTF16LE_PASS": "sha256($salt.utf16le($pass))",
|
||||
"EPISERVER_6_X_NET_4_MODE_1441": "Episerver 6.x >= .NET 4",
|
||||
"HMAC_SHA256_KEY_PASS": "HMAC-SHA256 (key = $pass)",
|
||||
"HMAC_SHA256_KEY_SALT": "HMAC-SHA256 (key = $salt)",
|
||||
"SHA256_UTF16LE": "sha256(utf16le($pass))",
|
||||
"DESCRYPT_DES_UNIX_TRADITIONAL_DES": "descrypt, DES (Unix), Traditional DES",
|
||||
"APACHE_APR1_MD5_MD5APR1_MD5_APR": "Apache $apr1$ MD5, md5apr1, MD5 (APR)",
|
||||
"SHA2_512": "SHA2-512",
|
||||
"SHA512_PASS_SALT": "sha512($pass.$salt)",
|
||||
"SSHA_512_BASE64_LDAP_SSHA512": "SSHA-512(Base64), LDAP {SSHA512}",
|
||||
"SHA512_SALT_PASS": "sha512($salt.$pass)",
|
||||
"MACOS_V10_7": "macOS v10.7",
|
||||
"SHA512_UTF16LE_PASS_SALT": "sha512(utf16le($pass).$salt)",
|
||||
"MSSQL_2012_2014": "MSSQL (2012, 2014)",
|
||||
"SHA512_SALT_UTF16LE_PASS": "sha512($salt.utf16le($pass))",
|
||||
"HMAC_SHA512_KEY_PASS": "HMAC-SHA512 (key = $pass)",
|
||||
"HMAC_SHA512_KEY_SALT": "HMAC-SHA512 (key = $salt)",
|
||||
"SHA512_UTF16LE": "sha512(utf16le($pass))",
|
||||
"SHA512_CRYPT_UNIX": "sha512crypt $6$, SHA512 (Unix)",
|
||||
"STDOUT": "STDOUT",
|
||||
"DCC2": "Domain Cached Credentials 2 (DCC2), MS Cache 2",
|
||||
"CISCO_PIX_MD5": "Cisco-PIX MD5",
|
||||
"CISCO_ASA_MD5": "Cisco-ASA MD5",
|
||||
"WPA_EAPOL_PBKDF2": "WPA-EAPOL-PBKDF2",
|
||||
"WPA_EAPOL_PMK": "WPA-EAPOL-PMK",
|
||||
"MD5_MD5_PASS": "md5(md5($pass))",
|
||||
"VBULLETIN_V3_8_5_MODE_2611": "vBulletin < v3.8.5",
|
||||
"PHPS": "PHPS",
|
||||
"MD5_MD5_PASS_SALT": "md5(md5($pass.$salt))",
|
||||
"VBULLETIN_V3_8_5_MODE_2711": "vBulletin >= v3.8.5",
|
||||
"MYBB_1_2_IPB2_INVISION_POWER_BOARD": "MyBB 1.2+, IPB2+ (Invision Power Board)",
|
||||
"LM": "LM",
|
||||
"ORACLE_H_TYPE_ORACLE_7": "Oracle H: Type (Oracle 7+)",
|
||||
"BCRYPT_UNIX": "bcrypt $2*$, Blowfish (Unix)",
|
||||
"MD5_MD5_MD5_PASS": "md5(md5(md5($pass)))",
|
||||
"MD5_MD5_MD5_PASS_SALT": "md5(md5(md5($pass)).$salt)",
|
||||
"MD5_SALT_MD5_PW": "md5($salt.md5($pass))",
|
||||
"MEDIAWIKI_B_TYPE": "MediaWiki B type",
|
||||
"MD5_SALT1_STRTOUPPER_MD5_SALT2_PASS": "md5($salt1.strtoupper(md5($salt2.$pass)))",
|
||||
"MD5_SALT_PW_SALT": "md5($salt.$pass.$salt)",
|
||||
"MD5_MD5_PASS_MD5_SALT": "md5(md5($pass).md5($salt))",
|
||||
"MD5_SALT_MD5_SALT_PASS": "md5($salt.md5($salt.$pass))",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_4110": "md5($salt.md5($pass.$salt))",
|
||||
"MD5_STRTOUPPER_MD5_PASS": "md5(strtoupper(md5($pass)))",
|
||||
"MD5_SHA1_PASS": "md5(sha1($pass))",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4410": "md5(sha1($pass).$salt)",
|
||||
"MD5_SHA1_PASS_SALT_MODE_4420": "md5(sha1($pass.$salt))",
|
||||
"MD5_SHA1_SALT_PASS": "md5(sha1($salt.$pass))",
|
||||
"SHA1_SHA1_PASS": "sha1(sha1($pass))",
|
||||
"SHA1_SHA1_PASS_SALT": "sha1(sha1($pass).$salt)",
|
||||
"SHA1_SALT_SHA1_PASS": "sha1($salt.sha1($pass))",
|
||||
"REDMINE": "Redmine",
|
||||
"PUNBB": "PunBB",
|
||||
"SHA1_MD5_PASS": "sha1(md5($pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_4710": "sha1(md5($pass).$salt)",
|
||||
"HUAWEI_SHA1_MD5_PASS_SALT": "Huawei sha1(md5($pass).$salt)",
|
||||
"ISCSI_CHAP": "iSCSI CHAP authentication, MD5(CHAP)",
|
||||
"SHA1_SALT_PASS_SALT": "sha1($salt.$pass.$salt)",
|
||||
"SHA1_SHA1_SALT_PASS_SALT": "sha1(sha1($salt.$pass.$salt))",
|
||||
"HALF_MD5": "Half MD5",
|
||||
"PASSWORD_SAFE_V3": "Password Safe v3",
|
||||
"IKE_MD5": "IKE-PSK MD5",
|
||||
"IKE_SHA1": "IKE-PSK SHA1",
|
||||
"NET_NTLM_V1": "NetNTLMv1 / NetNTLMv1+ESS",
|
||||
"NET_NTLM_V2": "NetNTLMv2",
|
||||
"CISCO_IOS_TYPE_4_SHA256": "Cisco-IOS type 4 (SHA256)",
|
||||
"CISCO_ISE_HASHED_PASSWORD_SHA256": "Cisco-ISE Hashed Password (SHA256)",
|
||||
"SAMSUNG_ANDROID": "Samsung Android Password/PIN",
|
||||
"RIPEMD_160": "RIPEMD-160",
|
||||
"HMAC_RIPEMD160_KEY_PASS": "HMAC-RIPEMD160 (key = $pass)",
|
||||
"HMAC_RIPEMD160_KEY_SALT": "HMAC-RIPEMD160 (key = $salt)",
|
||||
"WHIRLPOOL": "Whirlpool",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT_LEGACY": "TrueCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT_LEGACY": "TrueCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "TrueCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"AIX_SMD5": "AIX {smd5}",
|
||||
"AIX_SSHA256": "AIX {ssha256}",
|
||||
"AIX_SSHA512": "AIX {ssha512}",
|
||||
"HASHCAT_1PASSWORD_AGILEKEYCHAIN": "1Password, agilekeychain",
|
||||
"AIX_SSHA1": "AIX {ssha1}",
|
||||
"LASTPASS_LASTPASS_SNIFFED": "LastPass + LastPass sniffed",
|
||||
"GOST_R_34_11_94": "GOST R 34.11-94",
|
||||
"FORTIGATE_FORTIOS": "FortiGate (FortiOS)",
|
||||
"MACOS_10_8": "macOS v10.8+ (PBKDF2-SHA512)",
|
||||
"GRUB_2": "GRUB 2",
|
||||
"IPMI2_PAKP_HMAC_SHA1": "IPMI2 RAKP HMAC-SHA1",
|
||||
"IPMI2_RAKP_HMAC_MD5": "IPMI2 RAKP HMAC-MD5",
|
||||
"SHA256CRYPT_5_SHA256_UNIX": "sha256crypt $5$, SHA256 (Unix)",
|
||||
"MYSQL_A_SHA256CRYPT": "MySQL $A$ (sha256crypt)",
|
||||
"KERBEROS_23_SA_REQ_PREAUTH": "Kerberos 5, etype 23, AS-REQ Pre-Auth",
|
||||
"SAP_CODVN_B_BCODE": "SAP CODVN B (BCODE)",
|
||||
"SAP_CODVN_B_BCODE_FROM_RFC_READ_TABLE": "SAP CODVN B (BCODE) from RFC_READ_TABLE",
|
||||
"SAP_CODVN_F_G_PASSCODE": "SAP CODVN F/G (PASSCODE)",
|
||||
"SAP_CODVN_F_G_PASSCODE_FROM_RFC_READ_TABLE": "SAP CODVN F/G (PASSCODE) from RFC_READ_TABLE",
|
||||
"DRUPAL7": "Drupal7",
|
||||
"SYBASE_ASE": "Sybase ASE",
|
||||
"CITRIX_NETSCALER_SHA1": "Citrix NetScaler (SHA1)",
|
||||
"HASHCAT_1PASSWORD_CLOUDKEYCHAIN": "1Password, cloudkeychain",
|
||||
"DNSSEC_NSEC3": "DNSSEC (NSEC3)",
|
||||
"WBB3_WOLTLAB_BURNING_BOARD": "WBB3 (Woltlab Burning Board)",
|
||||
"RACF": "RACF",
|
||||
"AS_400_DES": "AS/400 DES",
|
||||
"LOTUS_NOTES_DOMINO_5": "Lotus Notes/Domino 5",
|
||||
"LOTUS_NOTES_DOMINO_6": "Lotus Notes/Domino 6",
|
||||
"ANDROID_FDE_4_3": "Android FDE <= 4.3",
|
||||
"SCRYPT": "scrypt",
|
||||
"PASSWORD_SAFE_V2": "Password Safe v2",
|
||||
"LOTUS_NOTES_DOMINO_8": "Lotus Notes/Domino 8",
|
||||
"CISCO_IOS_PBKDF2_SHA256": "Cisco-IOS $8$ (PBKDF2-SHA256)",
|
||||
"CISCO_IOS_SCRYPT": "Cisco-IOS $9$ (scrypt)",
|
||||
"MS_OFFICE_2007": "MS Office 2007",
|
||||
"MS_OFFICE_2010": "MS Office 2010",
|
||||
"MS_OFFICE_2013": "MS Office 2013",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4": "MS Office <= 2003 $0/$1, MD5 + RC4",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_1": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_0_1_MD5_RC4_COLLIDER_2": "MS Office <= 2003 $0/$1, MD5 + RC4, collider #2",
|
||||
"MS_OFFICE_2003_3_4_SHA1_RC4": "MS Office <= 2003 $3/$4, SHA1 + RC4",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_1": "MS Office <= 2003 $3, SHA1 + RC4, collider #1",
|
||||
"MS_OFFICE_2003_3_SHA1_RC4_COLLIDER_2": "MS Office <= 2003 $3, SHA1 + RC4, collider #2",
|
||||
"RADMIN2": "Radmin2",
|
||||
"DJANGO_PBKDF2_SHA256": "Django (PBKDF2-SHA256)",
|
||||
"SIPHASH": "SipHash",
|
||||
"CRAM_MD5": "CRAM-MD5",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA_1": "SAP CODVN H (PWDSALTEDHASH) iSSHA-1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4": "PDF 1.1 - 1.3 (Acrobat 2 - 4)",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_1": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #1",
|
||||
"PDF_1_1_1_3_ACROBAT_2_4_COLLIDER_2": "PDF 1.1 - 1.3 (Acrobat 2 - 4), collider #2",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8": "PDF 1.4 - 1.6 (Acrobat 5 - 8)",
|
||||
"PDF_1_3_1_6_ACROBAT_4_8_W_RC4_40": "PDF 1.3 - 1.6 (Acrobat 4 - 8) w/ RC4-40",
|
||||
"PDF_1_7_LEVEL_3_ACROBAT_9": "PDF 1.7 Level 3 (Acrobat 9)",
|
||||
"PDF_1_7_LEVEL_8_ACROBAT_10_11": "PDF 1.7 Level 8 (Acrobat 10 - 11)",
|
||||
"SHA2_384": "SHA2-384",
|
||||
"SHA384_PASS_SALT": "sha384($pass.$salt)",
|
||||
"SHA384_SALT_PASS": "sha384($salt.$pass)",
|
||||
"SHA384_UTF16LE_PASS_SALT": "sha384(utf16le($pass).$salt)",
|
||||
"SHA384_SALT_UTF16LE_PASS": "sha384($salt.utf16le($pass))",
|
||||
"SHA384_UTF16LE": "sha384(utf16le($pass))",
|
||||
"PBKDF2_HMAC_SHA256": "PBKDF2-HMAC-SHA256",
|
||||
"REDHAT_389_DS_LDAP_PBKDF2_HMAC_SHA256": "RedHat 389-DS LDAP (PBKDF2-HMAC-SHA256)",
|
||||
"PRESTASHOP": "PrestaShop",
|
||||
"POSTGRESQL_CRAM_MD5": "PostgreSQL CRAM (MD5)",
|
||||
"MYSQL_CRAM_SHA1": "MySQL CRAM (SHA1)",
|
||||
"BITCOIN_LITECOIN_WALLET_DAT": "Bitcoin/Litecoin wallet.dat",
|
||||
"SIP_DIGEST": "SIP digest authentication (MD5)",
|
||||
"CRC32": "CRC32",
|
||||
"HASHCAT_7_ZIP": "7-Zip",
|
||||
"GOST_R_32_11_2012_256": "GOST R 34.11-2012 (Streebog) 256-bit, big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-256 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_256_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-256 (key = $salt), big-endian",
|
||||
"GOST_R_32_11_2012_512": "GOST R 34.11-2012 (Streebog) 512-bit, big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_PASS_BIG_ENDIAN": "HMAC-Streebog-512 (key = $pass), big-endian",
|
||||
"HMAC_STREEBOG_512_KEY_SALT_BIG_ENDIAN": "HMAC-Streebog-512 (key = $salt), big-endian",
|
||||
"PBKDF2_HMAC_MD5": "PBKDF2-HMAC-MD5",
|
||||
"PBKDF2_HMAC_SHA1": "PBKDF2-HMAC-SHA1",
|
||||
"ATLASSIAN_PBKDF2_HMAC_SHA1": "Atlassian (PBKDF2-HMAC-SHA1)",
|
||||
"PBKDF2_HMAC_SHA512": "PBKDF2-HMAC-SHA512",
|
||||
"APACHE_SHIRO_1_SHA_512": "Apache Shiro 1 SHA-512",
|
||||
"ECRYPTFS": "eCryptfs",
|
||||
"ORACLE_T_TYPE_ORACLE_12": "Oracle T: Type (Oracle 12+)",
|
||||
"BSDI_CRYPT": "BSDi Crypt, Extended DES",
|
||||
"RAR3_HP": "RAR3-hp",
|
||||
"COLDFUSION_10": "ColdFusion 10+",
|
||||
"BLOCKCHAIN_MY_WALLET": "Blockchain, My Wallet",
|
||||
"MS_AZURE_SYNC": "MS-AzureSync PBKDF2-HMAC-SHA256",
|
||||
"ANDROID_FDE_SAMSUNG_DEK": "Android FDE (Samsung DEK)",
|
||||
"RAR5": "RAR5",
|
||||
"KERBEROS_23_TGS_REP": "Kerberos 5, etype 23, TGS-REP",
|
||||
"AXCRYPT_1": "AxCrypt 1",
|
||||
"AXCRYPT_1_IN_MEMORY_SHA1": "AxCrypt 1 in-memory SHA1",
|
||||
"KEEPASS_KDBX_V2_V3": "KeePass (KDBX v2/v3)",
|
||||
"PEOPLESOFT_PS_TOKEN": "PeopleSoft PS_TOKEN",
|
||||
"WINZIP": "WinZip",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT_LEGACY": "VeraCrypt SHA512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT_LEGACY": "VeraCrypt SHA512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT_LEGACY": "VeraCrypt Whirlpool + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_LEGACY": "VeraCrypt SHA256 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode (legacy)",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE_LEGACY": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode (legacy)",
|
||||
"WINDOWS_PHONE": "Windows Phone 8+ PIN/password",
|
||||
"OPENCART": "OpenCart",
|
||||
"DES": "DES (PT = $salt, key = $pass)",
|
||||
"THREE_DES": "3DES (PT = $salt, key = $pass)",
|
||||
"RACF_KDFAES": "RACF KDFAES",
|
||||
"SHA1_CX": "sha1(CX)",
|
||||
"LINUX_KERNEL_CRYPTO_API_24": "Linux Kernel Crypto API (2.4)",
|
||||
"LUKS_V1_LEGACY": "LUKS v1 (legacy)",
|
||||
"ITUNES_BACKUP_10_0_MODE_14700": "iTunes backup < 10.0",
|
||||
"ITUNES_BACKUP_10_0_MODE_14800": "iTunes backup >= 10.0",
|
||||
"SKIP_32": "Skip32 (PT = $salt, key = $pass)",
|
||||
"FILEZILLA_SERVER_0_9_55": "FileZilla Server >= 0.9.55",
|
||||
"JUNIPER_NETBSD_SHA1CRYPT": "Juniper/NetBSD sha1crypt",
|
||||
"BLOCKCHAIN_MY_WALLET_V2": "Blockchain, My Wallet, V2",
|
||||
"DPAPI_V1_CTX_1_AND_2": "DPAPI masterkey file v1 (context 1 and 2)",
|
||||
"DPAPI_V1_CTX_3": "DPAPI masterkey file v1 (context 3)",
|
||||
"CHA_CHA_20": "ChaCha20",
|
||||
"JKS_JAVA_KEY_STORE_PRIVATE_KEYS_SHA1": "JKS Java Key Store Private Keys (SHA1)",
|
||||
"ETHEREUM_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Wallet, PBKDF2-HMAC-SHA256",
|
||||
"ETHEREUM_WALLET_SCRYPT": "Ethereum Wallet, SCRYPT",
|
||||
"DPAPI_V2_CTX_1_AND_2": "DPAPI masterkey file v2 (context 1 and 2)",
|
||||
"DPAPI_V2_CTX_3": "DPAPI masterkey file v2 (context 3)",
|
||||
"TRIPCODE": "Tripcode",
|
||||
"TACACS_PLUS": "TACACS+",
|
||||
"APPLE_SECURE_NOTES": "Apple Secure Notes",
|
||||
"ETHEREUM_PRE_SALE_WALLET_PBKDF2_HMAC_SHA256": "Ethereum Pre-Sale Wallet, PBKDF2-HMAC-SHA256",
|
||||
"CRAM_MD5_DOVECOT": "CRAM-MD5 Dovecot",
|
||||
"JWT": "JWT (JSON Web Token)",
|
||||
"PERL_MOJOLICIOUS_SESSION_COOKIE_HMAC_SHA256_V9_19": "Perl Mojolicious session cookie (HMAC-SHA256, >= v9.19)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_1_3": "Electrum Wallet (Salt-Type 1-3)",
|
||||
"FILEVAULT_2": "FileVault 2",
|
||||
"WPA_PMKID_PBKDF2": "WPA-PMKID-PBKDF2",
|
||||
"WPA_PMKID_PMK": "WPA-PMKID-PMK",
|
||||
"ANSIBLE_VAULT": "Ansible Vault",
|
||||
"GPG": "GPG (AES-128/AES-256 (SHA-1($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_512_PASS": "GPG (AES-128/AES-256 (SHA-512($pass)))",
|
||||
"GPG_AES_128_AES_256_SHA_256_PASS": "GPG (AES-128/AES-256 (SHA-256($pass)))",
|
||||
"GPG_CAST5_SHA_1_PASS": "GPG (CAST5 (SHA-1($pass)))",
|
||||
"GPG_AES_OCB_128_SHA_1_PASS": "GPG (AES-OCB-128 (SHA-1($pass)))",
|
||||
"PKZIP_COMPRESSED": "PKZIP (Compressed)",
|
||||
"PKZIP_UNCOMPRESSED": "PKZIP (Uncompressed)",
|
||||
"PKZIP_COMPRESSED_MULTI_FILE": "PKZIP (Compressed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE": "PKZIP (Mixed Multi-File)",
|
||||
"PKZIP_MIXED_MULTI_FILE_CHECKSUM_ONLY": "PKZIP (Mixed Multi-File Checksum-Only)",
|
||||
"SHA3_224": "SHA3-224",
|
||||
"SHA3_256": "SHA3-256",
|
||||
"SHA3_384": "SHA3-384",
|
||||
"SHA3_512": "SHA3-512",
|
||||
"KECCAK_224": "Keccak-224",
|
||||
"KECCAK_256": "Keccak-256",
|
||||
"KECCAK_384": "Keccak-384",
|
||||
"KECCAK_512": "Keccak-512",
|
||||
"TOTP_HMAC_SHA1": "TOTP (HMAC-SHA1)",
|
||||
"KERBEROS_23_AS_REP": "Kerberos 5, etype 23, AS-REP",
|
||||
"APPLE_FILE_SYSTEM_APFS": "Apple File System (APFS)",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_2_SHA_256_AES": "Open Document Format (ODF) 1.2 (SHA-256, AES)",
|
||||
"SHA1_MD5_MD5_PASS": "sha1(md5(md5($pass)))",
|
||||
"OPEN_DOCUMENT_FORMAT_ODF_1_1_SHA_1_BLOWFISH": "Open Document Format (ODF) 1.1 (SHA-1, Blowfish)",
|
||||
"JAVA_OBJECT": "Java Object hashCode()",
|
||||
"BLOCKCHAIN_MY_WALLET_SECOND_PASSWORD_SHA256": "Blockchain, My Wallet, Second Password (SHA256)",
|
||||
"ANDROID_BACKUP": "Android Backup",
|
||||
"QNX_MD5": "QNX /etc/shadow (MD5)",
|
||||
"QNX_SHA256": "QNX /etc/shadow (SHA256)",
|
||||
"QNX_SHA512": "QNX /etc/shadow (SHA512)",
|
||||
"QNX_7_ETC_SHADOW_SHA512": "QNX 7 /etc/shadow (SHA512)",
|
||||
"SHA1_SALT1_PASS_SALT2": "sha1($salt1.$pass.$salt2)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTHENTICATION": "Ruby on Rails Restful-Authentication",
|
||||
"KERBEROS_17_TGS_REP": "Kerberos 5, etype 17, TGS-REP",
|
||||
"KERBEROS_18_TGS_REP": "Kerberos 5, etype 18, TGS-REP",
|
||||
"KERBEROS_17_PREAUTH": "Kerberos 5, etype 17, Pre-Auth",
|
||||
"KERBEROS_18_PREAUTH": "Kerberos 5, etype 18, Pre-Auth",
|
||||
"DISKCRYPTOR_SHA512_XTS_512_BIT": "DiskCryptor SHA512 + XTS 512 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1024_BIT": "DiskCryptor SHA512 + XTS 1024 bit",
|
||||
"DISKCRYPTOR_SHA512_XTS_1536_BIT": "DiskCryptor SHA512 + XTS 1536 bit",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA512": "Python passlib pbkdf2-sha512",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA256": "Python passlib pbkdf2-sha256",
|
||||
"PYTHON_PASSLIB_PBKDF2_SHA1": "Python passlib pbkdf2-sha1",
|
||||
"PKZIP_MASTER_KEY": "PKZIP Master Key",
|
||||
"PKZIP_MASTER_KEY_6_BYTE_OPTIMIZATION": "PKZIP Master Key (6 byte optimization)",
|
||||
"ORACLE_TRANSPORTATION_MANAGEMENT_SHA256": "Oracle Transportation Management (SHA256)",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20710": "sha256(sha256($pass).$salt)",
|
||||
"AUTHME_SHA256": "AuthMe sha256",
|
||||
"RSA_SECURITY_ANALYTICS_NETWITNESS_SHA256": "RSA Security Analytics / NetWitness (sha256)",
|
||||
"SHA256_SALT_SHA256_PASS": "sha256($salt.sha256($pass))",
|
||||
"SHA256_SHA256_PASS_SALT_MODE_20730": "sha256(sha256($pass.$salt))",
|
||||
"SHA256_MD5_PASS": "sha256(md5($pass))",
|
||||
"MD5_SHA1_PASS_MD5_PASS_SHA1_PASS": "md5(sha1($pass).md5($pass).sha1($pass))",
|
||||
"BITSHARES_V0_X_SHA512_SHA512_BIN_PASS": "BitShares v0.x - sha512(sha512_bin(pass))",
|
||||
"SHA1_MD5_PASS_SALT_MODE_21100": "sha1(md5($pass.$salt))",
|
||||
"MD5_SHA1_SALT_MD5_PASS": "md5(sha1($salt).md5($pass))",
|
||||
"MD5_SALT_SHA1_SALT_PASS": "md5($salt.sha1($salt.$pass))",
|
||||
"MD5_SALT1_SHA1_SALT2_PASS": "md5($salt1.sha1($salt2.$pass))",
|
||||
"SHA256_SHA256_BIN_PASS": "sha256(sha256_bin($pass))",
|
||||
"SHA256_SALT_SHA256_BIN_PASS": "sha256($salt.sha256_bin($pass))",
|
||||
"SOLARWINDS_ORION": "SolarWinds Orion",
|
||||
"SOLARWINDS_ORION_V2": "SolarWinds Orion v2",
|
||||
"WEB2PY_PBKDF2_SHA512": "Web2py pbkdf2-sha512",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_4": "Electrum Wallet (Salt-Type 4)",
|
||||
"ELECTRUM_WALLET_SALT_TYPE_5": "Electrum Wallet (Salt-Type 5)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900": "md5(md5(md5($pass.$salt1)).$salt2)",
|
||||
"WPA_PBKDF2_PMKID_EAPOL": "WPA-PBKDF2-PMKID+EAPOL",
|
||||
"WPA_PMK_PMKID_EAPOL": "WPA-PMK-PMKID+EAPOL",
|
||||
"BITLOCKER": "BitLocker",
|
||||
"CITRIX_NETSCALER_SHA512": "Citrix NetScaler (SHA512)",
|
||||
"SHA256_SALT_PASS_SALT": "sha256($salt.$pass.$salt)",
|
||||
"TELEGRAM_MOBILE_APP_PASSCODE_SHA256": "Telegram Mobile App Passcode (SHA256)",
|
||||
"AES_CRYPT_SHA256": "AES Crypt (SHA256)",
|
||||
"MULTIBIT_CLASSIC_KEY_MD5": "MultiBit Classic .key (MD5)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA1": "Telegram Desktop < v2.1.14 (PBKDF2-HMAC-SHA1)",
|
||||
"MULTIBIT_HD_SCRYPT": "MultiBit HD (scrypt)",
|
||||
"SIMPLA_CMS_MD5_SALT_PASS_MD5_PASS": "Simpla CMS - md5($salt.$pass.md5($pass))",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_0": "RSA/DSA/EC/OpenSSH Private Keys ($0$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_6": "RSA/DSA/EC/OpenSSH Private Keys ($6$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_1_3": "RSA/DSA/EC/OpenSSH Private Keys ($1, $3$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_4": "RSA/DSA/EC/OpenSSH Private Keys ($4$)",
|
||||
"RSA_DSA_EC_OPENSSH_PRIVATE_KEYS_5": "RSA/DSA/EC/OpenSSH Private Keys ($5$)",
|
||||
"SECUREZIP_AES_128": "SecureZIP AES-128",
|
||||
"SECUREZIP_AES_192": "SecureZIP AES-192",
|
||||
"SECUREZIP_AES_256": "SecureZIP AES-256",
|
||||
"APPLE_KEYCHAIN": "Apple Keychain",
|
||||
"XMPP_SCRAM_PBKDF2_SHA1": "XMPP SCRAM PBKDF2-SHA1",
|
||||
"APPLE_IWORK": "Apple iWork",
|
||||
"BITWARDEN": "Bitwarden",
|
||||
"AXCRYPT_2_AES_128": "AxCrypt 2 AES-128",
|
||||
"AXCRYPT_2_AES_256": "AxCrypt 2 AES-256",
|
||||
"RAR3_P_UNCOMPRESSED": "RAR3-p (Uncompressed)",
|
||||
"RAR3_P_COMPRESSED": "RAR3-p (Compressed)",
|
||||
"BESTCRYPT_V3_VOLUME_ENCRYPTION": "BestCrypt v3 Volume Encryption",
|
||||
"BESTCRYPT_V4_VOLUME_ENCRYPTION": "BestCrypt v4 Volume Encryption",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_1": "MongoDB ServerKey SCRAM-SHA-1",
|
||||
"MONGODB_SERVERKEY_SCRAM_SHA_256": "MongoDB ServerKey SCRAM-SHA-256",
|
||||
"SHA1_SALT_SHA1_PASS_SALT": "sha1($salt.sha1($pass.$salt))",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA1_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES)",
|
||||
"PKCS_8_PRIVATE_KEYS_PBKDF2_HMAC_SHA256_3DES_AES": "PKCS#8 Private Keys (PBKDF2-HMAC-SHA256 + 3DES/AES)",
|
||||
"TELEGRAM_DESKTOP_V2_1_14_PBKDF2_HMAC_SHA512": "Telegram Desktop >= v2.1.14 (PBKDF2-HMAC-SHA512)",
|
||||
"SQLCIPHER": "SQLCipher",
|
||||
"STUFFIT5": "Stuffit5",
|
||||
"UMBRACO_HMAC_SHA1": "Umbraco HMAC-SHA1",
|
||||
"DAHUA_AUTHENTICATION_MD5": "Dahua Authentication MD5",
|
||||
"BESDER_AUTHENTICATION_MD5": "Besder Authentication MD5",
|
||||
"SNMP_V3_HMAC_MD5_96__SHA1_96": "SNMPv3 HMAC-MD5-96/HMAC-SHA1-96",
|
||||
"SNMP_V3_HMAC_MD5_96": "SNMPv3 HMAC-MD5-96",
|
||||
"SNMP_V3_HMAC_SHA1_96": "SNMPv3 HMAC-SHA1-96",
|
||||
"MS_OFFICE_2016_SHEETPROTECTION": "MS Office 2016 - SheetProtection",
|
||||
"PDF_1_4_1_6_ACROBAT_5_8_USER_AND_OWNER_PASS": "PDF 1.4 - 1.6 (Acrobat 5 - 8) - user and owner pass",
|
||||
"STARGAZER_STELLAR_WALLET_XLM": "Stargazer Stellar Wallet XLM",
|
||||
"BCRYPT_MD5_PASS": "bcrypt(md5($pass))",
|
||||
"MURMUR": "MurmurHash",
|
||||
"BCRYPT_SHA1_PASS": "bcrypt(sha1($pass))",
|
||||
"KNX_IP_SECURE_DEVICE_AUTHENTICATION_CODE": "KNX IP Secure - Device Authentication Code",
|
||||
"MOZILLA_KEY3_DB": "Mozilla key3.db",
|
||||
"MOZILLA_KEY4_DB": "Mozilla key4.db",
|
||||
"OPENEDGE_PROGRESS_ENCODE": "OpenEdge Progress Encode",
|
||||
"FORTIGATE256_FORTIOS256": "FortiGate256 (FortiOS256)",
|
||||
"AES_128_ECB": "AES-128-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_192_ECB": "AES-192-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"AES_256_ECB": "AES-256-ECB NOKDF (PT = $salt, key = $pass)",
|
||||
"IPHONE_PASSCODE_UID_KEY_SYSTEM_KEYBAG": "iPhone passcode (UID key + System Keybag)",
|
||||
"METAMASK_WALLET_NEEDS_ALL_DATA_CHECKS_AES_GCM_TAG": "MetaMask Wallet (needs all data, checks AES-GCM tag)",
|
||||
"METAMASK_WALLET_SHORT_HASH_PLAINTEXT_CHECK": "MetaMask Wallet (short hash, plaintext check)",
|
||||
"SNMP_V3_HMAC_SHA224_128": "SNMPv3 HMAC-SHA224-128",
|
||||
"SNMP_V3_HMAC_SHA256_192": "SNMPv3 HMAC-SHA256-192",
|
||||
"SNMP_V3_HMAC_SHA384_256": "SNMPv3 HMAC-SHA384-256",
|
||||
"NET_NTLM_V1_NT": "NetNTLMv1 / NetNTLMv1+ESS (NT)",
|
||||
"NET_NTLM_V2_NT": "NetNTLMv2 (NT)",
|
||||
"RUBY_ON_RAILS_RESTFUL_AUTH_ONE_ROUND_NO_SITEKEY": "Ruby on Rails Restful Auth (one round, no sitekey)",
|
||||
"SNMP_V3_HMAC_SHA512_384": "SNMPv3 HMAC-SHA512-384",
|
||||
"VMWARE_VMX_PBKDF2_HMAC_SHA1_AES_256_CBC": "VMware VMX (PBKDF2-HMAC-SHA1 + AES-256-CBC)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_128_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-128-XTS)",
|
||||
"VIRTUALBOX_PBKDF2_HMAC_SHA256_AES_256_XTS": "VirtualBox (PBKDF2-HMAC-SHA256 & AES-256-XTS)",
|
||||
"MULTIBIT_CLASSIC_WALLET_SCRYPT": "MultiBit Classic .wallet (scrypt)",
|
||||
"MURMUR3": "MurmurHash3",
|
||||
"CRC32C": "CRC32C",
|
||||
"CRC64Jones": "CRC64Jones",
|
||||
"WINDOWS_HELLO_PIN": "Windows Hello PIN/Password",
|
||||
"EXODUS_DESKTOP_WALLET_SCRYPT": "Exodus Desktop Wallet (scrypt)",
|
||||
"TEAMSPEAK_3_CHANNEL_HASH": "Teamspeak 3 (channel hash)",
|
||||
"BCRYPT_SHA512_PASS": "bcrypt(sha512($pass))",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin WIF private key (P2PKH), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin WIF private key (P2PKH), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin WIF private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_WIF_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin WIF private key (P2SH(P2WPKH)), uncompressed",
|
||||
"POSTGRESQL_SCRAM_SHA_256": "PostgreSQL SCRAM-SHA-256",
|
||||
"AMAZON_AWS_SIGNATURE_VERSION_4": "Amazon AWS Signature Version 4",
|
||||
"KERBEROS_17_DB": "Kerberos 5, etype 17, DB",
|
||||
"KERBEROS_18_DB": "Kerberos 5, etype 18, DB",
|
||||
"SHA1_SALT_SHA1_UTF16LE_USERNAME_UTF16LE_PASS": "sha1($salt.sha1(utf16le($username).':'.utf16le($pass)))",
|
||||
"FLASK": "Flask Session Cookie ($salt.$salt.$pass)",
|
||||
"RADMIN_3": "Radmin3",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT": "TrueCrypt RIPEMD160 + XTS 512 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT": "TrueCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT": "TrueCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"TRUECRYPT_SHA512_XTS_512_BIT": "TrueCrypt SHA512 + XTS 512 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1024_BIT": "TrueCrypt SHA512 + XTS 1024 bit",
|
||||
"TRUECRYPT_SHA512_XTS_1536_BIT": "TrueCrypt SHA512 + XTS 1536 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_512_BIT": "TrueCrypt Whirlpool + XTS 512 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1024_BIT": "TrueCrypt Whirlpool + XTS 1024 bit",
|
||||
"TRUECRYPT_WHIRLPOOL_XTS_1536_BIT": "TrueCrypt Whirlpool + XTS 1536 bit",
|
||||
"TRUECRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"TRUECRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "TrueCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT": "VeraCrypt RIPEMD160 + XTS 512 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT": "VeraCrypt RIPEMD160 + XTS 1024 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT": "VeraCrypt RIPEMD160 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA512_XTS_512_BIT": "VeraCrypt SHA512 + XTS 512 bit",
|
||||
"VERACRYPT_SHA512_XTS_1024_BIT": "VeraCrypt SHA512 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA512_XTS_1536_BIT": "VeraCrypt SHA512 + XTS 1536 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_512_BIT": "VeraCrypt Whirlpool + XTS 512 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1024_BIT": "VeraCrypt Whirlpool + XTS 1024 bit",
|
||||
"VERACRYPT_WHIRLPOOL_XTS_1536_BIT": "VeraCrypt Whirlpool + XTS 1536 bit",
|
||||
"VERACRYPT_RIPEMD160_XTS_512_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1024_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_RIPEMD160_XTS_1536_BIT_BOOT_MODE": "VeraCrypt RIPEMD160 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT": "VeraCrypt SHA256 + XTS 512 bit",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT": "VeraCrypt SHA256 + XTS 1024 bit",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT": "VeraCrypt SHA256 + XTS 1536 bit",
|
||||
"VERACRYPT_SHA256_XTS_512_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1024_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_SHA256_XTS_1536_BIT_BOOT_MODE": "VeraCrypt SHA256 + XTS 1536 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT": "VeraCrypt Streebog-512 + XTS 512 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT": "VeraCrypt Streebog-512 + XTS 1024 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT": "VeraCrypt Streebog-512 + XTS 1536 bit",
|
||||
"VERACRYPT_STREEBOG_512_XTS_512_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 512 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1024_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1024 bit + boot-mode",
|
||||
"VERACRYPT_STREEBOG_512_XTS_1536_BIT_BOOT_MODE": "VeraCrypt Streebog-512 + XTS 1536 bit + boot-mode",
|
||||
"LUKS_V1_SHA_1_AES": "LUKS v1 SHA-1 + AES",
|
||||
"LUKS_V1_SHA_1_SERPENT": "LUKS v1 SHA-1 + Serpent",
|
||||
"LUKS_V1_SHA_1_TWOFISH": "LUKS v1 SHA-1 + Twofish",
|
||||
"LUKS_V1_SHA_256_AES": "LUKS v1 SHA-256 + AES",
|
||||
"LUKS_V1_SHA_256_SERPENT": "LUKS v1 SHA-256 + Serpent",
|
||||
"LUKS_V1_SHA_256_TWOFISH": "LUKS v1 SHA-256 + Twofish",
|
||||
"LUKS_V1_SHA_512_AES": "LUKS v1 SHA-512 + AES",
|
||||
"LUKS_V1_SHA_512_SERPENT": "LUKS v1 SHA-512 + Serpent",
|
||||
"LUKS_V1_SHA_512_TWOFISH": "LUKS v1 SHA-512 + Twofish",
|
||||
"LUKS_V1_RIPEMD_160_AES": "LUKS v1 RIPEMD-160 + AES",
|
||||
"LUKS_V1_RIPEMD_160_SERPENT": "LUKS v1 RIPEMD-160 + Serpent",
|
||||
"LUKS_V1_RIPEMD_160_TWOFISH": "LUKS v1 RIPEMD-160 + Twofish",
|
||||
"TERRA_STATION_WALLET_AES256_CBC_PBKDF2_PASS": "Terra Station Wallet (AES256-CBC(PBKDF2($pass)))",
|
||||
"KEEPASS_KDBX_V2_V3_KEYFILE_ONLY": "KeePass (KDBX v2/v3) - keyfile only",
|
||||
"BISQ_WALLET_SCRYPT": "Bisq .wallet (scrypt)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_NO_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_PBKDF2_KEYCHAIN": "ENCsecurity Datavault (PBKDF2/keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_NO_KEYCHAIN": "ENCsecurity Datavault (MD5/no keychain)",
|
||||
"ENCSECURITY_DATAVAULT_MD5_KEYCHAIN": "ENCsecurity Datavault (MD5/keychain)",
|
||||
"PYTHON_WERKZEUG_MD5_HMAC_MD5_KEY_SALT": "Python Werkzeug MD5 (HMAC-MD5 (key = $salt))",
|
||||
"PYTHON_WERKZEUG_SHA256_HMAC_SHA256_KEY_SALT": "Python Werkzeug SHA256 (HMAC-SHA256 (key = $salt))",
|
||||
"DANE_RFC7929_RFC8162_SHA2_256": "DANE RFC7929/RFC8162 SHA2-256",
|
||||
"MD5_MD5_SALT_MD5_MD5_PASS": "md5(md5($salt).md5(md5($pass)))",
|
||||
"BCRYPT_SHA256_PASS": "bcrypt(sha256($pass))",
|
||||
"BCRYPT_HMAC_SHA256_PASS": "bcrypt(HMAC-SHA256($pass))",
|
||||
"ANOPE_IRC_SERVICES_ENC_SHA256": "Anope IRC Services (enc_sha256)",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_COMPRESSED": "Bitcoin raw private key (P2PKH), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2PKH_UNCOMPRESSED": "Bitcoin raw private key (P2PKH), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_COMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2WPKH_BECH32_UNCOMPRESSED": "Bitcoin raw private key (P2WPKH, Bech32), uncompressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_COMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), compressed",
|
||||
"BITCOIN_RAW_PRIVATE_KEY_P2SH_P2WPKH_UNCOMPRESSED": "Bitcoin raw private key (P2SH(P2WPKH)), uncompressed",
|
||||
"BLAKE2S_256": "BLAKE2s-256",
|
||||
"SHANGMI_3_SM3": "ShangMi 3 (SM3)",
|
||||
"VEEAM_VBK": "Veeam VBK",
|
||||
"MS_SNTP": "MS SNTP",
|
||||
"SECURECRT_MASTERPASSPHRASE_V2": "SecureCRT MasterPassphrase v2",
|
||||
"DOMAIN_CACHED_CREDENTIALS_DCC_MS_CACHE_NT": "Domain Cached Credentials (DCC), MS Cache (NT)",
|
||||
"DOMAIN_CACHED_CREDENTIALS_2_DCC2_MS_CACHE_2_NT": "Domain Cached Credentials 2 (DCC2), MS Cache 2, (NT)",
|
||||
"MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700": "md5(md5(md5($pass).$salt1).$salt2)",
|
||||
"HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8": "1Password, mobilekeychain (1Password 8)",
|
||||
"METAMASK_MOBILE_WALLET": "MetaMask Mobile Wallet",
|
||||
"NETIQ_SSPR_MD5": "NetIQ SSPR (MD5)",
|
||||
"NETIQ_SSPR_SHA1": "NetIQ SSPR (SHA1)",
|
||||
"NETIQ_SSPR_SHA_1_WITH_SALT": "NetIQ SSPR (SHA-1 with Salt)",
|
||||
"NETIQ_SSPR_SHA_256_WITH_SALT": "NetIQ SSPR (SHA-256 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_256_WITH_SALT": "Adobe AEM (SSPR, SHA-256 with Salt)",
|
||||
"NETIQ_SSPR_SHA_512_WITH_SALT": "NetIQ SSPR (SHA-512 with Salt)",
|
||||
"ADOBE_AEM_SSPR_SHA_512_WITH_SALT": "Adobe AEM (SSPR, SHA-512 with Salt)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA1": "NetIQ SSPR (PBKDF2WithHmacSHA1)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA256": "NetIQ SSPR (PBKDF2WithHmacSHA256)",
|
||||
"NETIQ_SSPR_PBKDF2WITHHMACSHA512": "NetIQ SSPR (PBKDF2WithHmacSHA512)",
|
||||
"KERBEROS_5_ETYPE_17_AS_REP": "Kerberos 5, etype 17, AS-REP",
|
||||
"KERBEROS_5_ETYPE_18_AS_REP": "Kerberos 5, etype 18, AS-REP",
|
||||
"EMPIRE_CMS_ADMIN_PASSWORD": "Empire CMS (Admin password)",
|
||||
"SHA512_SHA512_PASS_SALT": "sha512(sha512($pass).$salt)",
|
||||
"SHA512_SHA512_BIN_PASS_SALT": "sha512(sha512_bin($pass).$salt)",
|
||||
"DOGECHAIN_INFO_WALLET": "Dogechain.info Wallet",
|
||||
"CUBECART_WHIRLPOOL_SALT_PASS_SALT": "CubeCart (whirlpool($salt.$pass.$salt))",
|
||||
"KREMLIN_ENCRYPT_3_0_W_NEWDES": "Kremlin Encrypt 3.0 w/NewDES",
|
||||
"MD5_SHA1_MD5_PASS": "md5(sha1(md5($pass)))",
|
||||
"PBKDF1_SHA1": "PBKDF1-SHA1",
|
||||
"MD5_SALT1_PASS_SALT2": "md5($salt1.$pass.$salt2)",
|
||||
"MD5_SALT_MD5_PASS_SALT_MODE_33100": "md5($salt.md5($pass).$salt)",
|
||||
"HMAC_BLAKE2S_KEY_PASS": "HMAC-BLAKE2S (key = $pass)",
|
||||
"MEGA_NZ_PASSWORD_PROTECTED_LINK_PBKDF2_HMAC_SHA512": "mega.nz password-protected link (PBKDF2-HMAC-SHA512)",
|
||||
"RC4_40_BIT_DROPN": "RC4 40-bit DropN",
|
||||
"RC4_72_BIT_DROPN": "RC4 72-bit DropN",
|
||||
"RC4_104_BIT_DROPN": "RC4 104-bit DropN",
|
||||
"RIPEMD_320": "RIPEMD-320",
|
||||
"HMAC_RIPEMD320_KEY_PASS": "HMAC-RIPEMD320 (key = $pass)",
|
||||
"HMAC_RIPEMD320_KEY_SALT": "HMAC-RIPEMD320 (key = $salt)",
|
||||
"MICROSOFT_ONLINE_ACCOUNT_PBKDF2_HMAC_SHA256_AES256": "Microsoft Online Account (PBKDF2-HMAC-SHA256 + AES256)",
|
||||
"WBB4_WOLTLAB_BURNING_BOARD_BCRYPT_BCRYPT_PASS": "WBB4 (Woltlab Burning Board) [bcrypt(bcrypt($pass))]",
|
||||
"CITRIX_NETSCALER_PBKDF2_HMAC_SHA256": "Citrix NetScaler (PBKDF2-HMAC-SHA256)",
|
||||
"ARGON2": "Argon2",
|
||||
"LUKS_V2_ARGON2_SHA_256_AES": "LUKS v2 argon2 + SHA-256 + AES",
|
||||
"MURMURHASH64A": "MurmurHash64A",
|
||||
"MURMURHASH64A_ZERO_SEED": "MurmurHash64A (zero seed)",
|
||||
"MURMURHASH64A_TRUNCATED_ZERO_SEED": "MurmurHash64A truncated (zero seed)",
|
||||
"KEEPASS_ARGON2_KDBX_V4": "KeePass Argon2 (KDBX v4)",
|
||||
"KEEPASS_AESKDF_KDBX_V4": "KeePass AESKDF (KDBX v4)",
|
||||
"SHA224_SHA224_PASS": "sha224(sha224($pass))",
|
||||
"SHA224_SHA1_PASS": "sha224(sha1($pass))",
|
||||
"MD6_256": "MD6 (256)",
|
||||
"BLOCKCHAIN_MY_WALLET_LEGACY_WALLETS": "Blockchain, My Wallet, Legacy Wallets",
|
||||
"BLAKE2B_256_MODE_34800": "BLAKE2b-256",
|
||||
"BLAKE2B_256_PASS_SALT": "BLAKE2b-256($pass.$salt)",
|
||||
"BLAKE2B_256_SALT_PASS": "BLAKE2b-256($salt.$pass)",
|
||||
"SAP_CODVN_H_PWDSALTEDHASH_ISSHA512": "SAP CODVN H (PWDSALTEDHASH) isSHA512",
|
||||
"SM3CRYPT_SM3_SM3_UNIX": "sm3crypt $sm3$, SM3 (Unix)",
|
||||
"AS_400_SSHA1": "AS/400 SSHA1",
|
||||
"KERBEROS_5_ETYPE_23_TGS_REP_NT": "Kerberos 5, etype 23, TGS-REP (NT)",
|
||||
"KERBEROS_5_ETYPE_23_AS_REP_NT": "Kerberos 5, etype 23, AS-REP (NT)",
|
||||
"WORDPRESS_BCRYPT_HMAC_SHA384_PASS": "Wordpress bcrypt(hmac-sha384($pass))",
|
||||
"GOST12512CRYPT_GOST12512HASH_UNIX": "gost12512crypt [$gost12512hash$] (Unix)",
|
||||
"PHPASS_MD5_PASS": "phpass(md5($pass))",
|
||||
"SYMFONY_LEGACY_SHA256": "Symfony Legacy SHA256",
|
||||
"YESCRYPT": "yescrypt",
|
||||
"GOST_YESCRYPT": "gost-yescrypt",
|
||||
"SCRYPTCRYPT_SCRYPT_UNIX": "scryptcrypt, scrypt (Unix)",
|
||||
"KDE_KWALLET_4_13_PBKDF2_HMAC_SHA512_BLOWFISH": "KDE KWallet 4.13+ (PBKDF2-HMAC-SHA512, Blowfish)",
|
||||
"KDE_KWALLET_4_13_SHA_1_BLOWFISH": "KDE KWallet < 4.13 (SHA-1, Blowfish)",
|
||||
"ARGON2ID_BRIDGED_REFERENCE_IMPLEMENTATION_TUNINGS": "Argon2id [Bridged: reference implementation + tunings]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_JANE_SMIX": "scrypt [Bridged: Scrypt-Jane SMix]",
|
||||
"SCRYPT_BRIDGED_SCRYPT_YESCRYPT": "scrypt [Bridged: Scrypt-Yescrypt]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_FREE_THREADING": "Generic Hash [Bridged: Python Interpreter free-threading]",
|
||||
"GENERIC_HASH_BRIDGED_PYTHON_INTERPRETER_WITH_GIL": "Generic Hash [Bridged: Python Interpreter with GIL]",
|
||||
"GENERIC_HASH_BRIDGED_RUST": "Generic Hash [Bridged: Rust]",
|
||||
"PLAINTEXT": "Plaintext",
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
const (
|
||||
pinnedHashcatVersion = "v7.1.2-armory.1"
|
||||
pinnedHashcatCommit = "deadd52de22e2753e9caaff6d1b92e5a22a35626"
|
||||
pinnedHashcatModuleCount = 595
|
||||
pinnedHashcatCatalogSHA256 = "1c041b6370a37ccf07ae71ff29205c56c67eeb7dbe1f7bbc134e0373ecd19e4f"
|
||||
pinnedHashcatModeIDsSHA256 = "bb206df105890129937d0c688101a3065aad2ca45eea7765bfda60c40f232a63"
|
||||
pinnedHashcatEnumsSHA256 = "989144b3d2a57f9659bc5aa53df42403ba78b08e5a6fab616d43c14e0352e023"
|
||||
)
|
||||
|
||||
type hashcatCatalogEntry struct {
|
||||
mode int32
|
||||
enumName string
|
||||
description string
|
||||
}
|
||||
|
||||
func TestHashcatV7CatalogCoverage(t *testing.T) {
|
||||
version := pinnedHashcatVersion + "@" + pinnedHashcatCommit
|
||||
validateHashcatCatalogCardinality(t, version)
|
||||
catalog := collectHashcatCatalog(t, version)
|
||||
validateHashTypeValueRoundTrips(t)
|
||||
validateHashTypeDescriptionParity(t)
|
||||
validateHashcatCatalogDigests(t, version, catalog)
|
||||
}
|
||||
|
||||
func validateHashcatCatalogCardinality(t *testing.T, version string) {
|
||||
t.Helper()
|
||||
|
||||
if got := len(clientpb.HashType_name); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_name contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(clientpb.HashType_value); got != pinnedHashcatModuleCount+1 {
|
||||
t.Fatalf("%s HashType_value contains %d values, want %d modules plus INVALID", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
if got := len(hashTypes); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s hashTypes contains %d descriptions, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
}
|
||||
|
||||
func collectHashcatCatalog(t *testing.T, version string) []hashcatCatalogEntry {
|
||||
t.Helper()
|
||||
|
||||
catalog := make([]hashcatCatalogEntry, 0, pinnedHashcatModuleCount)
|
||||
seenDescriptions := make(map[string]int32, pinnedHashcatModuleCount)
|
||||
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
if enumName != "INVALID" || mode != 9999 {
|
||||
t.Errorf("invalid sentinel is %d -> %q, want 9999 -> INVALID", mode, enumName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if mode < 0 {
|
||||
t.Errorf("HashType_name contains negative mode %d (%s)", mode, enumName)
|
||||
continue
|
||||
}
|
||||
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Errorf("hashTypes is missing %d -> %s", mode, enumName)
|
||||
continue
|
||||
}
|
||||
if previous, duplicate := seenDescriptions[description]; duplicate {
|
||||
t.Errorf("Hashcat name %q is shared by modes %d and %d", description, previous, mode)
|
||||
}
|
||||
seenDescriptions[description] = mode
|
||||
if got, ok := clientpb.HashType_value[enumName]; !ok || got != mode {
|
||||
t.Errorf("HashType_value[%q] = %d, %v; want %d, true", enumName, got, ok, mode)
|
||||
}
|
||||
if got := clientpb.HashType(mode).String(); got != enumName {
|
||||
t.Errorf("HashType(%d).String() = %q, want %q", mode, got, enumName)
|
||||
}
|
||||
catalog = append(catalog, hashcatCatalogEntry{mode: mode, enumName: enumName, description: description})
|
||||
}
|
||||
if got := len(catalog); got != pinnedHashcatModuleCount {
|
||||
t.Fatalf("%s catalog contains %d real modes, want %d", version, got, pinnedHashcatModuleCount)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func validateHashTypeValueRoundTrips(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, mode := range clientpb.HashType_value {
|
||||
canonical, ok := clientpb.HashType_name[mode]
|
||||
if !ok || canonical != enumName {
|
||||
t.Errorf("HashType alias or orphan %q -> %d; canonical name is %q, %v", enumName, mode, canonical, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashTypeDescriptionParity(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for enumName, description := range hashTypes {
|
||||
mode, ok := clientpb.HashType_value[enumName]
|
||||
if !ok || mode == int32(clientpb.HashType_INVALID) {
|
||||
t.Errorf("hashTypes contains non-catalog key %q -> %q", enumName, description)
|
||||
}
|
||||
if description == "" {
|
||||
t.Errorf("hashTypes[%q] has an empty description", enumName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateHashcatCatalogDigests(t *testing.T, version string, catalog []hashcatCatalogEntry) {
|
||||
t.Helper()
|
||||
|
||||
sort.Slice(catalog, func(i, j int) bool { return catalog[i].mode < catalog[j].mode })
|
||||
var catalogRows strings.Builder
|
||||
var modeRows strings.Builder
|
||||
var enumRows strings.Builder
|
||||
for _, entry := range catalog {
|
||||
fmt.Fprintf(&catalogRows, "%d\t%s\n", entry.mode, entry.description)
|
||||
fmt.Fprintf(&modeRows, "%05d\n", entry.mode)
|
||||
fmt.Fprintf(&enumRows, "%d\t%s\t%s\n", entry.mode, entry.enumName, entry.description)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(catalogRows.String()))); got != pinnedHashcatCatalogSHA256 {
|
||||
t.Errorf("%s mode/name catalog SHA-256 = %s, want %s", version, got, pinnedHashcatCatalogSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(modeRows.String()))); got != pinnedHashcatModeIDsSHA256 {
|
||||
t.Errorf("%s mode ID SHA-256 = %s, want %s", version, got, pinnedHashcatModeIDsSHA256)
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(enumRows.String()))); got != pinnedHashcatEnumsSHA256 {
|
||||
t.Errorf("%s mode/enum/name SHA-256 = %s, want %s", version, got, pinnedHashcatEnumsSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashcatIdentifierCollisionResolution(t *testing.T) {
|
||||
want := map[int32]string{
|
||||
141: "EPISERVER_6_X_NET_4_MODE_141",
|
||||
600: "BLAKE2B_256",
|
||||
1441: "EPISERVER_6_X_NET_4_MODE_1441",
|
||||
2611: "VBULLETIN_V3_8_5_MODE_2611",
|
||||
2711: "VBULLETIN_V3_8_5_MODE_2711",
|
||||
4110: "MD5_SALT_MD5_PASS_SALT_MODE_4110",
|
||||
4410: "MD5_SHA1_PASS_SALT_MODE_4410",
|
||||
4420: "MD5_SHA1_PASS_SALT_MODE_4420",
|
||||
4710: "SHA1_MD5_PASS_SALT_MODE_4710",
|
||||
14700: "ITUNES_BACKUP_10_0_MODE_14700",
|
||||
14800: "ITUNES_BACKUP_10_0_MODE_14800",
|
||||
20710: "SHA256_SHA256_PASS_SALT_MODE_20710",
|
||||
20730: "SHA256_SHA256_PASS_SALT_MODE_20730",
|
||||
21100: "SHA1_MD5_PASS_SALT_MODE_21100",
|
||||
21900: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_21900",
|
||||
31700: "MD5_MD5_MD5_PASS_SALT1_SALT2_MODE_31700",
|
||||
31800: "HASHCAT_1PASSWORD_MOBILEKEYCHAIN_1PASSWORD_8",
|
||||
33100: "MD5_SALT_MD5_PASS_SALT_MODE_33100",
|
||||
34800: "BLAKE2B_256_MODE_34800",
|
||||
6600: "HASHCAT_1PASSWORD_AGILEKEYCHAIN",
|
||||
8200: "HASHCAT_1PASSWORD_CLOUDKEYCHAIN",
|
||||
11600: "HASHCAT_7_ZIP",
|
||||
}
|
||||
for mode, enumName := range want {
|
||||
if got := clientpb.HashType_name[mode]; got != enumName {
|
||||
t.Errorf("HashType_name[%d] = %q, want collision-safe %q", mode, got, enumName)
|
||||
}
|
||||
}
|
||||
|
||||
groups := map[string][]int32{}
|
||||
for mode, enumName := range clientpb.HashType_name {
|
||||
if mode == int32(clientpb.HashType_INVALID) {
|
||||
continue
|
||||
}
|
||||
description, ok := hashTypes[enumName]
|
||||
if !ok {
|
||||
t.Fatalf("hashTypes is missing collision input %d -> %s", mode, enumName)
|
||||
}
|
||||
base := normalizeHashcatIdentifier(description)
|
||||
groups[base] = append(groups[base], mode)
|
||||
}
|
||||
collisions := 0
|
||||
for _, modes := range groups {
|
||||
if len(modes) > 1 {
|
||||
collisions++
|
||||
}
|
||||
}
|
||||
if collisions != 8 {
|
||||
t.Errorf("normalized catalog contains %d collision groups, want 8", collisions)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeHashcatIdentifier(name string) string {
|
||||
var builder strings.Builder
|
||||
underscore := false
|
||||
for _, r := range strings.ToUpper(name) {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
underscore = false
|
||||
continue
|
||||
}
|
||||
if builder.Len() > 0 && !underscore {
|
||||
builder.WriteByte('_')
|
||||
underscore = true
|
||||
}
|
||||
}
|
||||
return strings.Trim(builder.String(), "_")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cursed
|
||||
|
||||
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"
|
||||
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
|
||||
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
|
||||
|
||||
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
|
||||
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
|
||||
|
||||
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("save", "s", "", "save to file")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
|
||||
|
||||
cursedScreenshotCmd := &cobra.Command{
|
||||
Use: consts.ScreenshotStr,
|
||||
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
|
||||
f.StringP("save", "s", "", "save to file")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
|
||||
|
||||
return []*cobra.Command{cursedCmd}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func CursedChromeCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
if chromeExt != nil {
|
||||
con.Printf("success!\n")
|
||||
con.PrintInfof("Found viable Chrome extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
||||
con.PrintInfof("Found viable Chrome extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||
con.PrintInfof("Injecting payload ... ")
|
||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||
@@ -120,7 +120,7 @@ func avadaKedavraChrome(session *clientpb.Session, cmd *cobra.Command, con *cons
|
||||
if chromeProcess != nil {
|
||||
con.PrintWarnf("Found running Chrome process: %d (ppid: %d)\n", chromeProcess.GetPid(), chromeProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the Chrome process in order to perform code injection.\n")
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
|
||||
@@ -228,6 +228,14 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, chromeExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -241,7 +249,7 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func selectDebugTarget(targets []overlord.ChromeDebugTarget, con *console.Sliver
|
||||
if selected == "" {
|
||||
return nil
|
||||
}
|
||||
selectedID := strings.Split(selected, " ")[0]
|
||||
selectedID, _, _ := strings.Cut(selected, " ")
|
||||
selectedTarget := id2target[selectedID]
|
||||
return &selectedTarget
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
||||
tmpFile, _ := os.CreateTemp("", "cursed")
|
||||
shell := readline.NewShell()
|
||||
shell.History.AddFromFile("cursed history", tmpFile.Name())
|
||||
shell.Prompt.Primary(func() string { return "\033[31mcursed »\033[0m " })
|
||||
shell.Prompt.Primary(func() string { return console.StyleRed.Render("cursed »") + " " })
|
||||
// EOFPrompt: "exit",
|
||||
|
||||
if con.Settings.VimMode {
|
||||
@@ -111,7 +111,7 @@ func startCursedConsole(curse *core.CursedProcess, helpers bool, target *overlor
|
||||
}
|
||||
}
|
||||
|
||||
con.Printf(console.Bold+">>> Cursed Console, use ':help' for options%s\n\n", console.Normal)
|
||||
con.Printf("%s\n\n", console.StyleBold.Render(">>> Cursed Console, use ':help' for options"))
|
||||
|
||||
for {
|
||||
line, err := shell.Readline()
|
||||
|
||||
@@ -77,7 +77,7 @@ func CursedEdgeCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
if chromeExt != nil {
|
||||
con.Printf("success!\n")
|
||||
con.PrintInfof("Found viable Edge extension %s%s%s (%s)\n", console.Bold, chromeExt.Title, console.Normal, chromeExt.ID)
|
||||
con.PrintInfof("Found viable Edge extension %s (%s)\n", console.StyleBold.Render(chromeExt.Title), chromeExt.ID)
|
||||
con.PrintInfof("Injecting payload ... ")
|
||||
cmd, _, _ := overlord.GetChromeContext(chromeExt.WebSocketDebuggerURL, curse)
|
||||
// extCtxTimeout, cancel := context.WithTimeout(cmd, 10*time.Second)
|
||||
@@ -103,7 +103,7 @@ func avadaKedavraEdge(session *clientpb.Session, cmd *cobra.Command, con *consol
|
||||
if edgeProcess != nil {
|
||||
con.PrintWarnf("Found running Edge process: %d (ppid: %d)\n", edgeProcess.GetPid(), edgeProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the Edge process in order to perform code injection.\n")
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
|
||||
|
||||
@@ -20,6 +20,7 @@ package cursed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
@@ -85,7 +86,7 @@ func avadaKedavraElectron(electronExe string, session *clientpb.Session, cmd *co
|
||||
if electronProcess != nil {
|
||||
con.PrintWarnf("Found running '%s' process: %d (ppid: %d)\n", path.Base(electronExe), electronProcess.GetPid(), electronProcess.GetPpid())
|
||||
con.PrintWarnf("Sliver will need to kill and restart the process in order to perform code injection.\n")
|
||||
con.PrintWarnf("%sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.PrintWarnf("%s\n", console.StyleBold.Render("DATA LOSS MAY OCCUR!"))
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = forms.Confirm("Kill and restart the process?", &confirm)
|
||||
@@ -190,6 +191,14 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
DialTimeout: 30 * time.Second,
|
||||
}
|
||||
tcpProxy.AddRoute(bindAddr, channelProxy)
|
||||
if err := tcpProxy.Start(); err != nil {
|
||||
channelProxy.Stop()
|
||||
_ = tcpProxy.Close()
|
||||
return nil, errors.Join(
|
||||
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
|
||||
terminateStartedCursedProcess(con, session.ID, electronExec.GetPid()),
|
||||
)
|
||||
}
|
||||
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
|
||||
|
||||
curse := &core.CursedProcess{
|
||||
@@ -202,7 +211,7 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
|
||||
}
|
||||
core.CursedProcesses.Store(bindPort, curse)
|
||||
go func() {
|
||||
err := tcpProxy.Run()
|
||||
err := tcpProxy.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Proxy error %s", err)
|
||||
}
|
||||
|
||||
@@ -20,20 +20,47 @@ package cursed
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const cursedStartCleanupTimeout = 30 * time.Second
|
||||
|
||||
func terminateStartedCursedProcess(con *console.SliverClient, sessionID string, pid uint32) error {
|
||||
const maxTerminatePID = uint32(1<<31 - 1)
|
||||
if con == nil || con.Rpc == nil || sessionID == "" || pid == 0 || pid > maxTerminatePID {
|
||||
return errors.New("cannot terminate incompletely tracked cursed process")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cursedStartCleanupTimeout)
|
||||
defer cancel()
|
||||
response, err := con.Rpc.Terminate(ctx, &sliverpb.TerminateReq{
|
||||
Request: &commonpb.Request{SessionID: sessionID},
|
||||
Pid: int32(pid),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("terminate started cursed process %d: %w", pid, err)
|
||||
}
|
||||
if response.GetResponse().GetErr() != "" {
|
||||
return fmt.Errorf("terminate started cursed process %d: %s", pid, response.GetResponse().GetErr())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CursedChromeCmd - Execute a .NET assembly in-memory.
|
||||
func CursedCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
// Collect existing curses from core
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dllhijack
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["file"] = carapace.ActionFiles()
|
||||
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(dllhijackCmd, "reference-file", "file")
|
||||
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
|
||||
|
||||
return []*cobra.Command{dllhijackCmd}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Commands returns the docs command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.SliverCoreHelpGroup, con)}
|
||||
}
|
||||
|
||||
// ServerCommands returns the docs command for the top-level client REPL.
|
||||
func ServerCommands(con *console.SliverClient) []*cobra.Command {
|
||||
return []*cobra.Command{newDocsCommand(consts.GenericHelpGroup, con)}
|
||||
}
|
||||
|
||||
func newDocsCommand(groupID string, con *console.SliverClient) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: consts.DocsStr,
|
||||
Short: "Browse the embedded Sliver docs in a TUI",
|
||||
Long: help.GetHelpFor([]string{consts.DocsStr}),
|
||||
Args: cobra.NoArgs,
|
||||
GroupID: groupID,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
DocsCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// DocsCmd launches the embedded docs browser TUI.
|
||||
func DocsCmd(_ *cobra.Command, con *console.SliverClient, _ []string) {
|
||||
entries, err := loadDocEntries()
|
||||
if err != nil {
|
||||
if con != nil {
|
||||
con.PrintErrorf("Failed to load embedded docs: %s\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
model := newDocsModel(entries)
|
||||
|
||||
width, height := 100, 32
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
width, height = w, h
|
||||
}
|
||||
|
||||
program := tea.NewProgram(
|
||||
model,
|
||||
tea.WithWindowSize(width, height),
|
||||
tea.WithColorProfile(colorprofile.TrueColor),
|
||||
)
|
||||
if _, err := program.Run(); err != nil && con != nil {
|
||||
con.PrintErrorf("Docs TUI error: %s\n", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func sampleDocs() []docEntry {
|
||||
return []docEntry{
|
||||
{
|
||||
Name: "Getting Started",
|
||||
Content: "# Getting Started\n\n- first step\n- second step",
|
||||
Description: "Getting Started",
|
||||
},
|
||||
{
|
||||
Name: "Operators",
|
||||
Content: "# Operators\n\nTeam workflows",
|
||||
Description: "Team workflows",
|
||||
},
|
||||
{
|
||||
Name: "Networking",
|
||||
Content: "# Networking\n\nTransport details",
|
||||
Description: "Transport details",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderMarkdownWithGlowRendersBulletLists(t *testing.T) {
|
||||
rendered, err := renderMarkdownWithGlow(48, "# Title\n\n- first item")
|
||||
if err != nil {
|
||||
t.Fatalf("renderMarkdownWithGlow(): %v", err)
|
||||
}
|
||||
|
||||
plain := ansi.Strip(rendered)
|
||||
if !strings.Contains(plain, "Title") {
|
||||
t.Fatalf("expected heading text, got %q", plain)
|
||||
}
|
||||
if !strings.Contains(plain, "• first item") {
|
||||
t.Fatalf("expected glow-style bullet rendering, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownPrefersFirstMeaningfulLine(t *testing.T) {
|
||||
summary := summarizeMarkdown("# Title\n\n- Bullet\n\nParagraph")
|
||||
if summary != "Title" {
|
||||
t.Fatalf("expected first heading summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizeMarkdownStripsMarkdownAndUnicodeSymbols(t *testing.T) {
|
||||
summary := summarizeMarkdown("⚠️ **IMPORTANT:** This content was NOT created by the Sliver authors.")
|
||||
if summary != "IMPORTANT: This content was NOT created by the Sliver authors." {
|
||||
t.Fatalf("expected sanitized browser summary, got %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelDefaultsToGettingStarted(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
if model.currentDocName != "Getting Started" {
|
||||
t.Fatalf("expected Getting Started to be selected, got %q", model.currentDocName)
|
||||
}
|
||||
plain := ansi.Strip(model.viewer.View())
|
||||
if !strings.Contains(plain, "first step") {
|
||||
t.Fatalf("expected initial viewer content to render selected doc, got %q", plain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelSlashFocusesBrowserAndStartsFiltering(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
model.focus = docsFocusViewer
|
||||
|
||||
updated, cmd := model.Update(tea.KeyPressMsg{Text: "/", Code: '/'})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected slash to focus the search input")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusBrowser {
|
||||
t.Fatalf("expected slash to move focus to browser, got %v", updatedModel.focus)
|
||||
}
|
||||
if updatedModel.filterState != docsFilterStateFiltering {
|
||||
t.Fatalf("expected slash to enter filtering state, got %v", updatedModel.filterState)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelFilteringUpdatesVisibleEntries(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("oper")
|
||||
model.updateFilteredEntries(false)
|
||||
|
||||
visible := model.visibleEntries()
|
||||
if len(visible) != 1 || visible[0].Name != "Operators" {
|
||||
t.Fatalf("expected Operators to be the only filtered result, got %+v", visible)
|
||||
}
|
||||
if model.currentDocName != "Operators" {
|
||||
t.Fatalf("expected filtering to sync selection to Operators, got %q", model.currentDocName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEnterMovesFocusToViewer(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.focus != docsFocusViewer {
|
||||
t.Fatalf("expected enter to focus viewer, got %v", updatedModel.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelEscClearsAppliedFilter(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 32)
|
||||
|
||||
model.startFiltering()
|
||||
model.browserFilter.SetValue("net")
|
||||
model.finishFiltering(false)
|
||||
|
||||
updated, _ := model.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.filterState != docsFilterStateUnfiltered {
|
||||
t.Fatalf("expected esc to clear the filter, got %v", updatedModel.filterState)
|
||||
}
|
||||
if len(updatedModel.visibleEntries()) != len(sampleDocs()) {
|
||||
t.Fatalf("expected filter clear to restore all docs, got %d visible", len(updatedModel.visibleEntries()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsModelBrowserBodyPinsHelpToBottom(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(120, 28)
|
||||
|
||||
body := ansi.Strip(model.renderBrowserBody(model.browserWidth, model.browserHeight))
|
||||
lines := strings.Split(body, "\n")
|
||||
if len(lines) != model.browserHeight {
|
||||
t.Fatalf("expected browser body height %d, got %d lines", model.browserHeight, len(lines))
|
||||
}
|
||||
if !strings.Contains(lines[len(lines)-1], "q quit") {
|
||||
t.Fatalf("expected help controls on the last browser line, got %q", lines[len(lines)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsWindowPollSchedulesResizeAndKeepsPolling(t *testing.T) {
|
||||
model := newDocsModel(sampleDocs())
|
||||
model.applyWindowSize(100, 28)
|
||||
|
||||
updated, cmd := model.Update(docsWindowPollMsg{width: 120, height: 34})
|
||||
if cmd == nil {
|
||||
t.Fatal("expected window poll to schedule resize handling")
|
||||
}
|
||||
|
||||
updatedModel := updated.(*docsModel)
|
||||
if updatedModel.width != 100 || updatedModel.height != 28 {
|
||||
t.Fatalf("expected poll tick to leave dimensions unchanged until WindowSizeMsg, got %dx%d", updatedModel.width, updatedModel.height)
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
batch, ok := msg.(tea.BatchMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected poll tick to return tea.BatchMsg, got %T", msg)
|
||||
}
|
||||
if len(batch) != 2 {
|
||||
t.Fatalf("expected poll tick batch size 2, got %d", len(batch))
|
||||
}
|
||||
|
||||
var sawWindowSize bool
|
||||
var sawPollTick bool
|
||||
for _, subcmd := range batch {
|
||||
if subcmd == nil {
|
||||
continue
|
||||
}
|
||||
submsg := subcmd()
|
||||
switch msg := submsg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
sawWindowSize = true
|
||||
if msg.Width != 120 || msg.Height != 34 {
|
||||
t.Fatalf("expected resize 120x34, got %dx%d", msg.Width, msg.Height)
|
||||
}
|
||||
case docsWindowPollMsg:
|
||||
sawPollTick = true
|
||||
}
|
||||
}
|
||||
|
||||
if !sawWindowSize {
|
||||
t.Fatal("expected poll tick to emit WindowSizeMsg")
|
||||
}
|
||||
if !sawPollTick {
|
||||
t.Fatal("expected poll tick to schedule another poll")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package edit
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Commands returns the edit command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
editCmd := &cobra.Command{
|
||||
Use: consts.EditStr,
|
||||
Short: "Edit a remote text file",
|
||||
Long: help.GetHelpFor([]string{consts.EditStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
EditCmd(cmd, con, args)
|
||||
},
|
||||
GroupID: consts.FilesystemHelpGroup,
|
||||
}
|
||||
|
||||
flags.Bind("", false, editCmd, func(f *pflag.FlagSet) {
|
||||
f.String("syntax", "", "syntax highlighting lexer name (optional)")
|
||||
f.Bool("syntax-select", false, "prompt to select syntax highlighting lexer")
|
||||
f.Bool("line-numbers", false, "show line numbers")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
carapace.Gen(editCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to the remote file to edit"),
|
||||
)
|
||||
|
||||
return []*cobra.Command{editCmd}
|
||||
}
|
||||
Loaded 100 of 7645 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user